summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--archaeological_finds/admin.py5
-rw-r--r--archaeological_finds/migrations/0146_statement_condition.py168
-rw-r--r--archaeological_finds/migrations/0147_data_migration_statement_condition.json46
-rw-r--r--archaeological_finds/migrations/0147_data_migration_statement_condition.py21
-rw-r--r--archaeological_finds/models.py8
-rw-r--r--archaeological_finds/models_finds.py42
-rw-r--r--archaeological_finds/models_treatments.py329
-rw-r--r--ishtar_common/migrations/0272_ishtarsiteprofile_statementcondition.py42
-rw-r--r--ishtar_common/models.py19
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}",