facturation: attacher les paiements aux lignes de facturation (#81190) #99

Merged
lguerin merged 4 commits from wip/81190-invoicing-invoice-line-payment into main 2023-09-21 15:55:34 +02:00
22 changed files with 874 additions and 414 deletions

View File

@ -29,7 +29,7 @@ from lingo.invoicing.models import (
DraftJournalLine,
Invoice,
InvoiceLine,
InvoicePayment,
InvoiceLinePayment,
JournalLine,
Payer,
Payment,
@ -356,9 +356,10 @@ class AbstractInvoiceFilterSet(AgendaFieldsFilterSetMixin, django_filters.Filter
self._init_agenda_fields(line_model, self.queryset)
def filter_payment_number(self, queryset, name, value):
return queryset.filter(
pk__in=InvoicePayment.objects.filter(payment__formatted_number__contains=value).values('invoice')
line_queryset = InvoiceLine.objects.filter(
pk__in=InvoiceLinePayment.objects.filter(payment__formatted_number__contains=value).values('line')
)
return queryset.filter(pk__in=line_queryset.values('invoice'))
def filter_user_external_id(self, queryset, name, value):
if not value:
@ -666,9 +667,10 @@ class RegieInvoiceFilterSet(AgendaFieldsFilterSetMixin, django_filters.FilterSet
self._init_agenda_fields(InvoiceLine, self.queryset)
def filter_payment_number(self, queryset, name, value):
return queryset.filter(
pk__in=InvoicePayment.objects.filter(payment__formatted_number__contains=value).values('invoice')
line_queryset = InvoiceLine.objects.filter(
pk__in=InvoiceLinePayment.objects.filter(payment__formatted_number__contains=value).values('line')
)
return queryset.filter(pk__in=line_queryset.values('invoice'))
def filter_user_external_id(self, queryset, name, value):
if not value:
@ -768,23 +770,24 @@ class RegiePaymentFilterSet(AgendaFieldsFilterSetMixin, django_filters.FilterSet
super().__init__(*args, **kwargs)
self.filters['payment_type'].field.choices = [(t.pk, t) for t in self.regie.paymenttype_set.all()]
invoice_queryset = Invoice.objects.filter(
pk__in=InvoicePayment.objects.filter(payment__in=self.queryset).values('invoice')
line_queryset = InvoiceLine.objects.filter(
pk__in=InvoiceLinePayment.objects.filter(payment__in=self.queryset).values('line')
)
invoice_queryset = Invoice.objects.filter(pk__in=line_queryset.values('invoice'))
self._init_agenda_fields(InvoiceLine, invoice_queryset)
def filter_invoice_number(self, queryset, name, value):
return queryset.filter(
pk__in=InvoicePayment.objects.filter(invoice__formatted_number__contains=value).values('payment')
pk__in=InvoiceLinePayment.objects.filter(line__invoice__formatted_number__contains=value).values(
'payment'
)
)
def filter_agenda(self, queryset, name, value):
if not value:
return queryset
lines = InvoiceLine.objects.filter(
Q(details__agenda=value) | Q(slug__startswith='%s@' % value)
).values('invoice')
return queryset.filter(pk__in=InvoicePayment.objects.filter(invoice__in=lines).values('payment'))
lines = InvoiceLine.objects.filter(Q(details__agenda=value) | Q(slug__startswith='%s@' % value))
return queryset.filter(pk__in=InvoiceLinePayment.objects.filter(line__in=lines).values('payment'))
def filter_event(self, queryset, name, value):
if not value:
@ -792,8 +795,8 @@ class RegiePaymentFilterSet(AgendaFieldsFilterSetMixin, django_filters.FilterSet
agenda_slug, event_slug = value.split('@')
lines = InvoiceLine.objects.filter(
Q(details__agenda=agenda_slug, details__primary_event=event_slug) | Q(slug=value)
).values('invoice')
return queryset.filter(pk__in=InvoicePayment.objects.filter(invoice__in=lines).values('payment'))
)
return queryset.filter(pk__in=InvoiceLinePayment.objects.filter(line__in=lines).values('payment'))
class NewPayerForm(forms.ModelForm):

View File

@ -1,26 +1,9 @@
import os
from django.db import migrations
with open(
os.path.join(
os.path.dirname(os.path.realpath(__file__)),
'..',
'sql',
'invoice_triggers_for_amount.sql',
)
) as sql_file:
sql_triggers = sql_file.read()
sql_forwards = sql_triggers
class Migration(migrations.Migration):
dependencies = [
('invoicing', '0025_payments'),
]
operations = [
migrations.RunSQL(sql=sql_forwards, reverse_sql=migrations.RunSQL.noop),
]
operations = []

View File

@ -1,26 +1,9 @@
import os
from django.db import migrations
with open(
os.path.join(
os.path.dirname(os.path.realpath(__file__)),
'..',
'sql',
'invoice_triggers_for_amount.sql',
)
) as sql_file:
sql_triggers = sql_file.read()
sql_forwards = sql_triggers
class Migration(migrations.Migration):
dependencies = [
('invoicing', '0042_payer_external_id_from_nameid'),
]
operations = [
migrations.RunSQL(sql=sql_forwards, reverse_sql=migrations.RunSQL.noop),
]
operations = []

View File

@ -1,26 +1,9 @@
import os
from django.db import migrations
with open(
os.path.join(
os.path.dirname(os.path.realpath(__file__)),
'..',
'sql',
'invoice_triggers_for_amount.sql',
)
) as sql_file:
sql_triggers = sql_file.read()
sql_forwards = sql_triggers
class Migration(migrations.Migration):
dependencies = [
('invoicing', '0049_journal_lines'),
]
operations = [
migrations.RunSQL(sql=sql_forwards, reverse_sql=migrations.RunSQL.noop),
]
operations = []

View File

@ -1,25 +1,5 @@
import os
from django.db import migrations, models
sql_drop_triggers = """
DROP TRIGGER IF EXISTS set_draftinvoice_line_amount_trg ON invoicing_draftinvoiceline;
DROP TRIGGER IF EXISTS set_invoice_line_amount_trg ON invoicing_invoiceline;
"""
with open(
os.path.join(
os.path.dirname(os.path.realpath(__file__)),
'..',
'sql',
'invoice_triggers_for_amount.sql',
)
) as sql_file:
sql_triggers = sql_file.read()
sql_forwards = sql_triggers
class Migration(migrations.Migration):
dependencies = [
@ -27,7 +7,6 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RunSQL(sql=sql_drop_triggers, reverse_sql=migrations.RunSQL.noop),
migrations.AlterField(
model_name='draftinvoiceline',
name='quantity',
@ -38,5 +17,4 @@ class Migration(migrations.Migration):
name='quantity',
field=models.IntegerField(),
),
migrations.RunSQL(sql=sql_forwards, reverse_sql=migrations.RunSQL.noop),
]

View File

@ -1,25 +1,5 @@
import os
from django.db import migrations, models
sql_drop_triggers = """
DROP TRIGGER IF EXISTS set_draftinvoice_line_amount_trg ON invoicing_draftinvoiceline;
DROP TRIGGER IF EXISTS set_invoice_line_amount_trg ON invoicing_invoiceline;
"""
with open(
os.path.join(
os.path.dirname(os.path.realpath(__file__)),
'..',
'sql',
'invoice_triggers_for_amount.sql',
)
) as sql_file:
sql_triggers = sql_file.read()
sql_forwards = sql_triggers
class Migration(migrations.Migration):
dependencies = [
@ -27,7 +7,6 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RunSQL(sql=sql_drop_triggers, reverse_sql=migrations.RunSQL.noop),
migrations.AddField(
model_name='draftjournalline',
name='quantity',
@ -62,5 +41,4 @@ class Migration(migrations.Migration):
name='quantity',
field=models.DecimalField(decimal_places=2, max_digits=9),
),
migrations.RunSQL(sql=sql_forwards, reverse_sql=migrations.RunSQL.noop),
]

View File

@ -0,0 +1,85 @@
import os
from decimal import Decimal
import django.core.validators
import django.db.models.deletion
from django.db import migrations, models
with open(
os.path.join(
os.path.dirname(os.path.realpath(__file__)),
'..',
'sql',
'invoice_triggers_for_amount.sql',
)
) as sql_file:
sql_triggers = sql_file.read()
sql_forwards = sql_triggers
class Migration(migrations.Migration):
dependencies = [
('invoicing', '0061_pdf_appearance'),
]
operations = [
migrations.CreateModel(
name='InvoiceLinePayment',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
(
'amount',
models.DecimalField(
decimal_places=2,
max_digits=9,
validators=[django.core.validators.MinValueValidator(Decimal('0.01'))],
),
),
('created_at', models.DateTimeField(auto_now_add=True)),
(
'line',
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to='invoicing.invoiceline'
),
),
(
'payment',
models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='invoicing.payment'),
),
],
),
migrations.AddField(
model_name='invoiceline',
name='paid_amount',
field=models.DecimalField(decimal_places=2, default=0, max_digits=9),
),
migrations.AddField(
model_name='invoiceline',
name='remaining_amount',
field=models.DecimalField(decimal_places=2, default=0, max_digits=9),
),
migrations.AddConstraint(
model_name='invoiceline',
constraint=models.CheckConstraint(
check=models.Q(
models.Q(
('paid_amount__lte', django.db.models.expressions.F('total_amount')),
('total_amount__gt', 0),
),
models.Q(
('paid_amount__gte', django.db.models.expressions.F('total_amount')),
('total_amount__lt', 0),
),
models.Q(('paid_amount', 0), ('total_amount', 0)),
_connector='OR',
),
name='paid_amount_check',
),
),
migrations.RunSQL(sql=sql_forwards, reverse_sql=migrations.RunSQL.noop),
]

View File

@ -0,0 +1,40 @@
import decimal
from django.db import migrations
def forward(apps, schema_editor):
InvoicePayment = apps.get_model('invoicing', 'InvoicePayment')
InvoiceLinePayment = apps.get_model('invoicing', 'InvoiceLinePayment')
InvoiceLine = apps.get_model('invoicing', 'InvoiceLine')
for invoice_payment in InvoicePayment.objects.all():
amount_to_assign = invoice_payment.amount
for line in InvoiceLine.objects.filter(invoice=invoice_payment.invoice).order_by('pk'):
# trigger not played yet, remaining_amount is not up to date
line.remaining_amount = line.total_amount - line.paid_amount
if not line.remaining_amount:
# nothing to pay for this line
continue
# paid_amount for this line: it can not be greater than line remaining_amount
paid_amount = decimal.Decimal(min(line.remaining_amount, amount_to_assign))
# create payment for the line
InvoiceLinePayment.objects.create(
payment=invoice_payment.payment,
line=line,
amount=paid_amount,
)
# new amount to assign
amount_to_assign -= paid_amount
if amount_to_assign <= 0:
break
class Migration(migrations.Migration):
dependencies = [
('invoicing', '0062_invoice_line_payment'),
]
operations = [
migrations.RunPython(forward, reverse_code=migrations.RunPython.noop),
]

View File

@ -0,0 +1,13 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('invoicing', '0063_invoice_line_payment'),
]
operations = [
migrations.DeleteModel(
name='InvoicePayment',
),
]

View File

@ -15,7 +15,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import base64
import collections
import copy
import dataclasses
import datetime
import decimal
import sys
@ -673,7 +675,7 @@ class AbstractInvoice(models.Model):
context = {
'regie': self.regie,
'invoice': self,
'payments': self.invoicepayment_set.all().order_by('created_at'),
'payments': InvoiceLinePayment.objects.filter(line__invoice=self).order_by('created_at'),
}
return template.render(context)
@ -764,6 +766,20 @@ class Invoice(AbstractInvoice):
'payment_deadline_date': self.date_payment_deadline,
}
def get_invoice_payments(self):
invoice_line_payments = (
InvoiceLinePayment.objects.filter(line__invoice=self)
.select_related('payment', 'payment__payment_type')
.order_by('created_at')
)
invoice_payments = collections.defaultdict(InvoicePayment)
for invoice_line_payment in invoice_line_payments:
payment = invoice_line_payment.payment
invoice_payments[payment].invoice = self
invoice_payments[payment].payment = payment
invoice_payments[payment].amount += invoice_line_payment.amount
return sorted(invoice_payments.values(), key=lambda a: a.payment.created_at)
class InjectedLine(models.Model):
event_date = models.DateField()
@ -979,6 +995,8 @@ class DraftInvoiceLine(AbstractInvoiceLine):
final_line.pool = pool
final_line.invoice = invoice
final_line.error_status = ''
final_line.paid_amount = 0
final_line.remaining_amount = 0
final_line.save()
for line in self.journal_lines.all().order_by('pk'):
@ -987,6 +1005,27 @@ class DraftInvoiceLine(AbstractInvoiceLine):
class InvoiceLine(AbstractInvoiceLine):
invoice = models.ForeignKey(Invoice, on_delete=models.PROTECT, null=True, related_name='lines')
paid_amount = models.DecimalField(max_digits=9, decimal_places=2, default=0)
remaining_amount = models.DecimalField(max_digits=9, decimal_places=2, default=0)
class Meta:
constraints = [
models.CheckConstraint(
check=models.Q(
models.Q(
('paid_amount__lte', models.F('total_amount')),
('total_amount__gt', 0),
),
models.Q(
('paid_amount__gte', models.F('total_amount')),
('total_amount__lt', 0),
),
models.Q(('paid_amount', 0), ('total_amount', 0)),
_connector='OR',
),
name='paid_amount_check',
)
]
DEFAULT_PAYMENT_TYPES = [
@ -1078,18 +1117,27 @@ class Payment(models.Model):
)
payment.set_number()
payment.save()
remaining_amount = amount
amount_to_assign = amount
for invoice in invoices.order_by('date_publication', 'created_at'):
if not invoice.remaining_amount:
# nothing to pay for this invoice
continue
paid_amount = decimal.Decimal(min(invoice.remaining_amount, remaining_amount))
InvoicePayment.objects.create(
payment=payment,
invoice=invoice,
amount=paid_amount,
)
remaining_amount -= paid_amount
if remaining_amount <= 0:
for line in invoice.lines.order_by('pk'):
if not line.remaining_amount:
# nothing to pay for this line
continue
# paid_amount for this line: it can not be greater than line remaining_amount
paid_amount = decimal.Decimal(min(line.remaining_amount, amount_to_assign))
InvoiceLinePayment.objects.create(
payment=payment,
line=line,
amount=paid_amount,
)
# new amount to assign
amount_to_assign -= paid_amount
if amount_to_assign <= 0:
break
if amount_to_assign <= 0:
break
return payment
@ -1102,25 +1150,47 @@ class Payment(models.Model):
)
self.formatted_number = self.regie.format_number(self.created_at, self.number, 'payment')
def get_invoice_payments(self):
if hasattr(self, 'prefetched_invoicelinepayments'):
invoice_line_payments = self.prefetched_invoicelinepayments
else:
invoice_line_payments = self.invoicelinepayment_set.select_related('line__invoice').order_by(
'created_at'
)
invoice_payments = collections.defaultdict(InvoicePayment)
for invoice_line_payment in invoice_line_payments:
invoice = invoice_line_payment.line.invoice
invoice_payments[invoice].invoice = invoice
invoice_payments[invoice].payment = self
invoice_payments[invoice].amount += invoice_line_payment.amount
return sorted(invoice_payments.values(), key=lambda a: a.invoice.created_at)
def html(self):
template = get_template('lingo/invoicing/payment.html')
context = {
'regie': self.regie,
'payment': self,
'lines': self.invoicepayment_set.select_related('invoice').order_by('created_at'),
'invoice_payments': self.get_invoice_payments(),
}
return template.render(context)
class InvoicePayment(models.Model):
class InvoiceLinePayment(models.Model):
payment = models.ForeignKey(Payment, on_delete=models.PROTECT)
invoice = models.ForeignKey(Invoice, on_delete=models.PROTECT)
line = models.ForeignKey(InvoiceLine, on_delete=models.PROTECT)
amount = models.DecimalField(
max_digits=9, decimal_places=2, validators=[validators.MinValueValidator(decimal.Decimal('0.01'))]
)
created_at = models.DateTimeField(auto_now_add=True)
@dataclasses.dataclass
class InvoicePayment:
payment: Payment = None
invoice: Invoice = None
amount: decimal.Decimal = 0
class AppearanceSettings(models.Model):
logo = models.ImageField(
verbose_name=_('Logo'),

View File

@ -1,4 +1,7 @@
CREATE OR REPLACE FUNCTION set_invoice_line_amount() RETURNS TRIGGER AS $$
-- update total_amount on quantity, unit_amount changes for draft invoice line & invoice line
CREATE OR REPLACE FUNCTION set_invoice_line_total_amount() RETURNS TRIGGER AS $$
BEGIN
NEW.total_amount = NEW.quantity * NEW.unit_amount;
RETURN NEW;
@ -6,6 +9,87 @@ CREATE OR REPLACE FUNCTION set_invoice_line_amount() RETURNS TRIGGER AS $$
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS set_draftinvoice_line_amount_trg ON invoicing_draftinvoiceline;
CREATE TRIGGER set_draftinvoice_line_amount_trg
BEFORE INSERT OR UPDATE OF quantity, unit_amount ON invoicing_draftinvoiceline
FOR EACH ROW
EXECUTE PROCEDURE set_invoice_line_total_amount();
DROP TRIGGER IF EXISTS set_invoice_line_amount_trg ON invoicing_invoiceline;
CREATE TRIGGER set_invoice_line_amount_trg
BEFORE INSERT OR UPDATE OF quantity, unit_amount ON invoicing_invoiceline
FOR EACH ROW
EXECUTE PROCEDURE set_invoice_line_total_amount();
-- update line paid_amount & remaining_amount on invoice line payment change
CREATE OR REPLACE FUNCTION set_invoice_line_computed_amounts() RETURNS TRIGGER AS $$
DECLARE
line_ids integer[];
error_ids integer[];
BEGIN
IF TG_OP = 'INSERT' THEN
line_ids := ARRAY[NEW.line_id];
ELSIF TG_OP = 'DELETE' THEN
line_ids := ARRAY[OLD.line_id];
ELSIF TG_OP = 'UPDATE' THEN
line_ids := ARRAY[NEW.line_id, OLD.line_id];
END IF;
EXECUTE 'UPDATE invoicing_invoiceline l
SET paid_amount = COALESCE(
(
SELECT SUM(p.amount)
FROM invoicing_invoicelinepayment p
WHERE p.line_id = l.id
), 0
)
WHERE id = ANY($1);' USING line_ids;
EXECUTE 'UPDATE invoicing_invoiceline
SET remaining_amount = total_amount - paid_amount
WHERE id = ANY($1);' USING line_ids;
IF TG_OP = 'DELETE' THEN
RETURN OLD;
ELSE
RETURN NEW;
END IF;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS set_invoice_invoicelinepayment_trg ON invoicing_invoicelinepayment;
CREATE TRIGGER set_invoice_invoicelinepayment_trg
AFTER INSERT OR UPDATE OF amount OR DELETE ON invoicing_invoicelinepayment
FOR EACH ROW
EXECUTE PROCEDURE set_invoice_line_computed_amounts();
-- update line remaining_amount on total_amout update
CREATE OR REPLACE FUNCTION set_invoice_line_remaining_amount() RETURNS TRIGGER AS $$
BEGIN
NEW.remaining_amount = NEW.total_amount - NEW.paid_amount;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS set_invoice_line_remaining_amount_trg ON invoicing_invoiceline;
CREATE TRIGGER set_invoice_line_remaining_amount_trg
BEFORE INSERT OR UPDATE OF total_amount, paid_amount ON invoicing_invoiceline
FOR EACH ROW
EXECUTE PROCEDURE set_invoice_line_remaining_amount();
-- update invoice total_amount, paid_amount & remaining_amount on line amount changes
CREATE OR REPLACE FUNCTION set_invoice_amounts() RETURNS TRIGGER AS $$
DECLARE
invoice_ids integer[];
@ -19,37 +103,26 @@ CREATE OR REPLACE FUNCTION set_invoice_amounts() RETURNS TRIGGER AS $$
invoice_ids := ARRAY[NEW.invoice_id, OLD.invoice_id];
END IF;
IF TG_TABLE_NAME != 'invoicing_invoicepayment' THEN
EXECUTE 'UPDATE ' || substring(TG_TABLE_NAME for length(TG_TABLE_NAME) - 4) || ' i
SET total_amount = COALESCE(
EXECUTE 'UPDATE ' || substring(TG_TABLE_NAME for length(TG_TABLE_NAME) - 4) || ' i
SET total_amount = COALESCE(
(
SELECT SUM(l.total_amount)
FROM ' || TG_TABLE_NAME || ' l
WHERE l.invoice_id = i.id
), 0
)
WHERE id = ANY($1);' USING invoice_ids;
IF TG_TABLE_NAME = 'invoicing_invoiceline' THEN
EXECUTE 'UPDATE invoicing_invoice i
SET paid_amount = COALESCE(
(
SELECT SUM(l.total_amount)
FROM ' || TG_TABLE_NAME || ' l
SELECT SUM(l.paid_amount)
FROM invoicing_invoiceline l
WHERE l.invoice_id = i.id
), 0
)
WHERE id = ANY($1);' USING invoice_ids;
END IF;
IF TG_TABLE_NAME != 'invoicing_draftinvoiceline' THEN
EXECUTE 'UPDATE invoicing_invoice i
SET paid_amount = COALESCE(
(
SELECT SUM(p.amount)
FROM invoicing_invoicepayment p
WHERE p.invoice_id = i.id
), 0
)
WHERE id = ANY($1);' USING invoice_ids;
EXECUTE 'SELECT ARRAY_AGG(id) FROM invoicing_invoice
WHERE id = ANY($1) AND paid_amount > total_amount AND NOT (paid_amount = 0 and total_amount < 0)'
INTO error_ids
USING invoice_ids;
IF ARRAY_LENGTH(error_ids, 1) > 0 THEN
RAISE EXCEPTION 'paid_amount is greater than total_amount for invoices %', error_ids;
END IF;
EXECUTE 'UPDATE invoicing_invoice
SET remaining_amount = total_amount - paid_amount
WHERE id = ANY($1);' USING invoice_ids;
@ -64,20 +137,6 @@ CREATE OR REPLACE FUNCTION set_invoice_amounts() RETURNS TRIGGER AS $$
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS set_draftinvoice_line_amount_trg ON invoicing_draftinvoiceline;
CREATE TRIGGER set_draftinvoice_line_amount_trg
BEFORE INSERT OR UPDATE OF quantity, unit_amount ON invoicing_draftinvoiceline
FOR EACH ROW
EXECUTE PROCEDURE set_invoice_line_amount();
DROP TRIGGER IF EXISTS set_invoice_line_amount_trg ON invoicing_invoiceline;
CREATE TRIGGER set_invoice_line_amount_trg
BEFORE INSERT OR UPDATE OF quantity, unit_amount ON invoicing_invoiceline
FOR EACH ROW
EXECUTE PROCEDURE set_invoice_line_amount();
DROP TRIGGER IF EXISTS set_draftinvoice_line_trg ON invoicing_draftinvoiceline;
CREATE TRIGGER set_draftinvoice_line_trg
AFTER INSERT OR UPDATE OF total_amount, invoice_id OR DELETE ON invoicing_draftinvoiceline
@ -87,18 +146,11 @@ CREATE TRIGGER set_draftinvoice_line_trg
DROP TRIGGER IF EXISTS set_invoice_line_trg ON invoicing_invoiceline;
CREATE TRIGGER set_invoice_line_trg
AFTER INSERT OR UPDATE OF total_amount, invoice_id OR DELETE ON invoicing_invoiceline
AFTER INSERT OR UPDATE OF total_amount, paid_amount, remaining_amount, invoice_id OR DELETE ON invoicing_invoiceline
FOR EACH ROW
EXECUTE PROCEDURE set_invoice_amounts();
DROP TRIGGER IF EXISTS set_invoice_invoicepayment_trg ON invoicing_invoicepayment;
CREATE TRIGGER set_invoice_invoicepayment_trg
AFTER INSERT OR UPDATE OF amount OR DELETE ON invoicing_invoicepayment
FOR EACH ROW
EXECUTE PROCEDURE set_invoice_amounts();
-- old triggers and function
DROP TRIGGER IF EXISTS set_draftinvoice_total_amount_trg ON invoicing_draftinvoiceline;
DROP TRIGGER IF EXISTS set_invoice_total_amount_trg ON invoicing_invoiceline;
DROP FUNCTION IF EXISTS set_invoice_total_amount;
DROP TRIGGER IF EXISTS set_invoice_invoicepayment_trg ON invoicing_invoicepayment;
DROP FUNCTION IF EXISTS set_invoice_line_amount;

View File

@ -57,7 +57,8 @@
<td colspan="2">{% trans "Description" %}</td>
<td class="amount">{% trans "Amount" %}</td>
<td class="quantity">{% trans "Quantity" %}</td>
<td class="amount" colspan="2">{% trans "Subtotal" %}</td>
<td class="amount">{% trans "Subtotal" %}</td>
<td></td>
</tr>
{% endifchanged %}
{% ifchanged line.user_external_id line.agenda %}
@ -67,7 +68,7 @@
</tr>
{% endif %}
{% endifchanged %}
<tr class="line {% if pool.draft and forloop.last %}last-line{% endif %}" data-related-invoicing-element-id="{{ invoice.pk }}">
<tr class="line untoggled {% if pool.draft and forloop.last %}last-line{% endif %}" data-related-invoicing-element-id="{{ invoice.pk }}" data-invoicing-element-id="{{ invoice.pk }}-{{ line.pk }}">
<td class="event">
{% if line.pool %}<a href="{{ journal_url }}?invoice_line={{ line.pk }}">{% endif %}{% if not line.details %}{{ line.event_date|date:"d/m/Y" }} - {% endif %}{{ line.label }}{% if line.pool %}</a>{% endif %}
</td>
@ -91,10 +92,50 @@
<td class="quantity">
{{ line.quantity|floatformat }}
</td>
<td colspan="2" class="amount">
<td class="amount">
{% blocktrans with amount=line.total_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}
</td>
{% if not pool.draft and line.total_amount %}
<td class="with-togglable">
<span class="togglable"></span>
</td>
{% endif %}
</tr>
{% if not pool.draft and line.total_amount %}
<tr class="line" data-related-invoicing-element-id="{{ invoice.pk }}-{{ line.pk }}" style="display: none;">
<th colspan="6" class="payments">
{% trans "Payments" %}
</th>
</tr>
<tr class="line" data-related-invoicing-element-id="{{ invoice.pk }}-{{ line.pk }}" style="display: none;">
<td class="payment-num">{% trans "Payment" context 'payment' %}</td>
<td>{% trans "Date" context 'payment' %}</td>
<td colspan="2">{% trans "Type" context 'payment' %}</td>
<td class="amount" colspan="2">{% trans "Amount" %}</td>
</tr>
{% for invoice_line_payment in line.invoicelinepayment_set.all %}
<tr class="line payment {% if forloop.last %}last-line{% endif %}" data-related-invoicing-element-id="{{ invoice.pk }}-{{ line.pk }}" style="display: none;">
<td>
<a href="{% url 'lingo-manager-invoicing-regie-payment-list' regie_pk=regie.pk %}?number={{ invoice_line_payment.payment.formatted_number }}">{{ invoice_line_payment.payment.formatted_number }}</a>
</td>
<td>
{{ invoice_line_payment.payment.created_at|date:"DATETIME_FORMAT" }}
</td>
<td colspan="2">
{{ invoice_line_payment.payment.payment_type.label }}
</td>
<td class="amount" colspan="2">
{% blocktrans with amount=invoice_line_payment.amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}
</td>
</tr>
{% empty %}
<tr class="line payment" data-related-invoicing-element-id="{{ invoice.pk }}-{{ line.pk }}" style="display: none;">
<td colspan="6" class="no-payments">
{% trans "No payments for this line" %}
</td>
</tr>
{% endfor %}
{% endif %}
{% endfor %}
{% if not pool.draft %}
<tr class="line" data-related-invoicing-element-id="{{ invoice.pk }}">
@ -108,13 +149,13 @@
<td colspan="2">{% trans "Type" context 'payment' %}</td>
<td class="amount" colspan="2">{% trans "Amount" %}</td>
</tr>
{% for invoice_payment in invoice_payments %}
{% for invoice_payment in invoice.get_invoice_payments %}
<tr class="line" data-related-invoicing-element-id="{{ invoice.pk }}">
<td>
<a href="{% url 'lingo-manager-invoicing-regie-payment-list' regie_pk=regie.pk %}?number={{ invoice_payment.payment.formatted_number }}">{{ invoice_payment.payment.formatted_number }}</a>
</td>
<td>
{{ invoice_payment.created_at|date:"DATETIME_FORMAT" }}
{{ invoice_payment.payment.created_at|date:"DATETIME_FORMAT" }}
</td>
<td colspan="2">
{{ invoice_payment.payment.payment_type.label }}

View File

@ -32,29 +32,31 @@
<table class="main pk-compact-table invoicing-element-list">
{% url 'lingo-manager-invoicing-regie-invoice-list' regie_pk=regie.pk as regie_invoice_list_url %}
{% for payment in object_list %}
<tr class="payment untoggled" data-invoicing-element-id="{{ payment.pk }}">
<td colspan="3">
{% blocktrans with payment_number=payment.formatted_number cdate=payment.created_at|date:'d/m/Y' payer_id=payment.invoicepayment_set.all.0.invoice.payer_external_id payer_name=payment.invoicepayment_set.all.0.invoice.payer_name amount=payment.amount payment_type=payment.payment_type %}Payment {{ payment_number }} dated {{ cdate }} from <a href="?payer_external_id={{ payer_id }}">{{ payer_name }}</a>, amount {{ amount }}€ ({{ payment_type }}){% endblocktrans %}
- <a href="{% url 'lingo-manager-invoicing-regie-payment-pdf' regie_pk=regie.pk payment_pk=payment.pk %}">{% trans "download" %}</a>
</td>
<td class="with-togglable">
<span class="togglable"></span>
</td>
</tr>
<tr class="line" data-related-invoicing-element-id="{{ payment.pk }}" style="display: none;">
<td>{% trans "Invoice" %}</td>
<td class="amount">{% trans "Amount charged" %}</td>
<td class="amount" colspan="2">{% trans "Amount paid" %}</td>
</tr>
{% for line in payment.invoicepayment_set.all %}
<tr class="line {% if forloop.last %}last-line{% endif %}" data-related-invoicing-element-id="{{ payment.pk }}" style="display: none;">
<td class="invoice">
<a href="{{ regie_invoice_list_url }}?number={{ line.invoice.formatted_number }}">{{ line.invoice.formatted_number }}</a>
{% with invoice_payments=payment.get_invoice_payments %}
<tr class="payment untoggled" data-invoicing-element-id="{{ payment.pk }}">
<td colspan="3">
{% blocktrans with payment_number=payment.formatted_number cdate=payment.created_at|date:'d/m/Y' payer_id=invoice_payments.0.invoice.payer_external_id payer_name=invoice_payments.0.invoice.payer_name amount=payment.amount payment_type=payment.payment_type %}Payment {{ payment_number }} dated {{ cdate }} from <a href="?payer_external_id={{ payer_id }}">{{ payer_name }}</a>, amount {{ amount }}€ ({{ payment_type }}){% endblocktrans %}
- <a href="{% url 'lingo-manager-invoicing-regie-payment-pdf' regie_pk=regie.pk payment_pk=payment.pk %}">{% trans "download" %}</a>
</td>
<td class="with-togglable">
<span class="togglable"></span>
</td>
<td class="amount">{% blocktrans with amount=line.invoice.total_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
<td class="amount" colspan="2">{% blocktrans with amount=line.amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
</tr>
{% endfor %}
<tr class="line" data-related-invoicing-element-id="{{ payment.pk }}" style="display: none;">
<td>{% trans "Invoice" %}</td>
<td class="amount">{% trans "Amount charged" %}</td>
<td class="amount" colspan="2">{% trans "Amount assigned" %}</td>
</tr>
{% for invoice_payment in invoice_payments %}
<tr class="line {% if forloop.last %}last-line{% endif %}" data-related-invoicing-element-id="{{ payment.pk }}" style="display: none;">
<td class="invoice">
<a href="{{ regie_invoice_list_url }}?number={{ invoice_payment.invoice.formatted_number }}">{{ invoice_payment.invoice.formatted_number }}</a>
</td>
<td class="amount">{% blocktrans with amount=invoice_payment.invoice.total_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
<td class="amount" colspan="2">{% blocktrans with amount=invoice_payment.amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
</tr>
{% endfor %}
{% endwith %}
{% endfor %}
</table>
{% include "gadjo/pagination.html" %}

View File

@ -126,16 +126,16 @@
<th class="invoice">{% trans "Invoice number" %}</th>
<th class="object">{% trans "Invoice object" %}</th>
<th class="amount">{% trans "Amount charged" %}</th>
<th class="amount">{% trans "Amount payed" %}</th>
<th class="amount">{% trans "Amount assigned" %}</th>
</tr>
</thead>
<tbody>
{% for line in lines %}
{% for invoice_payment in invoice_payments %}
<tr>
<td class="invoice">{{ line.invoice.formatted_number }}</td>
<td class="object">{{ line.invoice.label }}</td>
<td class="amount">{% blocktrans with amount=line.invoice.total_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
<td class="amount">{% blocktrans with amount=line.amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
<td class="invoice">{{ invoice_payment.invoice.formatted_number }}</td>
<td class="object">{{ invoice_payment.invoice.label }}</td>
<td class="amount">{% blocktrans with amount=invoice_payment.invoice.total_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
<td class="amount">{% blocktrans with amount=invoice_payment.amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
</tr>
{% endfor %}
</tbody>

View File

@ -130,12 +130,12 @@
</tr>
</thead>
<tbody>
{% for invoice_payment in payments %}
{% for invoice_line_payment in payments %}
<tr>
<td class="invoice">{{ invoice_payment.payment.formatted_number }}</td>
<td class="date">{{ invoice_payment.created_at|date:"d/m/Y" }}</td>
<td class="type">{{ invoice_payment.payment.payment_type }}</td>
<td class="amount">{% blocktrans with amount=invoice_payment.amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
<td class="invoice">{{ invoice_line_payment.payment.formatted_number }}</td>
<td class="date">{{ invoice_line_payment.created_at|date:"d/m/Y" }}</td>
<td class="type">{{ invoice_line_payment.payment.payment_type }}</td>
<td class="amount">{% blocktrans with amount=invoice_line_payment.amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
</tr>
{% endfor %}
</tbody>

View File

@ -32,7 +32,6 @@ from lingo.invoicing.models import (
DraftJournalLine,
Invoice,
InvoiceLine,
InvoicePayment,
JournalLine,
Pool,
Regie,
@ -289,12 +288,6 @@ class InvoiceLineListView(ListView):
kwargs['object'] = self.pool.campaign
kwargs['pool'] = self.pool
kwargs['invoice'] = self.invoice
if not self.pool.draft:
kwargs['invoice_payments'] = (
InvoicePayment.objects.filter(invoice=self.invoice)
.select_related('payment')
.order_by('created_at')
)
return super().get_context_data(**kwargs)

View File

@ -34,7 +34,7 @@ from lingo.invoicing.models import (
Counter,
InjectedLine,
Invoice,
InvoicePayment,
InvoiceLinePayment,
JournalLine,
Payment,
PaymentType,
@ -462,11 +462,6 @@ class RegieInvoiceLineListView(ListView):
def get_context_data(self, **kwargs):
kwargs['regie'] = self.regie
kwargs['invoice'] = self.invoice
kwargs['invoice_payments'] = (
InvoicePayment.objects.filter(invoice=self.invoice)
.select_related('payment', 'payment__payment_type')
.order_by('created_at')
)
return super().get_context_data(**kwargs)
@ -482,12 +477,19 @@ class RegiePaymentListView(ListView):
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
invoice_payment_queryset = InvoicePayment.objects.select_related('invoice').order_by('created_at')
invoice_line_payment_queryset = InvoiceLinePayment.objects.select_related('line__invoice').order_by(
'created_at'
)
self.filterset = RegiePaymentFilterSet(
data=self.request.GET or None,
queryset=Payment.objects.filter(regie=self.regie)
.prefetch_related(
'payment_type', Prefetch('invoicepayment_set', queryset=invoice_payment_queryset)
'payment_type',
Prefetch(
'invoicelinepayment_set',
queryset=invoice_line_payment_queryset,
to_attr='prefetched_invoicelinepayments',
),
)
.order_by('-created_at'),
regie=self.regie,
@ -520,12 +522,12 @@ class RegiePaymentListView(ListView):
_('Payer first name'),
_('Payer last name'),
_('Payment type'),
_('Amount paid'),
_('Amount assigned'),
_('Total amount'),
]
)
for payment in self.object_list:
for invoice_payment in payment.invoicepayment_set.all():
for invoice_payment in payment.get_invoice_payments():
writer.writerow(
[
payment.formatted_number,

View File

@ -137,9 +137,12 @@ table.invoicing-element-list {
padding-bottom: 2em;
}
td {
&.quantity, &.amount, &.payment-num, &.with-togglable {
&.quantity, &.amount, &.payment-num {
width: 10em;
}
&.with-togglable {
width: 2em;
}
&.quantity, &.amount, &.with-togglable {
text-align: right;
}

View File

@ -14,7 +14,7 @@ from lingo.invoicing.models import (
InjectedLine,
Invoice,
InvoiceLine,
InvoicePayment,
InvoiceLinePayment,
PayerError,
Payment,
PaymentType,
@ -99,7 +99,7 @@ def test_list_invoices(mock_payer, app, user):
assert mock_payer.call_args_list == [mock.call(regie, mock.ANY, 'foobar')]
# invoice with something to pay
InvoiceLine.objects.create(
line = InvoiceLine.objects.create(
event_date=datetime.date.today(),
invoice=invoice,
quantity=1,
@ -155,9 +155,9 @@ def test_list_invoices(mock_payer, app, user):
amount=1,
payment_type=PaymentType.objects.get(regie=regie, slug='cash'),
)
invoice_payment = InvoicePayment.objects.create(
invoice_line_payment = InvoiceLinePayment.objects.create(
payment=payment,
invoice=invoice,
line=line,
amount=1,
)
resp = app.get('/api/regie/foo/invoices/', params={'NameID': 'foobar'})
@ -186,15 +186,15 @@ def test_list_invoices(mock_payer, app, user):
]
# invoice is paid
invoice_payment.amount = 42
invoice_payment.save()
invoice_line_payment.amount = 42
invoice_line_payment.save()
resp = app.get('/api/regie/foo/invoices/', params={'NameID': 'foobar'})
assert resp.json['err'] == 0
assert resp.json['data'] == []
# no matching payer id
invoice_payment.amount = 1
invoice_payment.save()
invoice_line_payment.amount = 1
invoice_line_payment.save()
mock_payer.return_value = 'payer:unknown'
resp = app.get('/api/regie/foo/invoices/', params={'NameID': 'foobar'})
assert resp.json['err'] == 0
@ -268,7 +268,7 @@ def test_list_invoices_for_payer(app, user):
assert resp.json['data'] == []
# invoice with something to pay
InvoiceLine.objects.create(
line = InvoiceLine.objects.create(
event_date=datetime.date.today(),
invoice=invoice,
quantity=1,
@ -324,9 +324,9 @@ def test_list_invoices_for_payer(app, user):
amount=1,
payment_type=PaymentType.objects.get(regie=regie, slug='cash'),
)
invoice_payment = InvoicePayment.objects.create(
invoice_line_payment = InvoiceLinePayment.objects.create(
payment=payment,
invoice=invoice,
line=line,
amount=1,
)
resp = app.get('/api/regie/foo/invoices/', params={'payer_external_id': 'payer:1'})
@ -355,15 +355,15 @@ def test_list_invoices_for_payer(app, user):
]
# invoice is paid
invoice_payment.amount = 42
invoice_payment.save()
invoice_line_payment.amount = 42
invoice_line_payment.save()
resp = app.get('/api/regie/foo/invoices/', params={'payer_external_id': 'payer:1'})
assert resp.json['err'] == 0
assert resp.json['data'] == []
# campaign is not finalized
invoice_payment.amount = 1
invoice_payment.save()
invoice_line_payment.amount = 1
invoice_line_payment.save()
campaign = Campaign.objects.create(
regie=regie,
date_start=datetime.date(2022, 9, 1),
@ -413,7 +413,7 @@ def test_list_history_invoices(mock_payer, app, user):
)
invoice.set_number()
invoice.save()
InvoiceLine.objects.create(
line = InvoiceLine.objects.create(
event_date=datetime.date.today(),
invoice=invoice,
quantity=1,
@ -425,9 +425,9 @@ def test_list_history_invoices(mock_payer, app, user):
amount=42,
payment_type=PaymentType.objects.get(regie=regie, slug='cash'),
)
invoice_payment = InvoicePayment.objects.create(
invoice_line_payment = InvoiceLinePayment.objects.create(
payment=payment,
invoice=invoice,
line=line,
amount=2,
)
invoice.refresh_from_db()
@ -441,8 +441,8 @@ def test_list_history_invoices(mock_payer, app, user):
assert mock_payer.call_args_list == [mock.call(regie, mock.ANY, 'foobar')]
# invoice with nothing to pay
invoice_payment.amount = 42
invoice_payment.save()
invoice_line_payment.amount = 42
invoice_line_payment.save()
invoice.refresh_from_db()
resp = app.get('/api/regie/foo/invoices/history/', params={'NameID': 'foobar'})
assert resp.json['err'] == 0
@ -552,7 +552,7 @@ def test_list_history_invoices_for_payer(app, user):
)
invoice.set_number()
invoice.save()
InvoiceLine.objects.create(
line = InvoiceLine.objects.create(
event_date=datetime.date.today(),
invoice=invoice,
quantity=1,
@ -563,9 +563,9 @@ def test_list_history_invoices_for_payer(app, user):
amount=42,
payment_type=PaymentType.objects.get(regie=regie, slug='cash'),
)
invoice_payment = InvoicePayment.objects.create(
invoice_line_payment = InvoiceLinePayment.objects.create(
payment=payment,
invoice=invoice,
line=line,
amount=2,
)
invoice.refresh_from_db()
@ -577,8 +577,8 @@ def test_list_history_invoices_for_payer(app, user):
assert resp.json['data'] == []
# invoice with nothing to pay
invoice_payment.amount = 42
invoice_payment.save()
invoice_line_payment.amount = 42
invoice_line_payment.save()
invoice.refresh_from_db()
resp = app.get('/api/regie/foo/invoices/history/', params={'payer_external_id': 'payer:1'})
assert resp.json['err'] == 0
@ -674,7 +674,7 @@ def test_detail_invoice(mock_payer, app, user):
)
invoice.set_number()
invoice.save()
InvoiceLine.objects.create(
line = InvoiceLine.objects.create(
event_date=datetime.date.today(),
invoice=invoice,
quantity=1,
@ -726,9 +726,9 @@ def test_detail_invoice(mock_payer, app, user):
amount=1,
payment_type=PaymentType.objects.get(regie=regie, slug='cash'),
)
invoice_payment = InvoicePayment.objects.create(
invoice_line_payment = InvoiceLinePayment.objects.create(
payment=payment,
invoice=invoice,
line=line,
amount=1,
)
resp = app.get('/api/regie/foo/invoice/%s/' % str(invoice.uuid), params={'NameID': 'foobar'})
@ -755,8 +755,8 @@ def test_detail_invoice(mock_payer, app, user):
}
# invoice is paid
invoice_payment.amount = 42
invoice_payment.save()
invoice_line_payment.amount = 42
invoice_line_payment.save()
resp = app.get('/api/regie/foo/invoice/%s/' % str(invoice.uuid), params={'NameID': 'foobar'})
assert resp.json['err'] == 0
assert resp.json['data'] == {
@ -781,8 +781,8 @@ def test_detail_invoice(mock_payer, app, user):
}
# no matching payer id
invoice_payment.amount = 1
invoice_payment.save()
invoice_line_payment.amount = 1
invoice_line_payment.save()
mock_payer.return_value = 'payer:unknown'
resp = app.get('/api/regie/foo/invoice/%s/' % str(invoice.uuid), params={'NameID': 'foobar'}, status=404)
@ -836,7 +836,7 @@ def test_detail_invoice_for_payer(app, user):
)
invoice.set_number()
invoice.save()
InvoiceLine.objects.create(
line = InvoiceLine.objects.create(
event_date=datetime.date.today(),
invoice=invoice,