diff options
| -rw-r--r-- | archaeological_finds/admin.py | 5 | ||||
| -rw-r--r-- | archaeological_finds/migrations/0146_statement_condition.py | 168 | ||||
| -rw-r--r-- | archaeological_finds/migrations/0147_data_migration_statement_condition.json | 46 | ||||
| -rw-r--r-- | archaeological_finds/migrations/0147_data_migration_statement_condition.py | 21 | ||||
| -rw-r--r-- | archaeological_finds/models.py | 8 | ||||
| -rw-r--r-- | archaeological_finds/models_finds.py | 42 | ||||
| -rw-r--r-- | archaeological_finds/models_treatments.py | 329 | ||||
| -rw-r--r-- | ishtar_common/migrations/0272_ishtarsiteprofile_statementcondition.py | 42 | ||||
| -rw-r--r-- | ishtar_common/models.py | 19 |
9 files changed, 676 insertions, 4 deletions
diff --git a/archaeological_finds/admin.py b/archaeological_finds/admin.py index ff131c9b3..555ef8821 100644 --- a/archaeological_finds/admin.py +++ b/archaeological_finds/admin.py @@ -31,7 +31,7 @@ class BaseFindAdmin(HistorizedObjectAdmin, MainGeoDataItem): exclude = ["line"] model = models.BaseFind autocomplete_fields = HistorizedObjectAdmin.autocomplete_fields + \ - MainGeoDataItem.autocomplete_fields + ["context_record"] + MainGeoDataItem.autocomplete_fields + ["context_record"] readonly_fields = HistorizedObjectAdmin.readonly_fields + [ 'cache_short_id', 'cache_complete_id', ] @@ -273,7 +273,8 @@ general_models = [ models.InventoryMarkingPresence, models.MarkingType, models.MaterialTypeQualityType, models.MuseumCollection, models.ObjectTypeQualityType, models.OriginalReproduction, models.RemarkabilityType, models.TreatmentEmergencyType, models.DiscoveryMethod, - models.ExhibitionType, models.OwnershipStatus + models.ExhibitionType, models.OwnershipStatus, models.FollowUpActionType, + models.StatementConditionType ] for model in general_models: diff --git a/archaeological_finds/migrations/0146_statement_condition.py b/archaeological_finds/migrations/0146_statement_condition.py new file mode 100644 index 000000000..03a71f3cf --- /dev/null +++ b/archaeological_finds/migrations/0146_statement_condition.py @@ -0,0 +1,168 @@ +# Generated by Django 4.2.21 on 2026-03-04 14:04 + +from django.conf import settings +import django.contrib.postgres.indexes +import django.contrib.postgres.search +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import ishtar_common.models +import ishtar_common.models_common +import re + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('ishtar_common', '0272_ishtarsiteprofile_statementcondition'), + ('archaeological_finds', '0145_migrate_periods_and_datings'), + ] + + operations = [ + migrations.CreateModel( + name='FollowUpActionType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('label', models.TextField(verbose_name='Label')), + ('txt_idx', models.TextField(help_text='The slug is the standardized version of the name. It contains only lowercase letters, numbers and hyphens. Each slug must be unique.', unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z'), 'Enter a valid “slug” consisting of letters, numbers, underscores or hyphens.', 'invalid')], verbose_name='Textual ID')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('available', models.BooleanField(default=True, verbose_name='Available')), + ('order', models.IntegerField(default=10, verbose_name='Order')), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='archaeological_finds.followupactiontype', verbose_name='Parent')), + ], + options={ + 'verbose_name': 'Follow-up action type', + 'verbose_name_plural': 'Follow-up action types', + 'ordering': ('order', 'parent__label', 'label'), + }, + bases=(ishtar_common.models_common.Cached, models.Model), + ), + migrations.AlterModelOptions( + name='ownertype', + options={'ordering': ('parent__order', 'parent__label', 'order', 'label'), 'verbose_name': 'Owner type', 'verbose_name_plural': 'Owner types'}, + ), + migrations.AddField( + model_name='treatmenttype', + name='is_statement_condition', + field=models.BooleanField(default=False, help_text='Available as a treatment for statement condition.', verbose_name='Related to statement condition'), + ), + migrations.AlterField( + model_name='find', + name='preservation_to_considers', + field=models.ManyToManyField(blank=True, help_text='Deprecated', related_name='old_finds_recommended', to='archaeological_finds.treatmenttype', verbose_name='Recommended treatments'), + ), + migrations.AlterField( + model_name='ownershipstatus', + name='txt_idx', + field=models.TextField(help_text='The slug is the standardized version of the name. It contains only lowercase letters, numbers and hyphens. Each slug must be unique.', unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z'), 'Enter a valid “slug” consisting of letters, numbers, underscores or hyphens.', 'invalid')], verbose_name='Textual ID'), + ), + migrations.AlterField( + model_name='ownertype', + name='txt_idx', + field=models.TextField(help_text='The slug is the standardized version of the name. It contains only lowercase letters, numbers and hyphens. Each slug must be unique.', unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z'), 'Enter a valid “slug” consisting of letters, numbers, underscores or hyphens.', 'invalid')], verbose_name='Textual ID'), + ), + migrations.AlterField( + model_name='recommendedtreatmenttype', + name='txt_idx', + field=models.TextField(help_text='The slug is the standardized version of the name. It contains only lowercase letters, numbers and hyphens. Each slug must be unique.', unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z'), 'Enter a valid “slug” consisting of letters, numbers, underscores or hyphens.', 'invalid')], verbose_name='Textual ID'), + ), + migrations.CreateModel( + name='StatementConditionType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('label', models.TextField(verbose_name='Label')), + ('txt_idx', models.TextField(help_text='The slug is the standardized version of the name. It contains only lowercase letters, numbers and hyphens. Each slug must be unique.', unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z'), 'Enter a valid “slug” consisting of letters, numbers, underscores or hyphens.', 'invalid')], verbose_name='Textual ID')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('available', models.BooleanField(default=True, verbose_name='Available')), + ('order', models.IntegerField(default=10, verbose_name='Order')), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='archaeological_finds.statementconditiontype', verbose_name='Parent')), + ], + options={ + 'verbose_name': 'Statement condition type', + 'verbose_name_plural': 'Statement condition types', + 'ordering': ('order', 'parent__label', 'label'), + }, + bases=(ishtar_common.models_common.Cached, models.Model), + ), + migrations.CreateModel( + name='StatementCondition', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('search_vector', django.contrib.postgres.search.SearchVectorField(blank=True, help_text='Auto filled at save', null=True, verbose_name='Search vector')), + ('timestamp_geo', models.IntegerField(blank=True, null=True, verbose_name='Timestamp geo')), + ('timestamp_label', models.IntegerField(blank=True, null=True, verbose_name='Timestamp label')), + ('data', models.JSONField(blank=True, default=dict)), + ('last_modified', models.DateTimeField(blank=True, default=django.utils.timezone.now)), + ('created', models.DateTimeField(blank=True, default=django.utils.timezone.now)), + ('history_m2m', models.JSONField(blank=True, default=dict)), + ('need_update', models.BooleanField(default=False, verbose_name='Need update')), + ('locked', models.BooleanField(default=False, verbose_name='Item locked for edition')), + ('cached_label', models.TextField(blank=True, db_index=True, default='', help_text='Generated automatically - do not edit', verbose_name='Cached name')), + ('complete_identifier', models.TextField(blank=True, default='', verbose_name='Complete identifier')), + ('custom_index', models.IntegerField(blank=True, null=True, verbose_name='Custom index')), + ('date', models.DateField(verbose_name='Date')), + ('applied', models.CharField(choices=[('D', 'Draft'), ('V', 'Validated'), ('T', 'Validated with treatment')], default='D', verbose_name='Input status')), + ('initial', models.BooleanField(default=False, verbose_name='Initial')), + ('last', models.BooleanField(default=True, verbose_name='Last')), + ('campaign_number', models.TextField(blank=True, default='', verbose_name='Campaign/observation number')), + ('report_number', models.TextField(blank=True, default='', verbose_name='Report number')), + ('observations', models.TextField(blank=True, default='', verbose_name='Observations')), + ('conservatory_comment', models.TextField(blank=True, default='', verbose_name='Conservatory comment')), + ('description', models.TextField(blank=True, default='', verbose_name='Description')), + ('find_number', models.IntegerField(blank=True, null=True, verbose_name='Number of remains')), + ('museum_observed_quantity', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Observed quantity')), + ('length', models.FloatField(blank=True, null=True, verbose_name='Length (cm)')), + ('width', models.FloatField(blank=True, null=True, verbose_name='Width (cm)')), + ('height', models.FloatField(blank=True, null=True, verbose_name='Height (cm)')), + ('volume', models.FloatField(blank=True, null=True, verbose_name='Volume (l)')), + ('weight', models.FloatField(blank=True, null=True, verbose_name='Weight')), + ('diameter', models.FloatField(blank=True, null=True, verbose_name='Diameter (cm)')), + ('circumference', models.FloatField(blank=True, null=True, verbose_name='Circumference (cm)')), + ('thickness', models.FloatField(blank=True, null=True, verbose_name='Thickness (cm)')), + ('clutter_long_side', models.FloatField(blank=True, null=True, verbose_name='Clutter - long side (cm)')), + ('clutter_short_side', models.FloatField(blank=True, null=True, verbose_name='Clutter - short side (cm)')), + ('clutter_height', models.FloatField(blank=True, null=True, verbose_name='Clutter - height (cm)')), + ('dimensions_comment', models.TextField(blank=True, default='', verbose_name='Dimensions comment')), + ('alteration_causes', models.ManyToManyField(blank=True, to='archaeological_finds.alterationcausetype', verbose_name='Alteration cause')), + ('alterations', models.ManyToManyField(blank=True, to='archaeological_finds.alterationtype', verbose_name='Alteration')), + ('conservatory_states', models.ManyToManyField(blank=True, to='archaeological_finds.conservatorystate', verbose_name='Conservatory states')), + ('find', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='statement_conditions', to='archaeological_finds.find', verbose_name='Find')), + ('follow_up_actions', models.ManyToManyField(blank=True, to='archaeological_finds.followupactiontype', verbose_name='Follow-up actions')), + ('history_creator', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Creator')), + ('history_modifier', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Last editor')), + ('imports', models.ManyToManyField(blank=True, related_name='imported_%(app_label)s_%(class)s', to='ishtar_common.import', verbose_name='Created by imports')), + ('imports_updated', models.ManyToManyField(blank=True, related_name='import_updated_%(app_label)s_%(class)s', to='ishtar_common.import', verbose_name='Updated by imports')), + ('integrities', models.ManyToManyField(blank=True, to='archaeological_finds.integritytype', verbose_name='Integrity')), + ('ishtar_users', models.ManyToManyField(blank=True, related_name='%(class)s_associated', to='ishtar_common.ishtaruser')), + ('lock_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Locked by')), + ('museum_inventory_marking_presence', models.ManyToManyField(blank=True, related_name='statement_conditions', to='archaeological_finds.inventorymarkingpresence', verbose_name='Presence of inventory marking')), + ('museum_marking_type', models.ManyToManyField(blank=True, related_name='statement_conditions', to='archaeological_finds.markingtype', verbose_name='Type of marking')), + ('recommended_treatments', models.ManyToManyField(blank=True, to='archaeological_finds.recommendedtreatmenttype', verbose_name='Recommended treatments')), + ('statement_condition_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='archaeological_finds.statementconditiontype', verbose_name='Type')), + ('treatment_emergency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='archaeological_finds.treatmentemergencytype', verbose_name='Treatment emergency')), + ('verification_officer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.person', verbose_name='Verification officer')), + ], + options={ + 'verbose_name': 'Statement of condition', + 'verbose_name_plural': 'Statements of condition', + 'ordering': ('find', '-date', 'cached_label'), + }, + bases=(ishtar_common.models_common.DocumentItem, ishtar_common.models_common.StatisticItem, ishtar_common.models_common.TemplateItem, models.Model, ishtar_common.models_common.CachedGen, ishtar_common.models_common.FixAssociated, ishtar_common.models.ValueGetter), + ), + migrations.AddField( + model_name='historicaltreatment', + name='statement_condition', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='archaeological_finds.statementcondition', verbose_name='Statement condition'), + ), + migrations.AddField( + model_name='treatment', + name='statement_condition', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='archaeological_finds.statementcondition', verbose_name='Statement condition'), + ), + migrations.AddIndex( + model_name='statementcondition', + index=django.contrib.postgres.indexes.GinIndex(fields=['data'], name='archaeologi_data_fdbd6f_gin'), + ), + ] diff --git a/archaeological_finds/migrations/0147_data_migration_statement_condition.json b/archaeological_finds/migrations/0147_data_migration_statement_condition.json new file mode 100644 index 000000000..369099933 --- /dev/null +++ b/archaeological_finds/migrations/0147_data_migration_statement_condition.json @@ -0,0 +1,46 @@ +[ +{ + "model": "archaeological_finds.statementconditiontype", + "fields": { + "label": "Constat d'\u00e9tat", + "txt_idx": "constat-d-etat", + "comment": "", + "available": true, + "order": 10, + "parent": null + } +}, +{ + "model": "archaeological_finds.statementconditiontype", + "fields": { + "label": "R\u00e9colement", + "txt_idx": "recolement", + "comment": "", + "available": true, + "order": 20, + "parent": null + } +}, +{ + "model": "archaeological_finds.statementconditiontype", + "fields": { + "label": "Vu", + "txt_idx": "recolement-vu", + "comment": "", + "available": true, + "order": 10, + "parent": ["recolement"] + } +}, +{ + "model": "archaeological_finds.statementconditiontype", + "fields": { + "label": "Non vu", + "txt_idx": "recolement-non-vu", + "comment": "", + "available": true, + "order": 20, + "parent": ["recolement"] + } +} +] diff --git a/archaeological_finds/migrations/0147_data_migration_statement_condition.py b/archaeological_finds/migrations/0147_data_migration_statement_condition.py new file mode 100644 index 000000000..eda8ad061 --- /dev/null +++ b/archaeological_finds/migrations/0147_data_migration_statement_condition.py @@ -0,0 +1,21 @@ +import os + +from django.db import migrations +from django.core.management import call_command + + +def load_data(apps, __): + migration = "0147_data_migration_statement_condition.json" + json_path = os.sep.join(os.path.abspath(__file__).split(os.sep)[:-1] + [migration]) + call_command("loaddata", json_path) + + +class Migration(migrations.Migration): + + dependencies = [ + ('archaeological_finds', '0146_statement_condition'), + ] + + operations = [ + migrations.RunPython(load_data) + ] diff --git a/archaeological_finds/models.py b/archaeological_finds/models.py index ef5a5b9a5..d79369711 100644 --- a/archaeological_finds/models.py +++ b/archaeological_finds/models.py @@ -41,11 +41,14 @@ from archaeological_finds.models_treatments import ( AbsFindTreatments, Exhibition, ExhibitionType, + FollowUpActionType, Treatment, FindDownstreamTreatments, FindNonModifTreatments, FindTreatments, FindUpstreamTreatments, + StatementCondition, + StatementConditionType, TreatmentFile, TreatmentFileType, TreatmentInputStatus, @@ -78,6 +81,7 @@ __all__ = [ "FindTreatment", "FindTreatments", "FindUpstreamTreatments", + "FollowUpActionType", "FunctionalArea", "IntegrityType", "InventoryConformity", @@ -89,9 +93,13 @@ __all__ = [ "ObjectType", "ObjectTypeQualityType", "OriginalReproduction", + "OwnershipStatus", + "OwnerType", "Property", "RemarkabilityType", "RecommendedTreatmentType", + "StatementCondition", + "StatementConditionType", "TechnicalAreaType", "TechnicalProcessType", "Treatment", diff --git a/archaeological_finds/models_finds.py b/archaeological_finds/models_finds.py index f1144a09a..9e9886635 100644 --- a/archaeological_finds/models_finds.py +++ b/archaeological_finds/models_finds.py @@ -180,6 +180,11 @@ class TreatmentType(HierarchicalType): "current location." ), ) + is_statement_condition = models.BooleanField( + _("Related to statement condition"), + default=False, + help_text=_("Available as a treatment for statement condition."), + ) class Meta: verbose_name = _("Treatment type") @@ -2419,6 +2424,7 @@ class Find( verbose_name=_("Recommended treatments"), related_name="old_finds_recommended", blank=True, + help_text=_("Deprecated") ) recommended_treatments = models.ManyToManyField( RecommendedTreatmentType, @@ -2597,6 +2603,42 @@ class Find( return reverse("show-find", args=[self.pk, ""]) @property + def statement_conditions_list(self): + if not self.statement_conditions.exists(): + return [] + # get all diff from previous states + StatementCondition = apps.get_model("archaeological_finds", "StatementCondition") + statement_conditions_list = list( + self.statement_conditions.filter(initial=False).order_by("pk").all()) + q = self.statement_conditions.order_by("-pk").filter(initial=True) + if q.exists(): + previous = q.all()[0] + else: # if no initial is set get diff from the find + previous = self + for state in statement_conditions_list: + diff = {} + for field in StatementCondition._meta.get_fields(): + attr = field.name + if attr in StatementCondition.OVERLOADED_FIELDS: + previous_value = getattr(previous, attr) or None + value = getattr(state, attr) or None + if previous_value != value: + if value is None or value == "": + value = "-" + diff[field.verbose_name] = value + elif attr in StatementCondition.OVERLOADED_M2M_FIELDS: + previous_value = [str(v) for v in getattr(previous, attr).all()] + value = [str(v) for v in list(getattr(state, attr).all())] + if previous_value != value: + if value: + diff[field.verbose_name] = " ; ".join(value) + else: + diff[field.verbose_name] = "-" + state.diff = diff + previous = state + return list(reversed(statement_conditions_list)) + + @property def has_packaging_for_current_container(self): return FindTreatment.objects.filter(find=self, location_type__in=["B", "C"]).exists() diff --git a/archaeological_finds/models_treatments.py b/archaeological_finds/models_treatments.py index 845da66ac..cd82ab57e 100644 --- a/archaeological_finds/models_treatments.py +++ b/archaeological_finds/models_treatments.py @@ -51,6 +51,7 @@ from ishtar_common.models import ( HistoryModel, ImageModel, MainItem, + OrderedHierarchicalType, OrderedType, Organization, OwnPerms, @@ -59,7 +60,11 @@ from ishtar_common.models import ( SearchVectorConfig, ValueGetter, ) -from ishtar_common.models_common import CompleteIdentifierItem, HistoricalRecords +from ishtar_common.models_common import ( + CompleteIdentifierItem, + IdentifierItem, + HistoricalRecords +) from ishtar_common.utils import ( cached_label_changed, get_current_year, @@ -385,6 +390,13 @@ class Treatment( null=True, on_delete=models.SET_NULL, ) + statement_condition = models.ForeignKey( + "StatementCondition", + verbose_name=_("Statement condition"), + blank=True, + null=True, + on_delete=models.SET_NULL, + ) treatment_status = models.ForeignKey( TreatmentStatus, verbose_name=_("Treatment status"), @@ -1642,10 +1654,323 @@ class TreatmentFile( m2m_changed.connect(document_attached_changed, sender=TreatmentFile.documents.through) - post_save.connect(cached_label_changed, sender=TreatmentFile) +class StatementConditionType(OrderedHierarchicalType): + class Meta: + verbose_name = _("Statement condition type") + verbose_name_plural = _("Statement condition types") + ordering = ( + "order", + "parent__label", + "label", + ) + ADMIN_SECTION = _("Treatments") + + +post_save.connect(post_save_cache, sender=StatementConditionType) +post_delete.connect(post_save_cache, sender=StatementConditionType) + + +class FollowUpActionType(OrderedHierarchicalType): + class Meta: + verbose_name = _("Follow-up action type") + verbose_name_plural = _("Follow-up action types") + ordering = ( + "order", + "parent__label", + "label", + ) + ADMIN_SECTION = _("Treatments") + + +post_save.connect(post_save_cache, sender=FollowUpActionType) +post_delete.connect(post_save_cache, sender=FollowUpActionType) + + +class StatementCondition( + DocumentItem, + BaseHistorizedItem, + IdentifierItem, + ValueGetter, +): + SLUG = "statementcondition" + APP = "archaeological-finds" + MODEL = SLUG + SHOW_URL = "show-statementcondition" + + # changing theses fields on the statement condition change them in + # associated find + OVERLOADED_FIELDS = ( + "description", + "conservatory_comment", + "length", + "width", + "height", + "volume", + "weight", + "diameter", + "circumference", + "thickness", + "clutter_long_side", + "clutter_short_side", + "clutter_height", + "dimensions_comment", + "treatment_emergency_id", + "find_number", + "museum_observed_quantity", + ) + OVERLOADED_M2M_FIELDS = ( + "integrities", + "conservatory_states", + "recommended_treatments", + "alterations", + "alteration_causes", + "museum_marking_type", + "museum_inventory_marking_presence" + ) + APPLIED_CHOICES = ( + ("D", _("Draft")), + ("V", _("Validated")), + ("T", _("Validated with treatment")), + ) + + date = models.DateField(_("Date")) + applied = models.CharField(_("Input status"), default="D", choices=APPLIED_CHOICES) + initial = models.BooleanField(_("Initial"), default=False) + last = models.BooleanField(_("Last"), default=True) + find = models.ForeignKey(Find, verbose_name=_("Find"), on_delete=models.CASCADE, + related_name="statement_conditions") + campaign_number = models.TextField(_("Campaign/observation number"), default="", + blank=True) + report_number = models.TextField(_("Report number"), default="", blank=True) + verification_officer = models.ForeignKey(Person, verbose_name=_("Verification officer"), null=True, + blank=True, on_delete=models.SET_NULL) + statement_condition_type = models.ForeignKey( + StatementConditionType, verbose_name=_("Type"), on_delete=models.PROTECT) + follow_up_actions = models.ManyToManyField( + FollowUpActionType, verbose_name=_("Follow-up actions"), blank=True + ) + observations = models.TextField(_("Observations"), blank=True, default="") + + # find field + integrities = models.ManyToManyField( + "IntegrityType", + verbose_name=_("Integrity"), + blank=True, + ) + conservatory_states = models.ManyToManyField( + "ConservatoryState", + verbose_name=_("Conservatory states"), + blank=True, + ) + recommended_treatments = models.ManyToManyField( + "RecommendedTreatmentType", + verbose_name=_("Recommended treatments"), + blank=True, + ) + treatment_emergency = models.ForeignKey( + "TreatmentEmergencyType", + verbose_name=_("Treatment emergency"), + on_delete=models.SET_NULL, + blank=True, + null=True, + ) + conservatory_comment = models.TextField( + _("Conservatory comment"), blank=True, default="" + ) + alterations = models.ManyToManyField( + "AlterationType", verbose_name=_("Alteration"), blank=True + ) + alteration_causes = models.ManyToManyField( + "AlterationCauseType", verbose_name=_("Alteration cause"), blank=True, + ) + description = models.TextField(_("Description"), blank=True, default="") + # find fields - museum + museum_inventory_marking_presence = models.ManyToManyField( + "InventoryMarkingPresence", blank=True, + related_name="statement_conditions", + verbose_name=_("Presence of inventory marking"), + ) + museum_marking_type = models.ManyToManyField( + "MarkingType", + verbose_name=_("Type of marking"), + blank=True, + related_name="statement_conditions", + ) + # find field - dimensions + find_number = models.IntegerField(_("Number of remains"), blank=True, null=True) + museum_observed_quantity = models.PositiveSmallIntegerField( + _("Observed quantity"), blank=True, null=True + ) + length = models.FloatField(_("Length (cm)"), blank=True, null=True) + width = models.FloatField(_("Width (cm)"), blank=True, null=True) + height = models.FloatField(_("Height (cm)"), blank=True, null=True) + volume = models.FloatField(_("Volume (l)"), blank=True, null=True) + weight = models.FloatField(_("Weight"), blank=True, null=True) + diameter = models.FloatField(_("Diameter (cm)"), blank=True, null=True) + circumference = models.FloatField(_("Circumference (cm)"), blank=True, null=True) + thickness = models.FloatField(_("Thickness (cm)"), blank=True, null=True) + clutter_long_side = models.FloatField( + _("Clutter - long side (cm)"), blank=True, null=True + ) + clutter_short_side = models.FloatField( + _("Clutter - short side (cm)"), blank=True, null=True + ) + clutter_height = models.FloatField( + _("Clutter - height (cm)"), blank=True, null=True + ) + dimensions_comment = models.TextField( + _("Dimensions comment"), blank=True, default="" + ) + + class Meta: + verbose_name = _("Statement of condition") + verbose_name_plural = _("Statements of condition") + ordering = ("find", "-date", "cached_label") + indexes = [ + GinIndex(fields=["data"]), + ] + ADMIN_SECTION = _("Treatments") + + @property + def is_last(self): + return not self.__class__.objects.filter(date__gt=self.date).exists() + + @property + def applied_label(self): + dct = dict(self.APPLIED_CHOICES) + if self.applied not in dct: + return "-" + return dct[self.applied] + + @classmethod + def get_initial_from_find(cls, find, prefix="qa_"): + initial = {} + base_attrs = list(cls.OVERLOADED_FIELDS) + for attr in base_attrs: + initial[f"{prefix}{attr}"] = getattr(find, attr) + m2m_attrs = cls.OVERLOADED_M2M_FIELDS + for attr in m2m_attrs: + initial[f"{prefix}{attr}"] = list( + getattr(find, attr).all().values_list("id", flat=True) + ) + return initial + + def get_initial(self, prefix="qa_"): + initial = {} + base_attrs = ["pk", "date", "find_id", "statement_condition_type_id", "verification_officer_id", + "campaign_number", "report_number", "observations"] + for attr in base_attrs: + initial[attr] = getattr(self, attr) + for attr in self.OVERLOADED_FIELDS: + initial[f"{prefix}{attr}"] = getattr(self, attr) + m2m_attrs = list(self.OVERLOADED_M2M_FIELDS) + ["follow_up_actions"] + for attr in m2m_attrs: + initial[f"{prefix}{attr}"] = list( + getattr(self, attr).all().values_list("id", flat=True) + ) + return initial + + def _check_apply_validation(self): + if not self.pk: + # no previous state so apply immediatly + return True + # check previous applied is draft + return list( + self.__class__.objects.filter(pk=self.pk).values_list( + "applied", flat=True) + )[0] == "D" + + def _create_associated_treatment(self, treatment_fields): + pass + + def _create_initial_statementcondition(self): + """ + Create a reference statement condition in order to get the diff + """ + obj = self.__class__.objects.get(pk=self.pk) + obj.pk = None # duplicate + obj.applied = "V" + obj.initial = True + obj.last = False + obj.verification_officer = None + for k in ("campaign_number", "report_number", "observations"): + setattr(obj, k, "") + # reinit with find fields + for attr in self.OVERLOADED_FIELDS: + setattr(obj, attr, getattr(self.find, attr)) + obj.save() + obj.follow_up_actions.clear() + + for attr in self.OVERLOADED_M2M_FIELDS: + new_m2m = list( + sorted( + list(getattr(self.find, attr).values_list("id", flat=True)) + ) + ) + m2m = list( + sorted( + list(getattr(obj, attr).values_list("id", flat=True)) + ) + ) + if m2m == new_m2m: + continue + getattr(obj, attr).clear() + getattr(obj, attr).add(*new_m2m) + + def apply_validation(self, treatment_fields=None): + """ + Copy statement condition fields to the associated find + """ + # create a reference condition if not available + if not self.__class__.objects.filter(initial=True, find=self.find).exists(): + self._create_initial_statementcondition() + + # treatment creation + if self.applied == "T": + find = self._create_associated_treatment(treatment_fields) + if find.pk != self.find_id: # new find is created after treatment + self.find, self.find_id = find, find.pk + self.__class__.objects.filter(pk=self.pk).update(find_id=find.pk) + + # update find fields + for attr in self.OVERLOADED_FIELDS: + setattr(self.find, attr, getattr(self, attr)) + for attr in self.OVERLOADED_M2M_FIELDS: + m2m = list( + sorted( + list(getattr(self.find, attr).values_list("id", flat=True)) + ) + ) + new_m2m = list( + sorted( + list(getattr(self, attr).values_list("id", flat=True)) + ) + ) + if m2m == new_m2m: + continue + getattr(self.find, attr).clear() + getattr(self.find, attr).add(*new_m2m) + # TODO: verify find history + self.find.save() + + # update last field + self.__class__.objects.filter(find=self.find).exclude(pk=self.pk).update( + last=False) + # set last field and applied state + self.__class__.objects.filter(pk=self.pk).update(last=True, applied=self.applied) + + def save(self, *args, **kwargs): + apply_validation = False + if self.applied in ("V", "T"): + apply_validation = self._check_apply_validation() + super().save(*args, **kwargs) + if apply_validation: + self.apply_validation() + + class ExhibitionType(GeneralType): treatment_file_type = models.ForeignKey( TreatmentFileType, diff --git a/ishtar_common/migrations/0272_ishtarsiteprofile_statementcondition.py b/ishtar_common/migrations/0272_ishtarsiteprofile_statementcondition.py new file mode 100644 index 000000000..0f26cb382 --- /dev/null +++ b/ishtar_common/migrations/0272_ishtarsiteprofile_statementcondition.py @@ -0,0 +1,42 @@ +# Generated by Django 4.2.19 on 2026-01-20 10:15 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('ishtar_common', '0271_importerduplicatefield_concat_str'), + ] + + operations = [ + migrations.AlterModelOptions( + name='import', + options={'permissions': (('view_own_import', 'Can view own Import'), ('add_own_import', 'Can add own Import'), ('change_own_import', 'Can change own Import'), ('delete_own_import', 'Can delete own Import'), ('view_gis_import', 'Can export to QGIS'), ('view_own_gis_import', 'Can export own to QGIS'), ('change_gis_import', 'Can import from QGIS'), ('change_own_gis_import', 'Can import own from QGIS')), 'verbose_name': 'Import - Import', 'verbose_name_plural': 'Import - Imports'}, + ), + migrations.AlterModelOptions( + name='importertype', + options={'ordering': ('name',), 'verbose_name': 'Importer - Type', 'verbose_name_plural': 'Importer - Types'}, + ), + migrations.AlterField( + model_name='documenttemplate', + name='export_format', + field=models.CharField(blank=True, choices=[('docx', 'DOCX'), ('html', 'HTML'), ('pdf', 'PDF'), ('xlsx', 'XLSX')], default='', max_length=4, verbose_name='Export format'), + ), + migrations.AlterField( + model_name='importertype', + name='gis_type', + field=models.ForeignKey(blank=True, help_text='For QGIS importer type. Geographic data type used for import and export.', null=True, on_delete=django.db.models.deletion.CASCADE, to='ishtar_common.geodatatype', verbose_name='GIS Type'), + ), + migrations.AddField( + model_name='ishtarsiteprofile', + name='statementcondition_complete_identifier', + field=models.TextField(default='--TO BE DEFINED---', help_text='Formula to manage statement of condition complete identifier.', verbose_name='Statement of condition complete identifier'), + ), + migrations.AddField( + model_name='ishtarsiteprofile', + name='statementcondition_custom_index', + field=models.TextField(blank=True, default='', help_text='Keys to be used to manage statement of condition custom index. Separate keys with a semicolon.', verbose_name='Statement of condition custom index key'), + ), + ] diff --git a/ishtar_common/models.py b/ishtar_common/models.py index 5e8107d17..40c69b753 100644 --- a/ishtar_common/models.py +++ b/ishtar_common/models.py @@ -271,6 +271,9 @@ post_save.connect(post_save_user, sender=User) class ValueGetter: + """ + Manage values for templates + """ _prefix = "" COL_LABELS = {} GET_VALUES_EXTRA = [] @@ -1466,6 +1469,22 @@ class IshtarSiteProfile(models.Model, Cached): "Formula to manage cached label. If not set a default formula is used." ), ) + statementcondition_complete_identifier = models.TextField( + _("Statement of condition complete identifier"), + default="--TO BE DEFINED---", + help_text=_( + "Formula to manage statement of condition complete identifier." + ), + ) + statementcondition_custom_index = models.TextField( + _("Statement of condition custom index key"), + default="", + blank=True, + help_text=_( + "Keys to be used to manage statement of condition custom index. " + "Separate keys with a semicolon." + ), + ) container_external_id = models.TextField( _("Container external id"), default="{parent_external_id}-{container_type__txt_idx}-" "{reference}", |
