django-ledger 0.8.2.1__py3-none-any.whl → 0.8.2.2__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.
Potentially problematic release.
This version of django-ledger might be problematic. Click here for more details.
- django_ledger/__init__.py +1 -1
- django_ledger/forms/data_import.py +63 -110
- django_ledger/io/roles.py +238 -303
- django_ledger/migrations/0027_alter_accountmodel_role_alter_receiptmodel_amount_and_more.py +159 -0
- django_ledger/models/chart_of_accounts.py +32 -90
- django_ledger/models/coa_default.py +1 -0
- django_ledger/models/customer.py +17 -26
- django_ledger/models/data_import.py +457 -153
- django_ledger/models/receipt.py +90 -121
- django_ledger/templates/django_ledger/data_import/data_import_job_txs.html +6 -0
- django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_imported.html +66 -36
- django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_table.html +10 -10
- django_ledger/urls/data_import.py +3 -0
- django_ledger/views/data_import.py +37 -21
- {django_ledger-0.8.2.1.dist-info → django_ledger-0.8.2.2.dist-info}/METADATA +1 -1
- {django_ledger-0.8.2.1.dist-info → django_ledger-0.8.2.2.dist-info}/RECORD +20 -19
- {django_ledger-0.8.2.1.dist-info → django_ledger-0.8.2.2.dist-info}/WHEEL +0 -0
- {django_ledger-0.8.2.1.dist-info → django_ledger-0.8.2.2.dist-info}/licenses/AUTHORS.md +0 -0
- {django_ledger-0.8.2.1.dist-info → django_ledger-0.8.2.2.dist-info}/licenses/LICENSE +0 -0
- {django_ledger-0.8.2.1.dist-info → django_ledger-0.8.2.2.dist-info}/top_level.txt +0 -0
django_ledger/models/receipt.py
CHANGED
|
@@ -76,8 +76,7 @@ class ReceiptModelQuerySet(QuerySet):
|
|
|
76
76
|
manage.
|
|
77
77
|
"""
|
|
78
78
|
return self.filter(
|
|
79
|
-
Q(ledger_model__entity__admin=user_model)
|
|
80
|
-
| Q(ledger_model__entity__managers__in=[user_model])
|
|
79
|
+
Q(ledger_model__entity__admin=user_model) | Q(ledger_model__entity__managers__in=[user_model])
|
|
81
80
|
)
|
|
82
81
|
|
|
83
82
|
def for_dates(self, from_date, to_date) -> 'ReceiptModelQuerySet':
|
|
@@ -97,9 +96,7 @@ class ReceiptModelQuerySet(QuerySet):
|
|
|
97
96
|
"""
|
|
98
97
|
return self.filter(receipt_date__gte=from_date, receipt_date__lte=to_date)
|
|
99
98
|
|
|
100
|
-
def for_vendor(
|
|
101
|
-
self, vendor_model: VendorModel | str | UUID
|
|
102
|
-
) -> 'ReceiptModelQuerySet':
|
|
99
|
+
def for_vendor(self, vendor_model: VendorModel | str | UUID) -> 'ReceiptModelQuerySet':
|
|
103
100
|
"""Filter receipts tied to a specific vendor.
|
|
104
101
|
|
|
105
102
|
Parameters
|
|
@@ -134,14 +131,10 @@ class ReceiptModelQuerySet(QuerySet):
|
|
|
134
131
|
customer_model__isnull=True,
|
|
135
132
|
)
|
|
136
133
|
raise ReceiptModelValidationError(
|
|
137
|
-
'Invalid Vendor Model: {}, must be instance of VendorModel, UUID, str'.format(
|
|
138
|
-
vendor_model
|
|
139
|
-
)
|
|
134
|
+
'Invalid Vendor Model: {}, must be instance of VendorModel, UUID, str'.format(vendor_model)
|
|
140
135
|
)
|
|
141
136
|
|
|
142
|
-
def for_customer(
|
|
143
|
-
self, customer_model: CustomerModel | str | UUID
|
|
144
|
-
) -> 'ReceiptModelQuerySet':
|
|
137
|
+
def for_customer(self, customer_model: CustomerModel | str | UUID) -> 'ReceiptModelQuerySet':
|
|
145
138
|
"""Filter receipts tied to a specific customer.
|
|
146
139
|
|
|
147
140
|
Parameters
|
|
@@ -176,9 +169,7 @@ class ReceiptModelQuerySet(QuerySet):
|
|
|
176
169
|
vendor_model__isnull=True,
|
|
177
170
|
)
|
|
178
171
|
raise ReceiptModelValidationError(
|
|
179
|
-
'Invalid Customer Model: {}, must be instance of CustomerModel, UUID, str'.format(
|
|
180
|
-
customer_model
|
|
181
|
-
)
|
|
172
|
+
'Invalid Customer Model: {}, must be instance of CustomerModel, UUID, str'.format(customer_model)
|
|
182
173
|
)
|
|
183
174
|
|
|
184
175
|
|
|
@@ -202,9 +193,7 @@ class ReceiptModelManager(Manager):
|
|
|
202
193
|
)
|
|
203
194
|
)
|
|
204
195
|
|
|
205
|
-
def for_entity(
|
|
206
|
-
self, entity_model: EntityModel | str | UUID
|
|
207
|
-
) -> ReceiptModelQuerySet:
|
|
196
|
+
def for_entity(self, entity_model: EntityModel | str | UUID) -> ReceiptModelQuerySet:
|
|
208
197
|
"""Filter receipts for a specific entity.
|
|
209
198
|
|
|
210
199
|
Parameters
|
|
@@ -250,6 +239,7 @@ class ReceiptModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn, IOMixIn):
|
|
|
250
239
|
EXPENSE_RECEIPT = 'expense'
|
|
251
240
|
EXPENSE_REFUND = 'expense_refund'
|
|
252
241
|
TRANSFER_RECEIPT = 'transfer'
|
|
242
|
+
DEBT_PAYMENT = 'debt_paydown'
|
|
253
243
|
|
|
254
244
|
RECEIPT_TYPES = [
|
|
255
245
|
(SALES_RECEIPT, 'Sales Receipt'),
|
|
@@ -257,15 +247,14 @@ class ReceiptModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn, IOMixIn):
|
|
|
257
247
|
(EXPENSE_RECEIPT, 'Expense Receipt'),
|
|
258
248
|
(EXPENSE_REFUND, 'Expense Refund'),
|
|
259
249
|
(TRANSFER_RECEIPT, 'Transfer Receipt'),
|
|
250
|
+
(DEBT_PAYMENT, 'Debt Paydown Receipt'),
|
|
260
251
|
]
|
|
261
252
|
RECEIPT_TYPES_MAP = dict(RECEIPT_TYPES)
|
|
262
253
|
|
|
263
254
|
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
|
|
264
255
|
receipt_number = models.CharField(_('Receipt Number'), max_length=255)
|
|
265
256
|
receipt_date = models.DateField(_('Receipt Date'))
|
|
266
|
-
receipt_type = models.CharField(
|
|
267
|
-
choices=RECEIPT_TYPES, verbose_name=_('Receipt Type')
|
|
268
|
-
)
|
|
257
|
+
receipt_type = models.CharField(choices=RECEIPT_TYPES, max_length=15, verbose_name=_('Receipt Type'))
|
|
269
258
|
|
|
270
259
|
ledger_model = models.ForeignKey(
|
|
271
260
|
'django_ledger.LedgerModel',
|
|
@@ -278,9 +267,7 @@ class ReceiptModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn, IOMixIn):
|
|
|
278
267
|
'django_ledger.EntityUnitModel',
|
|
279
268
|
on_delete=models.PROTECT,
|
|
280
269
|
verbose_name=_('Unit Model'),
|
|
281
|
-
help_text=_(
|
|
282
|
-
'Helps segregate receipts and transactions into different classes or departments.'
|
|
283
|
-
),
|
|
270
|
+
help_text=_('Helps segregate receipts and transactions into different classes or departments.'),
|
|
284
271
|
null=True,
|
|
285
272
|
blank=True,
|
|
286
273
|
)
|
|
@@ -304,9 +291,7 @@ class ReceiptModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn, IOMixIn):
|
|
|
304
291
|
'django_ledger.AccountModel',
|
|
305
292
|
on_delete=models.PROTECT,
|
|
306
293
|
verbose_name=_('Charge Account'),
|
|
307
|
-
help_text=_(
|
|
308
|
-
'The financial account (cash or credit) where this transaction was made.'
|
|
309
|
-
),
|
|
294
|
+
help_text=_('The financial account (cash or credit) where this transaction was made.'),
|
|
310
295
|
related_name='charge_receiptmodel_set',
|
|
311
296
|
)
|
|
312
297
|
|
|
@@ -314,9 +299,9 @@ class ReceiptModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn, IOMixIn):
|
|
|
314
299
|
'django_ledger.AccountModel',
|
|
315
300
|
on_delete=models.PROTECT,
|
|
316
301
|
verbose_name=_('PnL Account'),
|
|
317
|
-
help_text=_(
|
|
318
|
-
|
|
319
|
-
|
|
302
|
+
help_text=_('The income or expense account where this transaction will be reflected'),
|
|
303
|
+
null=True,
|
|
304
|
+
blank=True,
|
|
320
305
|
)
|
|
321
306
|
|
|
322
307
|
amount = models.DecimalField(
|
|
@@ -324,7 +309,7 @@ class ReceiptModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn, IOMixIn):
|
|
|
324
309
|
max_digits=20,
|
|
325
310
|
verbose_name=_('Receipt Amount'),
|
|
326
311
|
help_text=_('Amount of the receipt.'),
|
|
327
|
-
validators=[MinValueValidator(0)],
|
|
312
|
+
validators=[MinValueValidator(limit_value=0)],
|
|
328
313
|
)
|
|
329
314
|
|
|
330
315
|
staged_transaction_model = models.OneToOneField(
|
|
@@ -333,9 +318,7 @@ class ReceiptModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn, IOMixIn):
|
|
|
333
318
|
null=True,
|
|
334
319
|
blank=True,
|
|
335
320
|
verbose_name=_('Staged Transaction Model'),
|
|
336
|
-
help_text=_(
|
|
337
|
-
'The staged transaction associated with the receipt from bank feeds.'
|
|
338
|
-
),
|
|
321
|
+
help_text=_('The staged transaction associated with the receipt from bank feeds.'),
|
|
339
322
|
)
|
|
340
323
|
|
|
341
324
|
objects = ReceiptModelManager.from_queryset(queryset_class=ReceiptModelQuerySet)()
|
|
@@ -438,9 +421,7 @@ class ReceiptModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn, IOMixIn):
|
|
|
438
421
|
]
|
|
439
422
|
)
|
|
440
423
|
|
|
441
|
-
def delete(
|
|
442
|
-
self, using=None, keep_parents=False, delete_ledger: bool = True, **kwargs
|
|
443
|
-
):
|
|
424
|
+
def delete(self, using=None, keep_parents=False, delete_ledger: bool = True, **kwargs):
|
|
444
425
|
"""Delete the receipt and related journal entries if allowed.
|
|
445
426
|
|
|
446
427
|
Parameters
|
|
@@ -467,9 +448,7 @@ class ReceiptModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn, IOMixIn):
|
|
|
467
448
|
"""
|
|
468
449
|
if not self.can_delete():
|
|
469
450
|
raise ReceiptModelValidationError(
|
|
470
|
-
message=_(
|
|
471
|
-
'Receipt cannot be deleted because it falls within a closed period.'
|
|
472
|
-
),
|
|
451
|
+
message=_('Receipt cannot be deleted because it falls within a closed period.'),
|
|
473
452
|
)
|
|
474
453
|
ledger = self.ledger_model
|
|
475
454
|
with transaction.atomic():
|
|
@@ -556,9 +535,7 @@ class ReceiptModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn, IOMixIn):
|
|
|
556
535
|
if self.vendor_model_id:
|
|
557
536
|
return self.EXPENSE_REFUND if float(amount) > 0 else self.EXPENSE_RECEIPT
|
|
558
537
|
|
|
559
|
-
raise ReceiptModelValidationError(
|
|
560
|
-
message='Cannot determine receipt type without a customer or vendor.'
|
|
561
|
-
)
|
|
538
|
+
raise ReceiptModelValidationError(message='Cannot determine receipt type without a customer or vendor.')
|
|
562
539
|
|
|
563
540
|
def is_configured(self) -> bool:
|
|
564
541
|
"""Whether the receipt has enough data to operate.
|
|
@@ -612,9 +589,7 @@ class ReceiptModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn, IOMixIn):
|
|
|
612
589
|
|
|
613
590
|
try:
|
|
614
591
|
state_model_qs = (
|
|
615
|
-
EntityStateModel.objects.filter(**LOOKUP)
|
|
616
|
-
.select_related('entity_model')
|
|
617
|
-
.select_for_update()
|
|
592
|
+
EntityStateModel.objects.filter(**LOOKUP).select_related('entity_model').select_for_update()
|
|
618
593
|
)
|
|
619
594
|
|
|
620
595
|
state_model = state_model_qs.get()
|
|
@@ -656,9 +631,7 @@ class ReceiptModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn, IOMixIn):
|
|
|
656
631
|
state_model = self._get_next_state_model(raise_exception=False)
|
|
657
632
|
|
|
658
633
|
seq = str(state_model.sequence).zfill(DJANGO_LEDGER_DOCUMENT_NUMBER_PADDING)
|
|
659
|
-
self.receipt_number =
|
|
660
|
-
f'{DJANGO_LEDGER_RECEIPT_NUMBER_PREFIX}-{state_model.fiscal_year}-{seq}'
|
|
661
|
-
)
|
|
634
|
+
self.receipt_number = f'{DJANGO_LEDGER_RECEIPT_NUMBER_PREFIX}-{state_model.fiscal_year}-{seq}'
|
|
662
635
|
|
|
663
636
|
if commit:
|
|
664
637
|
self.save(update_fields=['receipt_number', 'updated'])
|
|
@@ -668,9 +641,7 @@ class ReceiptModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn, IOMixIn):
|
|
|
668
641
|
def configure(
|
|
669
642
|
self,
|
|
670
643
|
entity_model: EntityModel | str | UUID,
|
|
671
|
-
receipt_type: Literal[
|
|
672
|
-
SALES_RECEIPT, SALES_REFUND, EXPENSE_RECEIPT, EXPENSE_REFUND
|
|
673
|
-
],
|
|
644
|
+
receipt_type: Literal[SALES_RECEIPT, SALES_REFUND, EXPENSE_RECEIPT, EXPENSE_REFUND, TRANSFER_RECEIPT],
|
|
674
645
|
amount: int | float | Decimal,
|
|
675
646
|
unit_model: Optional[EntityUnitModel | str | UUID] = None,
|
|
676
647
|
receipt_date: Optional[datetime | str] = None,
|
|
@@ -719,9 +690,7 @@ class ReceiptModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn, IOMixIn):
|
|
|
719
690
|
if not self.is_configured():
|
|
720
691
|
with transaction.atomic():
|
|
721
692
|
if amount < 0:
|
|
722
|
-
raise ReceiptModelValidationError(
|
|
723
|
-
message='Receipt amount must be greater than zero'
|
|
724
|
-
)
|
|
693
|
+
raise ReceiptModelValidationError(message='Receipt amount must be greater than zero')
|
|
725
694
|
if isinstance(entity_model, EntityModel):
|
|
726
695
|
pass
|
|
727
696
|
elif isinstance(entity_model, UUID):
|
|
@@ -729,48 +698,58 @@ class ReceiptModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn, IOMixIn):
|
|
|
729
698
|
elif isinstance(entity_model, str):
|
|
730
699
|
entity_model = EntityModel.objects.get(slug__exact=entity_model)
|
|
731
700
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
701
|
+
self.receipt_type = receipt_type
|
|
702
|
+
self.amount = amount
|
|
703
|
+
self.receipt_date = localdate() if not receipt_date else receipt_date
|
|
704
|
+
self.charge_account = charge_account
|
|
705
|
+
self.receipt_account = receipt_account
|
|
706
|
+
self.unit_model = unit_model
|
|
707
|
+
self.staged_transaction_model = staged_transaction_model
|
|
736
708
|
|
|
737
|
-
if
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
709
|
+
if self.is_transfer_receipt():
|
|
710
|
+
if any([vendor_model, customer_model]):
|
|
711
|
+
raise ReceiptModelValidationError(
|
|
712
|
+
message='Transfer receipts do not require a vendor model or customer model',
|
|
713
|
+
)
|
|
714
|
+
else:
|
|
715
|
+
if all([vendor_model, customer_model]):
|
|
716
|
+
raise ReceiptModelValidationError(
|
|
717
|
+
message='Must pass VendorModel or CustomerModel, not both.',
|
|
718
|
+
)
|
|
741
719
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
self.vendor_model_id is not None,
|
|
747
|
-
]
|
|
748
|
-
):
|
|
749
|
-
raise ReceiptModelValidationError(
|
|
750
|
-
message='Vendor Model already set.'
|
|
751
|
-
)
|
|
720
|
+
if not any([vendor_model, customer_model]):
|
|
721
|
+
raise ReceiptModelValidationError(
|
|
722
|
+
message='Must pass VendorModel or CustomerModel.',
|
|
723
|
+
)
|
|
752
724
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
725
|
+
# checks if a vendor model has been previously assigned....
|
|
726
|
+
if all(
|
|
727
|
+
[
|
|
728
|
+
vendor_model is not None,
|
|
729
|
+
self.vendor_model_id is not None,
|
|
730
|
+
]
|
|
731
|
+
):
|
|
732
|
+
raise ReceiptModelValidationError(message='Vendor Model already set.')
|
|
733
|
+
|
|
734
|
+
# checks if a customer model has been previously assigned....
|
|
735
|
+
if all(
|
|
736
|
+
[
|
|
737
|
+
customer_model is not None,
|
|
738
|
+
self.customer_model_id is not None,
|
|
739
|
+
]
|
|
740
|
+
):
|
|
741
|
+
raise ReceiptModelValidationError(message='Customer Model already set.')
|
|
763
742
|
|
|
764
743
|
# get vendor model...
|
|
765
744
|
if vendor_model:
|
|
766
745
|
if isinstance(vendor_model, str):
|
|
767
|
-
vendor_model = VendorModel.objects.for_entity(
|
|
768
|
-
|
|
769
|
-
)
|
|
746
|
+
vendor_model = VendorModel.objects.for_entity(entity_model=entity_model).get(
|
|
747
|
+
vendor_number__iexact=vendor_model
|
|
748
|
+
)
|
|
770
749
|
elif isinstance(customer_model, UUID):
|
|
771
|
-
vendor_model = VendorModel.objects.for_entity(
|
|
772
|
-
|
|
773
|
-
)
|
|
750
|
+
vendor_model = VendorModel.objects.for_entity(entity_model=entity_model).get(
|
|
751
|
+
uuid__exact=vendor_model
|
|
752
|
+
)
|
|
774
753
|
elif isinstance(vendor_model, VendorModel):
|
|
775
754
|
vendor_model.validate_for_entity(entity_model=entity_model)
|
|
776
755
|
else:
|
|
@@ -782,13 +761,13 @@ class ReceiptModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn, IOMixIn):
|
|
|
782
761
|
# get customer model
|
|
783
762
|
if customer_model:
|
|
784
763
|
if isinstance(customer_model, str):
|
|
785
|
-
customer_model = CustomerModel.objects.for_entity(
|
|
786
|
-
|
|
787
|
-
)
|
|
764
|
+
customer_model = CustomerModel.objects.for_entity(entity_model=customer_model).get(
|
|
765
|
+
customer_number__iexact=customer_model
|
|
766
|
+
)
|
|
788
767
|
elif isinstance(customer_model, UUID):
|
|
789
|
-
customer_model = CustomerModel.objects.for_entity(
|
|
790
|
-
|
|
791
|
-
)
|
|
768
|
+
customer_model = CustomerModel.objects.for_entity(entity_model=customer_model).get(
|
|
769
|
+
uuid__exact=customer_model
|
|
770
|
+
)
|
|
792
771
|
elif isinstance(customer_model, CustomerModel):
|
|
793
772
|
customer_model.validate_for_entity(entity_model=entity_model)
|
|
794
773
|
else:
|
|
@@ -799,33 +778,27 @@ class ReceiptModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn, IOMixIn):
|
|
|
799
778
|
|
|
800
779
|
if unit_model:
|
|
801
780
|
if isinstance(unit_model, str):
|
|
802
|
-
unit_model = EntityUnitModel.objects.for_entity(
|
|
803
|
-
|
|
804
|
-
)
|
|
781
|
+
unit_model = EntityUnitModel.objects.for_entity(entity_model=entity_model).get(
|
|
782
|
+
slug__exact=unit_model
|
|
783
|
+
)
|
|
805
784
|
elif isinstance(unit_model, UUID):
|
|
806
|
-
unit_model = EntityUnitModel.objects.for_entity(
|
|
807
|
-
|
|
808
|
-
)
|
|
785
|
+
unit_model = EntityUnitModel.objects.for_entity(entity_model=entity_model).get(
|
|
786
|
+
uuid__exact=unit_model
|
|
787
|
+
)
|
|
809
788
|
elif isinstance(unit_model, EntityUnitModel):
|
|
810
789
|
unit_model.validate_for_entity(entity_model=entity_model)
|
|
811
790
|
|
|
812
|
-
self.receipt_type = receipt_type
|
|
813
|
-
self.amount = amount
|
|
814
|
-
self.receipt_date = localdate() if not receipt_date else receipt_date
|
|
815
|
-
self.charge_account = charge_account
|
|
816
|
-
self.receipt_account = receipt_account
|
|
817
|
-
self.unit_model = unit_model
|
|
818
|
-
self.staged_transaction_model = staged_transaction_model
|
|
819
|
-
|
|
820
791
|
self.ledger_model = entity_model.create_ledger(
|
|
821
792
|
name=entity_model.name,
|
|
822
793
|
posted=True,
|
|
823
794
|
commit=False,
|
|
824
795
|
)
|
|
825
|
-
|
|
796
|
+
|
|
797
|
+
receipt_number = self.generate_receipt_number(commit=False)
|
|
798
|
+
|
|
826
799
|
self.ledger_model.name = receipt_number
|
|
827
800
|
self.ledger_model.save()
|
|
828
|
-
self.
|
|
801
|
+
self.clean()
|
|
829
802
|
|
|
830
803
|
if commit:
|
|
831
804
|
self.save()
|
|
@@ -841,10 +814,12 @@ class ReceiptModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn, IOMixIn):
|
|
|
841
814
|
return all(
|
|
842
815
|
[
|
|
843
816
|
self.receipt_date is not None,
|
|
817
|
+
self.amount > 0.00,
|
|
818
|
+
self.charge_account_id is not None,
|
|
844
819
|
any(
|
|
845
820
|
[
|
|
846
|
-
self.vendor_model_id is not None,
|
|
847
|
-
self.customer_model_id is not None,
|
|
821
|
+
self.vendor_model_id is not None if not self.is_transfer_receipt() else True,
|
|
822
|
+
self.customer_model_id is not None if not self.is_transfer_receipt() else True,
|
|
848
823
|
]
|
|
849
824
|
),
|
|
850
825
|
]
|
|
@@ -910,9 +885,7 @@ class ReceiptModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn, IOMixIn):
|
|
|
910
885
|
str
|
|
911
886
|
URL string for listing receipts of the same entity.
|
|
912
887
|
"""
|
|
913
|
-
return reverse(
|
|
914
|
-
'django_ledger:receipt-list', kwargs={'entity_slug': self.entity_slug}
|
|
915
|
-
)
|
|
888
|
+
return reverse('django_ledger:receipt-list', kwargs={'entity_slug': self.entity_slug})
|
|
916
889
|
|
|
917
890
|
def get_delete_url(self) -> str:
|
|
918
891
|
"""URL for the receipt delete view.
|
|
@@ -1039,16 +1012,12 @@ class ReceiptModelAbstract(CreateUpdateMixIn, MarkdownNotesMixIn, IOMixIn):
|
|
|
1039
1012
|
"""
|
|
1040
1013
|
if self.is_sales_receipt():
|
|
1041
1014
|
if not self.customer_model_id:
|
|
1042
|
-
raise ReceiptModelValidationError(
|
|
1043
|
-
message=_('Sales receipt must have a customer model.')
|
|
1044
|
-
)
|
|
1015
|
+
raise ReceiptModelValidationError(message=_('Sales receipt must have a customer model.'))
|
|
1045
1016
|
self.vendor_model = None
|
|
1046
1017
|
|
|
1047
1018
|
if self.is_expense_receipt():
|
|
1048
1019
|
if not self.vendor_model_id:
|
|
1049
|
-
raise ReceiptModelValidationError(
|
|
1050
|
-
message=_('Expense receipt must have a vendor model.')
|
|
1051
|
-
)
|
|
1020
|
+
raise ReceiptModelValidationError(message=_('Expense receipt must have a vendor model.'))
|
|
1052
1021
|
self.customer_model = None
|
|
1053
1022
|
|
|
1054
1023
|
|
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
<h1 class="is-size-2">{% trans 'Pending Transactions' %}</h1>
|
|
11
11
|
{% data_import_job_txs_pending staged_txs_formset %}
|
|
12
12
|
</div>
|
|
13
|
+
<div class="column is-12 has-text-centered">
|
|
14
|
+
<form action="{{ import_job.get_data_import_reset_url }}" method="post">
|
|
15
|
+
{% csrf_token %}
|
|
16
|
+
<button class="button is-warning is-large">Reset Job</button>
|
|
17
|
+
</form>
|
|
18
|
+
</div>
|
|
13
19
|
<div class="column is-12">
|
|
14
20
|
<h2 class="is-size-2">{% trans 'Imported Transactions' %}</h2>
|
|
15
21
|
{% data_import_job_txs_imported import_job %}
|
|
@@ -16,44 +16,74 @@
|
|
|
16
16
|
</tr>
|
|
17
17
|
</thead>
|
|
18
18
|
<tbody>
|
|
19
|
-
{%
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
<td
|
|
23
|
-
|
|
24
|
-
{%
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<button class="button is-small" aria-haspopup="true" aria-controls="actions-{{ imported_tx.uuid }}">
|
|
35
|
-
<span>{% trans 'Actions' %}</span>
|
|
36
|
-
<span class="icon is-small">{% icon 'mdi:chevron-down' 14 %}</span>
|
|
37
|
-
</button>
|
|
38
|
-
</div>
|
|
39
|
-
<div class="dropdown-menu" id="actions-{{ imported_tx.uuid }}" role="menu">
|
|
40
|
-
<div class="dropdown-content">
|
|
41
|
-
<a href="{% url 'django_ledger:je-detail' entity_slug=import_job_model.entity_slug ledger_pk=imported_tx.transaction_model.journal_entry.ledger_id je_pk=imported_tx.transaction_model.journal_entry_id %}"
|
|
42
|
-
class="dropdown-item">{% trans 'View JE' %}</a>
|
|
43
|
-
{% if imported_tx.has_receipt %}
|
|
44
|
-
<a href="{% url 'django_ledger:receipt-detail' entity_slug=import_job_model.entity_slug receipt_pk=imported_tx.receiptmodel.uuid %}"
|
|
45
|
-
class="dropdown-item">{% trans 'View Receipt' %}</a>
|
|
46
|
-
{% endif %}
|
|
47
|
-
<hr class="dropdown-divider">
|
|
48
|
-
<form method="post" action="{% url 'django_ledger:data-import-staged-tx-undo' entity_slug=import_job_model.entity_slug job_pk=import_job_model.uuid staged_tx_pk=imported_tx.uuid %}">
|
|
49
|
-
{% csrf_token %}
|
|
50
|
-
<button type="submit" class="dropdown-item has-text-danger">{% trans 'Undo Import' %}</button>
|
|
51
|
-
</form>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
19
|
+
{% regroup imported_txs by group_uuid as bundled_groups %}
|
|
20
|
+
{% for group in bundled_groups %}
|
|
21
|
+
<tr class="has-background-light">
|
|
22
|
+
<td colspan="8" class="has-text-weight-bold">
|
|
23
|
+
{% trans 'Bundle' %} {{ forloop.counter }} —
|
|
24
|
+
{% trans 'Parent' %}:
|
|
25
|
+
{% with parent_tx=group.list.0.parent|default:group.list.0 %}
|
|
26
|
+
{{ parent_tx.date_posted }} · {{ parent_tx.name }} ·
|
|
27
|
+
{% currency_symbol %}{{ parent_tx.get_amount }}
|
|
28
|
+
{% if parent_tx.get_activity_display %} · {{ parent_tx.get_activity_display }}{% endif %}
|
|
29
|
+
{% endwith %}
|
|
30
|
+
<span class="tag is-info is-light" style="margin-left: .5rem;">
|
|
31
|
+
{% if group.list.0.is_bundled %}{% trans 'Bundled' %}{% else %}
|
|
32
|
+
{% trans 'Not Bundled' %}{% endif %}
|
|
33
|
+
</span>
|
|
55
34
|
</td>
|
|
56
35
|
</tr>
|
|
36
|
+
{% for imported_tx in group.list %}
|
|
37
|
+
<tr id="staged-tx-{{ imported_tx.uuid }}" class="{% if imported_tx.is_children %}is-dark{% endif %}">
|
|
38
|
+
<td>{{ imported_tx.date_posted }}</td>
|
|
39
|
+
<td>
|
|
40
|
+
{% if imported_tx.is_children %}
|
|
41
|
+
<span class="tag is-primary is-light">{% trans 'Child' %}</span>
|
|
42
|
+
{% else %}
|
|
43
|
+
<span class="tag is-dark is-light">{% trans 'Parent' %}</span>
|
|
44
|
+
{% endif %}
|
|
45
|
+
{{ imported_tx.name }}
|
|
46
|
+
</td>
|
|
47
|
+
<td class="{% if imported_tx.get_amount < 0.00 %}has-text-danger{% endif %} has-text-centered">
|
|
48
|
+
{% currency_symbol %}{{ imported_tx.get_amount }}</td>
|
|
49
|
+
<td>{% if imported_tx.activity %}
|
|
50
|
+
{{ imported_tx.get_activity_display }}
|
|
51
|
+
{% endif %}</td>
|
|
52
|
+
<td>{% if imported_tx.entity_unit %}{{ imported_tx.entity_unit }}{% endif %}</td>
|
|
53
|
+
<td>{{ imported_tx.account_model }}</td>
|
|
54
|
+
<td>{{ imported_tx.transaction_model }}</td>
|
|
55
|
+
<td class="has-text-centered">
|
|
56
|
+
<div class="dropdown is-hoverable is-right">
|
|
57
|
+
<div class="dropdown-trigger">
|
|
58
|
+
<button class="button is-small" aria-haspopup="true"
|
|
59
|
+
aria-controls="actions-{{ imported_tx.uuid }}">
|
|
60
|
+
<span>{% trans 'Actions' %}</span>
|
|
61
|
+
<span class="icon is-small">{% icon 'mdi:chevron-down' 14 %}</span>
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="dropdown-menu" id="actions-{{ imported_tx.uuid }}" role="menu">
|
|
65
|
+
<div class="dropdown-content">
|
|
66
|
+
<a href="{% url 'django_ledger:je-detail' entity_slug=import_job_model.entity_slug ledger_pk=imported_tx.transaction_model.journal_entry.ledger_id je_pk=imported_tx.transaction_model.journal_entry_id %}"
|
|
67
|
+
class="dropdown-item">{% trans 'View JE' %}</a>
|
|
68
|
+
{% if imported_tx.has_receipt %}
|
|
69
|
+
<a href="{% url 'django_ledger:receipt-detail' entity_slug=import_job_model.entity_slug receipt_pk=imported_tx.receiptmodel.uuid %}"
|
|
70
|
+
class="dropdown-item">{% trans 'View Receipt' %}</a>
|
|
71
|
+
{% endif %}
|
|
72
|
+
<hr class="dropdown-divider">
|
|
73
|
+
{% if imported_tx.can_undo_import %}
|
|
74
|
+
<form method="post"
|
|
75
|
+
action="{% url 'django_ledger:data-import-staged-tx-undo' entity_slug=import_job_model.entity_slug job_pk=import_job_model.uuid staged_tx_pk=imported_tx.uuid %}">
|
|
76
|
+
{% csrf_token %}
|
|
77
|
+
<button type="submit"
|
|
78
|
+
class="dropdown-item has-text-danger">{% trans 'Undo Import' %}</button>
|
|
79
|
+
</form>
|
|
80
|
+
{% endif %}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</td>
|
|
85
|
+
</tr>
|
|
86
|
+
{% endfor %}
|
|
57
87
|
{% endfor %}
|
|
58
88
|
</tbody>
|
|
59
89
|
</table>
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
<form method="post">
|
|
6
6
|
|
|
7
7
|
{{ staged_txs_formset.non_form_errors }}
|
|
8
|
-
{
|
|
9
|
-
|
|
10
|
-
{
|
|
8
|
+
{% if staged_txs_formset.errors %}
|
|
9
|
+
{{ staged_txs_formset.errors }}
|
|
10
|
+
{% endif %}
|
|
11
11
|
{{ staged_txs_formset.management_form }}
|
|
12
12
|
|
|
13
13
|
{% csrf_token %}
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
<tbody>
|
|
36
36
|
|
|
37
37
|
{% for txf in staged_txs_formset %}
|
|
38
|
-
<tr id="staged-tx-{{ txf.instance.uuid }}"
|
|
38
|
+
<tr id="staged-tx-{{ txf.instance.uuid }}"
|
|
39
|
+
class="{% if txf.instance.is_children %}has-background-primary{% elif txf.instance.has_children %}has-background-primary-light{% endif %}">
|
|
39
40
|
<td>{{ forloop.counter }}</td>
|
|
40
41
|
{% for hidden_field in txf.hidden_fields %}{{ hidden_field }}{% endfor %}
|
|
41
42
|
{% if txf.instance.is_children %}
|
|
@@ -61,7 +62,7 @@
|
|
|
61
62
|
<span class="has-text-weight-bold">
|
|
62
63
|
{{ txf.instance.get_prospect_je_activity_display }}
|
|
63
64
|
</span>
|
|
64
|
-
{% elif not txf.instance.is_children %}
|
|
65
|
+
{% elif not txf.instance.is_children and txf.instance.bundle_split %}
|
|
65
66
|
<span class="has-text-danger">{% icon 'ooui:block' 16 %}</span>
|
|
66
67
|
<span>Invalid or Pending Account Mapping.</span>
|
|
67
68
|
{% endif %}
|
|
@@ -75,11 +76,10 @@
|
|
|
75
76
|
<td>{{ txf.unit_model }}</td>
|
|
76
77
|
<td>{{ txf.receipt_type }}</td>
|
|
77
78
|
<td class="has-text-centered">
|
|
78
|
-
{
|
|
79
|
-
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
{% endif %}
|
|
79
|
+
{{ txf.customer_model }}
|
|
80
|
+
{{ txf.vendor_model }}
|
|
81
|
+
{# {{ txf.instance.can_migrate }}#}
|
|
82
|
+
{# {{ txf.instance.ready_to_import }}#}
|
|
83
83
|
</td>
|
|
84
84
|
<td class="has-text-centered">{{ txf.tx_import }}</td>
|
|
85
85
|
<td class="has-text-centered">{{ txf.bundle_split }}</td>
|
|
@@ -18,6 +18,9 @@ urlpatterns = [
|
|
|
18
18
|
path('<slug:entity_slug>/jobs/<uuid:job_pk>/txs/',
|
|
19
19
|
views.DataImportJobDetailView.as_view(),
|
|
20
20
|
name='data-import-job-txs'),
|
|
21
|
+
path('<slug:entity_slug>/jobs/<uuid:job_pk>/reset/',
|
|
22
|
+
views.ImportJobModelResetView.as_view(),
|
|
23
|
+
name='data-import-job-txs-undo'),
|
|
21
24
|
path('<slug:entity_slug>/jobs/<uuid:job_pk>/txs/<uuid:staged_tx_pk>/undo/',
|
|
22
25
|
views.StagedTransactionUndoView.as_view(),
|
|
23
26
|
name='data-import-staged-tx-undo'),
|