pricing: duplicate pricing model (#65231)
This commit is contained in:
parent
fe16e1af83
commit
272c541b94
|
@ -51,6 +51,10 @@ class CriteriaForm(NewCriteriaForm):
|
|||
return slug
|
||||
|
||||
|
||||
class PricingDuplicateForm(forms.Form):
|
||||
label = forms.CharField(label=_('New label'), max_length=150, required=False)
|
||||
|
||||
|
||||
class PricingVariableForm(forms.Form):
|
||||
key = forms.CharField(label=_('Variable name'), required=False)
|
||||
value = forms.CharField(
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
# 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 copy
|
||||
import dataclasses
|
||||
import decimal
|
||||
|
||||
|
@ -252,6 +253,24 @@ class Pricing(models.Model):
|
|||
'categories': [pcc.export_json() for pcc in PricingCriteriaCategory.objects.filter(pricing=self)],
|
||||
}
|
||||
|
||||
def duplicate(self, label=None):
|
||||
# clone current pricing
|
||||
new_pricing = copy.deepcopy(self)
|
||||
new_pricing.pk = None
|
||||
new_pricing.label = label or _('Copy of %s') % self.label
|
||||
# reset slug
|
||||
new_pricing.slug = None
|
||||
new_pricing.save()
|
||||
|
||||
# set criterias
|
||||
new_pricing.criterias.set(self.criterias.all())
|
||||
|
||||
# set categories
|
||||
for pcc in PricingCriteriaCategory.objects.filter(pricing=self):
|
||||
pcc.duplicate(pricing_target=new_pricing)
|
||||
|
||||
return new_pricing
|
||||
|
||||
|
||||
class PricingCriteriaCategory(models.Model):
|
||||
pricing = models.ForeignKey(Pricing, on_delete=models.CASCADE)
|
||||
|
@ -280,6 +299,13 @@ class PricingCriteriaCategory(models.Model):
|
|||
'criterias': [c.slug for c in self.pricing.criterias.all() if c.category == self.category],
|
||||
}
|
||||
|
||||
def duplicate(self, pricing_target):
|
||||
new_pcc = copy.deepcopy(self)
|
||||
new_pcc.pk = None
|
||||
new_pcc.pricing = pricing_target
|
||||
new_pcc.save()
|
||||
return new_pcc
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PricingMatrixCell:
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
<a class="extra-actions-menu-opener"></a>
|
||||
<ul class="extra-actions-menu">
|
||||
<li><a rel="popup" href="{% url 'chrono-manager-pricing-edit' pk=object.pk %}">{% trans 'Options' %}</a></li>
|
||||
<li><a rel="popup" class="action-duplicate" href="{% url 'chrono-manager-pricing-duplicate' pk=object.pk %}">{% trans 'Duplicate' %}</a></li>
|
||||
<li><a href="{% url 'chrono-manager-pricing-export' pk=object.pk %}">{% trans 'Export' %}</a></li>
|
||||
<li><a rel="popup" href="{% url 'chrono-manager-pricing-delete' pk=object.pk %}">{% trans 'Delete' %}</a></li>
|
||||
</ul>
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
{% extends "chrono/pricing/manager_pricing_detail.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'chrono-manager-pricing-duplicate' object.pk %}">{% trans "Duplicate pricing" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans "Duplicate pricing" %}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<div class="buttons">
|
||||
<button class="submit-button">{% trans "Duplicate" %}</button>
|
||||
<a class="cancel" href="{% url 'chrono-manager-pricing-detail' object.pk %}">{% trans 'Cancel' %}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -40,6 +40,11 @@ urlpatterns = [
|
|||
views.pricing_delete,
|
||||
name='chrono-manager-pricing-delete',
|
||||
),
|
||||
url(
|
||||
r'^(?P<pk>\d+)/duplicate/$',
|
||||
views.pricing_duplicate,
|
||||
name='chrono-manager-pricing-duplicate',
|
||||
),
|
||||
url(
|
||||
r'^(?P<pk>\d+)/export/$',
|
||||
views.pricing_export,
|
||||
|
|
|
@ -26,6 +26,7 @@ from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpRespo
|
|||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.views.generic import CreateView, DeleteView, DetailView, FormView, ListView, UpdateView
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
|
||||
from chrono.agendas.models import Agenda
|
||||
from chrono.manager.views import ManagedAgendaMixin, ViewableAgendaMixin
|
||||
|
@ -35,6 +36,7 @@ from chrono.pricing.forms import (
|
|||
NewCriteriaForm,
|
||||
PricingCriteriaCategoryAddForm,
|
||||
PricingCriteriaCategoryEditForm,
|
||||
PricingDuplicateForm,
|
||||
PricingMatrixForm,
|
||||
PricingVariableFormSet,
|
||||
)
|
||||
|
@ -144,6 +146,28 @@ class PricingDeleteView(DeleteView):
|
|||
pricing_delete = PricingDeleteView.as_view()
|
||||
|
||||
|
||||
class PricingDuplicate(SingleObjectMixin, FormView):
|
||||
template_name = 'chrono/pricing/manager_pricing_duplicate_form.html'
|
||||
model = Pricing
|
||||
form_class = PricingDuplicateForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
if not request.user.is_staff:
|
||||
raise PermissionDenied()
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('chrono-manager-pricing-detail', kwargs={'pk': self.new_pricing.pk})
|
||||
|
||||
def form_valid(self, form):
|
||||
self.new_pricing = self.object.duplicate(label=form.cleaned_data['label'])
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
pricing_duplicate = PricingDuplicate.as_view()
|
||||
|
||||
|
||||
class PricingExport(DetailView):
|
||||
model = Pricing
|
||||
|
||||
|
|
|
@ -112,6 +112,36 @@ def test_delete_pricing_as_manager(app, manager_user):
|
|||
app.get('/manage/pricing/%s/delete/' % pricing.pk, status=403)
|
||||
|
||||
|
||||
def test_duplicate_pricing(app, admin_user):
|
||||
pricing = Pricing.objects.create(label='Model')
|
||||
assert Pricing.objects.count() == 1
|
||||
|
||||
app = login(app)
|
||||
resp = app.get('/manage/pricing/%s/' % pricing.pk)
|
||||
resp = resp.click(href='/manage/pricing/%s/duplicate/' % pricing.pk)
|
||||
resp = resp.form.submit()
|
||||
assert Pricing.objects.count() == 2
|
||||
|
||||
new_pricing = Pricing.objects.latest('pk')
|
||||
assert resp.location == '/manage/pricing/%s/' % new_pricing.pk
|
||||
assert new_pricing.pk != pricing.pk
|
||||
|
||||
resp = resp.follow()
|
||||
assert 'copy-of-model' in resp.text
|
||||
|
||||
resp = resp.click(href='/manage/pricing/%s/duplicate/' % new_pricing.pk)
|
||||
resp.form['label'] = 'hop'
|
||||
resp = resp.form.submit().follow()
|
||||
assert 'hop' in resp.text
|
||||
|
||||
|
||||
def test_duplicate_pricing_as_manager(app, manager_user):
|
||||
pricing = Pricing.objects.create(label='Model')
|
||||
|
||||
app = login(app, username='manager', password='manager')
|
||||
app.get('/manage/pricing/%s/duplicate/' % pricing.pk, status=403)
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2021-07-08')
|
||||
def test_import_pricing(app, admin_user):
|
||||
pricing = Pricing.objects.create(label='Model')
|
||||
|
|
|
@ -171,6 +171,57 @@ def test_pricing_category_criteria_duplicate_orders():
|
|||
assert pcc.order == 3
|
||||
|
||||
|
||||
def test_pricing_duplicate():
|
||||
category1 = CriteriaCategory.objects.create(label='Cat 1')
|
||||
Criteria.objects.create(label='Crit 1-1', slug='crit-1-1', category=category1, order=1)
|
||||
Criteria.objects.create(label='Crit 1-2', slug='crit-1-2', category=category1, order=2)
|
||||
category2 = CriteriaCategory.objects.create(label='Cat 2')
|
||||
Criteria.objects.create(label='Crit 2-1', slug='crit-2-1', category=category2, order=1)
|
||||
Criteria.objects.create(label='Crit 2-2', slug='crit-2-2', category=category2, order=2)
|
||||
Criteria.objects.create(label='Crit 2-3', slug='crit-2-3', category=category2, order=3)
|
||||
category3 = CriteriaCategory.objects.create(label='Cat 3')
|
||||
Criteria.objects.create(label='Crit 3-1', slug='crit-3-1', category=category3, order=1)
|
||||
Criteria.objects.create(label='Crit 3-3', slug='crit-3-3', category=category3, order=3)
|
||||
Criteria.objects.create(label='Crit 3-4', slug='crit-3-4', category=category3, order=4)
|
||||
Criteria.objects.create(label='Crit 3-2', slug='crit-3-2', category=category3, order=2)
|
||||
not_used = Criteria.objects.create(label='Not used', slug='crit-3-notused', category=category3, order=5)
|
||||
|
||||
pricing = Pricing.objects.create(
|
||||
label='Foo',
|
||||
extra_variables={
|
||||
'foo': 'bar',
|
||||
},
|
||||
)
|
||||
pricing.categories.add(category1, through_defaults={'order': 1})
|
||||
pricing.categories.add(category2, through_defaults={'order': 2})
|
||||
pricing.categories.add(category3, through_defaults={'order': 3})
|
||||
pricing.criterias.set(Criteria.objects.exclude(pk=not_used.pk))
|
||||
|
||||
new_pricing = pricing.duplicate()
|
||||
assert new_pricing.label == 'Copy of Foo'
|
||||
assert new_pricing.slug == 'copy-of-foo'
|
||||
assert new_pricing.extra_variables == pricing.extra_variables
|
||||
assert list(new_pricing.criterias.all()) == list(pricing.criterias.all())
|
||||
original_pcc_categories = list(
|
||||
PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
|
||||
)
|
||||
new_pcc_categories = list(
|
||||
PricingCriteriaCategory.objects.filter(pricing=new_pricing).values_list('category', flat=True)
|
||||
)
|
||||
assert new_pcc_categories == original_pcc_categories
|
||||
original_pcc_orders = list(
|
||||
PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)
|
||||
)
|
||||
new_pcc_orders = list(
|
||||
PricingCriteriaCategory.objects.filter(pricing=new_pricing).values_list('order', flat=True)
|
||||
)
|
||||
assert new_pcc_orders == original_pcc_orders
|
||||
|
||||
new_pricing = pricing.duplicate(label='Bar')
|
||||
assert new_pricing.label == 'Bar'
|
||||
assert new_pricing.slug == 'bar'
|
||||
|
||||
|
||||
def test_get_agenda_pricing():
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events')
|
||||
pricing = Pricing.objects.create(label='Foo bar')
|
||||
|
|
Loading…
Reference in New Issue