basket: credit generation on validate with negative basket (#83457)
gitea/lingo/pipeline/head Build queued...
Details
gitea/lingo/pipeline/head Build queued...
Details
This commit is contained in:
parent
4517920611
commit
c05baf5da8
|
@ -0,0 +1,19 @@
|
|||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('invoicing', '0074_credit'),
|
||||
('basket', '0005_basket_expiry'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='basket',
|
||||
name='credit',
|
||||
field=models.ForeignKey(
|
||||
null=True, on_delete=django.db.models.deletion.PROTECT, to='invoicing.credit'
|
||||
),
|
||||
),
|
||||
]
|
|
@ -24,7 +24,7 @@ from django.db import models, transaction
|
|||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from lingo.invoicing.models import DraftInvoice, Invoice, Regie
|
||||
from lingo.invoicing.models import Credit, DraftInvoice, Invoice, Regie
|
||||
from lingo.utils.requests_wrapper import requests as requests_wrapper
|
||||
|
||||
|
||||
|
@ -34,6 +34,7 @@ class Basket(models.Model):
|
|||
regie = models.ForeignKey(Regie, on_delete=models.PROTECT)
|
||||
draft_invoice = models.ForeignKey(DraftInvoice, on_delete=models.PROTECT)
|
||||
invoice = models.ForeignKey(Invoice, on_delete=models.PROTECT, null=True)
|
||||
credit = models.ForeignKey(Credit, on_delete=models.PROTECT, null=True)
|
||||
|
||||
payer_nameid = models.CharField(max_length=250)
|
||||
payer_external_id = models.CharField(max_length=250)
|
||||
|
|
|
@ -14,11 +14,14 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import datetime
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import FormView, TemplateView, View
|
||||
|
||||
from lingo.basket.models import Basket
|
||||
|
@ -79,12 +82,28 @@ class BasketValidateView(LoginRequiredMixin, FormView):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if self.basket.draft_invoice.total_amount >= 0:
|
||||
self.generate_invoice()
|
||||
else:
|
||||
self.generate_credit()
|
||||
return redirect(reverse('lingo-basket-detail'))
|
||||
|
||||
def generate_invoice(self):
|
||||
self.basket.invoice = self.basket.draft_invoice.promote()
|
||||
self.basket.status = 'tobepaid'
|
||||
self.basket.validated_at = now()
|
||||
self.basket.save()
|
||||
self.basket.notify('validation')
|
||||
return redirect(reverse('lingo-basket-detail'))
|
||||
|
||||
def generate_credit(self):
|
||||
label = _('Credit from %s') % datetime.date.today().strftime('%d/%m/%Y')
|
||||
self.basket.credit = self.basket.draft_invoice.promote_into_credit(label)
|
||||
self.basket.status = 'completed'
|
||||
self.basket.validated_at = now()
|
||||
self.basket.completed_at = now()
|
||||
self.basket.save()
|
||||
self.basket.notify('validation')
|
||||
self.basket.notify('credit')
|
||||
|
||||
|
||||
basket_validate = BasketValidateView.as_view()
|
||||
|
|
|
@ -954,6 +954,22 @@ class DraftInvoice(AbstractInvoice):
|
|||
|
||||
return final_invoice
|
||||
|
||||
def promote_into_credit(self, label):
|
||||
credit = copy.deepcopy(self)
|
||||
credit.__class__ = Credit
|
||||
credit.pk = None
|
||||
credit.uuid = uuid.uuid4()
|
||||
credit.set_number()
|
||||
credit.assigned_amount = 0
|
||||
credit.remaining_amount = 0
|
||||
credit.label = label
|
||||
credit.save()
|
||||
|
||||
for line in self.lines.all().order_by('pk'):
|
||||
line.promote_into_credit(credit=credit)
|
||||
|
||||
return credit
|
||||
|
||||
|
||||
class Invoice(AbstractInvoice):
|
||||
number = models.PositiveIntegerField(default=0)
|
||||
|
@ -1109,6 +1125,16 @@ class DraftInvoiceLine(AbstractInvoiceLine):
|
|||
for line in self.journal_lines.all().order_by('pk'):
|
||||
line.promote(pool=pool, invoice_line=final_line)
|
||||
|
||||
def promote_into_credit(self, credit=None):
|
||||
credit_line = copy.deepcopy(self)
|
||||
credit_line.__class__ = CreditLine
|
||||
credit_line.pk = None
|
||||
credit_line.credit = credit
|
||||
credit_line.quantity = -self.quantity # inverse quantities, so credit total_amout is positive
|
||||
credit_line.assigned_amount = 0
|
||||
credit_line.remaining_amount = 0
|
||||
credit_line.save()
|
||||
|
||||
|
||||
class InvoiceLine(AbstractInvoiceLine):
|
||||
invoice = models.ForeignKey(Invoice, on_delete=models.PROTECT, null=True, related_name='lines')
|
||||
|
|
|
@ -7,7 +7,7 @@ from django.utils.timezone import now
|
|||
from pyquery import PyQuery
|
||||
|
||||
from lingo.basket.models import Basket, BasketLine, BasketLineItem
|
||||
from lingo.invoicing.models import DraftInvoice, Invoice, Regie
|
||||
from lingo.invoicing.models import Credit, DraftInvoice, DraftInvoiceLine, Invoice, Regie
|
||||
from tests.utils import login
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
@ -261,6 +261,7 @@ def test_basket_validate(app, simple_user):
|
|||
assert Invoice.objects.count() == 1
|
||||
invoice = Invoice.objects.latest('pk')
|
||||
assert basket.invoice == invoice
|
||||
assert basket.credit is None
|
||||
|
||||
# wrong status
|
||||
for status in ['tobepaid', 'cancelled', 'expired', 'completed']:
|
||||
|
@ -272,12 +273,14 @@ def test_basket_validate(app, simple_user):
|
|||
basket.status = 'open'
|
||||
basket.save()
|
||||
line.validation_callback_url = 'http://validation1.com'
|
||||
line.credit_callback_url = 'http://validation1.com'
|
||||
line.save()
|
||||
BasketLine.objects.create(
|
||||
basket=basket,
|
||||
closed=True,
|
||||
user_external_id='user:2',
|
||||
validation_callback_url='http://validation2.com',
|
||||
credit_callback_url='http://validation2.com',
|
||||
)
|
||||
resp = app.get('/basket/validate/')
|
||||
with mock.patch('lingo.utils.requests_wrapper.RequestsSession.send') as mock_send:
|
||||
|
@ -294,6 +297,179 @@ def test_basket_validate(app, simple_user):
|
|||
app.get('/basket/validate/', status=404)
|
||||
|
||||
|
||||
def test_basket_validate_generate_invoice(app, simple_user):
|
||||
app = login(app, username='user', password='user')
|
||||
|
||||
regie = Regie.objects.create(label='Foo')
|
||||
draft_invoice = DraftInvoice.objects.create(
|
||||
regie=regie,
|
||||
date_publication=datetime.date(2023, 4, 21),
|
||||
date_payment_deadline=datetime.date(2023, 4, 22),
|
||||
date_due=datetime.date(2023, 4, 23),
|
||||
)
|
||||
DraftInvoiceLine.objects.create(
|
||||
slug='event-a-foo-bar',
|
||||
label='Event A',
|
||||
event_date=datetime.date(2022, 9, 1),
|
||||
invoice=draft_invoice,
|
||||
quantity=1,
|
||||
unit_amount=1,
|
||||
user_external_id='user:1',
|
||||
)
|
||||
# invoice total amount is positive
|
||||
draft_invoice.refresh_from_db()
|
||||
assert draft_invoice.total_amount == 1
|
||||
basket = Basket.objects.create(
|
||||
regie=regie,
|
||||
draft_invoice=draft_invoice,
|
||||
payer_nameid='ab' * 16,
|
||||
expiry_at=now() + datetime.timedelta(hours=1),
|
||||
)
|
||||
BasketLine.objects.create(
|
||||
basket=basket,
|
||||
closed=True,
|
||||
user_external_id='user:1',
|
||||
group_items=False,
|
||||
)
|
||||
|
||||
resp = app.get('/basket/validate/')
|
||||
resp = resp.form.submit()
|
||||
assert resp.location.endswith('/basket/')
|
||||
basket.refresh_from_db()
|
||||
assert basket.status == 'tobepaid'
|
||||
assert basket.validated_at is not None
|
||||
assert basket.completed_at is None
|
||||
invoice = Invoice.objects.latest('pk')
|
||||
assert basket.invoice == invoice
|
||||
assert invoice.total_amount == 1
|
||||
assert Credit.objects.count() == 0
|
||||
|
||||
# total is zero
|
||||
DraftInvoiceLine.objects.create(
|
||||
slug='event-b-foo-bar',
|
||||
label='Event B',
|
||||
event_date=datetime.date(2022, 9, 1),
|
||||
invoice=draft_invoice,
|
||||
quantity=-1,
|
||||
unit_amount=1,
|
||||
user_external_id='user:1',
|
||||
)
|
||||
draft_invoice.refresh_from_db()
|
||||
assert draft_invoice.total_amount == 0
|
||||
basket.status = 'open'
|
||||
basket.save()
|
||||
|
||||
resp = app.get('/basket/validate/')
|
||||
resp = resp.form.submit()
|
||||
assert resp.location.endswith('/basket/')
|
||||
basket.refresh_from_db()
|
||||
assert basket.status == 'tobepaid'
|
||||
assert basket.validated_at is not None
|
||||
assert basket.completed_at is None
|
||||
invoice = Invoice.objects.latest('pk')
|
||||
assert basket.invoice == invoice
|
||||
assert invoice.total_amount == 0
|
||||
assert Credit.objects.count() == 0
|
||||
|
||||
|
||||
def test_basket_validate_generate_credit(app, simple_user):
|
||||
app = login(app, username='user', password='user')
|
||||
|
||||
regie = Regie.objects.create(label='Foo')
|
||||
draft_invoice = DraftInvoice.objects.create(
|
||||
regie=regie,
|
||||
date_publication=datetime.date(2023, 4, 21),
|
||||
date_payment_deadline=datetime.date(2023, 4, 22),
|
||||
date_due=datetime.date(2023, 4, 23),
|
||||
payer_external_id='payer:1',
|
||||
payer_first_name='First',
|
||||
payer_last_name='Last',
|
||||
payer_address='41 rue des kangourous\n99999 Kangourou Ville',
|
||||
)
|
||||
DraftInvoiceLine.objects.create(
|
||||
slug='event-a-foo-bar',
|
||||
label='Event A',
|
||||
event_date=datetime.date(2022, 9, 1),
|
||||
invoice=draft_invoice,
|
||||
quantity=-1,
|
||||
unit_amount=1,
|
||||
user_external_id='user:1',
|
||||
user_first_name='First1',
|
||||
user_last_name='Last1',
|
||||
)
|
||||
draft_invoice.refresh_from_db()
|
||||
assert draft_invoice.total_amount == -1
|
||||
basket = Basket.objects.create(
|
||||
regie=regie,
|
||||
draft_invoice=draft_invoice,
|
||||
payer_nameid='ab' * 16,
|
||||
expiry_at=now() + datetime.timedelta(hours=1),
|
||||
)
|
||||
line = BasketLine.objects.create(
|
||||
basket=basket,
|
||||
closed=True,
|
||||
user_external_id='user:1',
|
||||
)
|
||||
|
||||
resp = app.get('/basket/validate/')
|
||||
with mock.patch('lingo.utils.requests_wrapper.RequestsSession.send') as mock_send:
|
||||
resp = resp.form.submit()
|
||||
assert {x[0][0].url for x in mock_send.call_args_list} == set()
|
||||
basket.refresh_from_db()
|
||||
assert basket.status == 'completed'
|
||||
assert basket.validated_at is not None
|
||||
assert basket.completed_at is not None
|
||||
assert basket.invoice is None
|
||||
credit = Credit.objects.latest('pk')
|
||||
assert basket.credit == credit
|
||||
assert credit.label == 'Credit from %s' % datetime.date.today().strftime('%d/%m/%Y')
|
||||
assert credit.total_amount == 1
|
||||
assert credit.regie == regie
|
||||
assert credit.payer_external_id == 'payer:1'
|
||||
assert credit.payer_first_name == 'First'
|
||||
assert credit.payer_last_name == 'Last'
|
||||
assert credit.payer_address == '41 rue des kangourous\n99999 Kangourou Ville'
|
||||
assert credit.lines.count() == 1
|
||||
(line1,) = credit.lines.all().order_by('pk')
|
||||
assert line1.event_date == datetime.date(2022, 9, 1)
|
||||
assert line1.slug == 'event-a-foo-bar'
|
||||
assert line1.label == 'Event A'
|
||||
assert line1.quantity == 1
|
||||
assert line1.unit_amount == 1
|
||||
assert line1.total_amount == 1
|
||||
assert line1.user_external_id == 'user:1'
|
||||
assert line1.user_first_name == 'First1'
|
||||
assert line1.user_last_name == 'Last1'
|
||||
assert Invoice.objects.count() == 0
|
||||
|
||||
# check callback
|
||||
basket.status = 'open'
|
||||
basket.save()
|
||||
line.validation_callback_url = 'http://validation1.com'
|
||||
line.credit_callback_url = 'http://credit1.com'
|
||||
line.save()
|
||||
BasketLine.objects.create(
|
||||
basket=basket,
|
||||
closed=True,
|
||||
user_external_id='user:2',
|
||||
validation_callback_url='http://validation2.com',
|
||||
credit_callback_url='http://credit2.com',
|
||||
)
|
||||
resp = app.get('/basket/validate/')
|
||||
with mock.patch('lingo.utils.requests_wrapper.RequestsSession.send') as mock_send:
|
||||
resp = resp.form.submit()
|
||||
basket.refresh_from_db()
|
||||
credit = Credit.objects.latest('pk')
|
||||
assert basket.credit == credit
|
||||
assert Invoice.objects.count() == 0
|
||||
assert {x[0][0].url for x in mock_send.call_args_list} == {
|
||||
'http://validation1.com/',
|
||||
'http://validation2.com/',
|
||||
'http://credit1.com/',
|
||||
'http://credit2.com/',
|
||||
}
|
||||
|
||||
|
||||
def test_basket_cancel(app, simple_user):
|
||||
resp = app.get('/basket/cancel/')
|
||||
assert resp.location.endswith('/login/?next=/basket/cancel/')
|
||||
|
|
Loading…
Reference in New Issue