From eb3a9b9230c1c4d4174e37cadfb10df2849fd3f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Romain=20S=C3=A9bille?= Date: Mon, 30 Dec 2024 19:43:43 +0100 Subject: [PATCH] WIP --- itou/employee_record/admin.py | 18 +++- .../0003_employeerecordtransitionlog.py | 86 +++++++++++++++++++ itou/employee_record/models.py | 54 ++++++------ tests/employee_record/test_models.py | 44 ---------- 4 files changed, 130 insertions(+), 72 deletions(-) create mode 100644 itou/employee_record/migrations/0003_employeerecordtransitionlog.py diff --git a/itou/employee_record/admin.py b/itou/employee_record/admin.py index b358d9cd30..46f546fe44 100644 --- a/itou/employee_record/admin.py +++ b/itou/employee_record/admin.py @@ -103,6 +103,22 @@ def approval_data_sent(self, obj): return f"{number} ({start} – {end})" +class EmployeeRecordTransitionLog(ASPExchangeInformationAdminMixin, ReadonlyMixin, ItouTabularInline): + model = models.EmployeeRecordTransitionLog + extra = 0 + fields = ( + "transition", + "from_state", + "to_state", + "user", + "timestamp", + "asp_processing_code", + "asp_processing_label", + "asp_batch_file", + ) + raw_id_fields = ("user",) + + @admin.register(models.EmployeeRecord) class EmployeeRecordAdmin(ASPExchangeInformationAdminMixin, ItouModelAdmin): form = EmployeeRecordAdminForm @@ -131,7 +147,7 @@ def schedule_approval_update_notification(self, request, queryset): schedule_approval_update_notification, ] - inlines = (EmployeeRecordUpdateNotificationInline,) + inlines = (EmployeeRecordUpdateNotificationInline, EmployeeRecordTransitionLog) list_display = ( "pk", diff --git a/itou/employee_record/migrations/0003_employeerecordtransitionlog.py b/itou/employee_record/migrations/0003_employeerecordtransitionlog.py new file mode 100644 index 0000000000..02562b9364 --- /dev/null +++ b/itou/employee_record/migrations/0003_employeerecordtransitionlog.py @@ -0,0 +1,86 @@ +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + +import itou.employee_record.models + + +class Migration(migrations.Migration): + dependencies = [ + ("employee_record", "0002_alter_employeerecord_status_and_more"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="EmployeeRecordTransitionLog", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("transition", models.CharField(db_index=True, max_length=255, verbose_name="transition")), + ("from_state", models.CharField(db_index=True, max_length=255, verbose_name="from state")), + ("to_state", models.CharField(db_index=True, max_length=255, verbose_name="to state")), + ( + "timestamp", + models.DateTimeField( + db_index=True, default=django.utils.timezone.now, verbose_name="performed at" + ), + ), + ( + "asp_processing_code", + models.CharField(max_length=4, null=True, verbose_name="code de traitement ASP"), + ), + ( + "asp_processing_label", + models.CharField(max_length=200, null=True, verbose_name="libellé de traitement ASP"), + ), + ( + "asp_batch_file", + models.CharField( + max_length=27, + null=True, + validators=[itou.employee_record.models.validate_asp_batch_filename], + verbose_name="fichier de batch ASP", + ), + ), + ( + "asp_batch_line_number", + models.IntegerField(null=True, verbose_name="ligne correspondante dans le fichier batch ASP"), + ), + ( + "archived_json", + models.JSONField(blank=True, null=True, verbose_name="archive JSON de la fiche salarié"), + ), + ( + "employee_record", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="logs", + to="employee_record.employeerecord", + ), + ), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.RESTRICT, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "log des transitions de la fiche salarié", + "verbose_name_plural": "log des transitions des fiches salarié", + "ordering": ["-timestamp"], + "abstract": False, + "constraints": [ + models.UniqueConstraint( + condition=models.Q(("asp_batch_file__isnull", False)), + fields=("asp_batch_file", "asp_batch_line_number"), + name="unique_employeerecordtransitionlog_asp_batch_file_and_line", + ) + ], + }, + ), + ] diff --git a/itou/employee_record/models.py b/itou/employee_record/models.py index e8a89175e4..9651f804c5 100644 --- a/itou/employee_record/models.py +++ b/itou/employee_record/models.py @@ -3,6 +3,7 @@ import json from dateutil.relativedelta import relativedelta +from django.conf import settings from django.core.exceptions import ValidationError from django.db import models from django.db.models import Exists, OuterRef @@ -84,16 +85,6 @@ def _set_archived_json(self, archive): archive = json.loads(archive) self.archived_json = archive - def set_asp_batch_information(self, file, line_number, archive): - self.asp_batch_file = file - self.asp_batch_line_number = line_number - self._set_archived_json(archive) - - def set_asp_processing_information(self, code, label, archive): - self.asp_processing_code = code - self.asp_processing_label = label - self._set_archived_json(archive) - class EmployeeRecordTransition(enum.StrEnum): READY = "ready" @@ -120,6 +111,7 @@ class EmployeeRecordWorkflow(xwf_models.Workflow): (EmployeeRecordTransition.ENABLE, Status.DISABLED, Status.NEW), (EmployeeRecordTransition.ARCHIVE, CAN_BE_ARCHIVED_STATES, Status.ARCHIVED), ) + log_model = "employee_record.EmployeeRecordTransitionLog" class EmployeeRecordQuerySet(models.QuerySet): @@ -317,7 +309,6 @@ def sent(self, *, file, line_number, archive): The file name is stored for further feedback processing (also done via a file) """ self.clean() - self.set_asp_batch_information(file, line_number, archive) @xwf_models.transition() def reject(self, *, code, label, archive): @@ -325,7 +316,6 @@ def reject(self, *, code, label, archive): Update status after an ASP rejection of the employee record """ self.clean() - self.set_asp_processing_information(code, label, archive) @xwf_models.transition() def process(self, *, code, label, archive, as_duplicate=False): @@ -335,9 +325,6 @@ def process(self, *, code, label, archive, as_duplicate=False): self.clean() self.processed_at = timezone.now() self.processed_as_duplicate = as_duplicate - self.set_asp_processing_information( - code, label if not as_duplicate else "Statut forcé suite à doublon ASP", archive - ) @xwf_models.transition() def enable(self): @@ -453,6 +440,31 @@ def from_job_application(cls, job_application, clean=True): return fs +class EmployeeRecordTransitionLog(xwf_models.BaseTransitionLog, ASPExchangeInformation): + MODIFIED_OBJECT_FIELD = "employee_record" + EXTRA_LOG_ATTRIBUTES = ( + ("user", "user", None), + ("asp_batch_file", "file", None), + ("asp_batch_line_number", "line_number", None), + ("asp_processing_code", "code", None), + ("asp_processing_label", "label", None), + ("archived_json", "archive", None), + ) + + employee_record = models.ForeignKey(EmployeeRecord, related_name="logs", on_delete=models.CASCADE) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + blank=True, + null=True, + on_delete=models.RESTRICT, # For traceability and accountability + ) + + class Meta(ASPExchangeInformation.Meta): + verbose_name = "log des transitions de la fiche salarié" + verbose_name_plural = "log des transitions des fiches salarié" + ordering = ["-timestamp"] + + class EmployeeRecordBatch: """ Transient wrapper for a list of employee records. @@ -583,15 +595,3 @@ class Meta(ASPExchangeInformation.Meta): def __repr__(self): return f"<{type(self).__name__} pk={self.pk}>" - - @xwf_models.transition() - def sent(self, *, file, line_number, archive): - self.set_asp_batch_information(file, line_number, archive) - - @xwf_models.transition() - def reject(self, *, code, label, archive): - self.set_asp_processing_information(code, label, archive) - - @xwf_models.transition() - def process(self, *, code, label, archive): - self.set_asp_processing_information(code, label, archive) diff --git a/tests/employee_record/test_models.py b/tests/employee_record/test_models.py index dce1b65478..c46c95918f 100644 --- a/tests/employee_record/test_models.py +++ b/tests/employee_record/test_models.py @@ -22,7 +22,6 @@ from tests.companies.factories import CompanyFactory from tests.employee_record.factories import ( BareEmployeeRecordFactory, - BareEmployeeRecordUpdateNotificationFactory, EmployeeRecordFactory, EmployeeRecordWithProfileFactory, ) @@ -621,46 +620,3 @@ def test_for_asp_company(self): EmployeeRecord.objects.for_asp_company(employee_record_2.job_application.to_company).get() == employee_record_2 ) - - -@pytest.mark.parametrize("factory", [BareEmployeeRecordFactory, BareEmployeeRecordUpdateNotificationFactory]) -@pytest.mark.parametrize( - "archive,expected_archive", - [ - ('{"Hello": "World"}', {"Hello": "World"}), - ("{}", {}), - ("", ""), - (None, None), - ], - ids=repr, -) -class TestASPExchangeInformationModel: - def test_set_asp_batch_information(self, factory, archive, expected_archive): - obj = factory() - - obj.set_asp_batch_information("RIAE_FS_20230123103950.json", 42, archive) - obj.save() - obj.refresh_from_db() - - assert obj.asp_batch_file == "RIAE_FS_20230123103950.json" - assert obj.asp_batch_line_number == 42 - assert obj.archived_json == expected_archive - - @pytest.mark.parametrize( - "code,expected_code", - [ - ("0000", "0000"), - (9999, "9999"), - ], - ids=repr, - ) - def test_set_asp_processing_information(self, factory, archive, expected_archive, code, expected_code): - obj = factory() - - obj.set_asp_processing_information(code, "The label", archive) - obj.save() - obj.refresh_from_db() - - assert obj.asp_processing_code == expected_code - assert obj.asp_processing_label == "The label" - assert obj.archived_json == expected_archive