diff --git a/counter/forms.py b/counter/forms.py index 5e144d44..1c40e5ae 100644 --- a/counter/forms.py +++ b/counter/forms.py @@ -235,6 +235,19 @@ class ScheduledProductActionForm(forms.ModelForm): ) return super().clean() + def set_product(self, product: Product): + """Set the product to which this form's instance is linked. + + When this form is linked to a ProductForm in the case of a product's creation, + the product doesn't exist yet, so saving this form as is will result + in having `{"product_id": null}` in the action kwargs. + For the creation to be useful, it may be needed to inject the newly created + product into this form, before saving the latter. + """ + self.product = product + kwargs = json.loads(self.instance.kwargs) | {"product_id": self.product.id} + self.instance.kwargs = json.dumps(kwargs) + class BaseScheduledProductActionFormSet(BaseModelFormSet): def __init__(self, *args, product: Product, **kwargs): @@ -321,11 +334,19 @@ class ProductForm(forms.ModelForm): def is_valid(self): return super().is_valid() and self.action_formset.is_valid() - def save(self, *args, **kwargs): - ret = super().save(*args, **kwargs) - self.instance.counters.set(self.cleaned_data["counters"]) + def save(self, *args, **kwargs) -> Product: + product = super().save(*args, **kwargs) + product.counters.set(self.cleaned_data["counters"]) + for form in self.action_formset: + # if it's a creation, the product given in the formset + # wasn't a persisted instance. + # So if we tried to persist the scheduled actions in the current state, + # they would be linked to no product, thus be completely useless + # To make it work, we have to replace + # the initial product with a persisted one + form.set_product(product) self.action_formset.save() - return ret + return product class ReturnableProductForm(forms.ModelForm): @@ -369,7 +390,6 @@ class EticketForm(forms.ModelForm): class CloseCustomerAccountForm(forms.Form): user = forms.ModelChoiceField( label=_("Refound this account"), - help_text=None, required=True, widget=AutoCompleteSelectUser, queryset=User.objects.all(), diff --git a/counter/tests/test_auto_actions.py b/counter/tests/test_auto_actions.py index bc7f6937..e0f82ed7 100644 --- a/counter/tests/test_auto_actions.py +++ b/counter/tests/test_auto_actions.py @@ -11,8 +11,12 @@ from model_bakery import baker from core.models import Group, User from counter.baker_recipes import counter_recipe, product_recipe -from counter.forms import ScheduledProductActionForm, ScheduledProductActionFormSet -from counter.models import ScheduledProductAction +from counter.forms import ( + ProductForm, + ScheduledProductActionForm, + ScheduledProductActionFormSet, +) +from counter.models import Product, ScheduledProductAction @pytest.mark.django_db @@ -34,6 +38,39 @@ def test_edit_product(client: Client): assert res.status_code == 200 +@pytest.mark.django_db +def test_create_actions_alongside_product(): + """The form should work when the product and the actions are created alongside.""" + # non-persisted instance + product: Product = product_recipe.prepare(_save_related=True) + trigger_at = now() + timedelta(minutes=10) + form = ProductForm( + data={ + "name": "foo", + "description": "bar", + "product_type": product.product_type_id, + "club": product.club_id, + "code": "FOO", + "purchase_price": 1.0, + "selling_price": 1.0, + "special_selling_price": 1.0, + "limit_age": 0, + "form-TOTAL_FORMS": "2", + "form-INITIAL_FORMS": "0", + "form-0-task": "counter.tasks.archive_product", + "form-0-trigger_at": trigger_at, + }, + ) + assert form.is_valid() + product = form.save() + action = ScheduledProductAction.objects.last() + assert action.clocked.clocked_time == trigger_at + assert action.enabled is True + assert action.one_off is True + assert action.task == "counter.tasks.archive_product" + assert action.kwargs == json.dumps({"product_id": product.id}) + + @pytest.mark.django_db class TestProductActionForm: def test_single_form_archive(self):