#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2012-2017 Étienne Loks # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # See the file COPYING for details. import datetime import uuid from django.conf import settings from django.contrib.gis.db import models from django.contrib.postgres.indexes import GinIndex from django.core.urlresolvers import reverse from django.db.models import Q, Max from django.db.models.signals import post_save, post_delete, m2m_changed from django.template.defaultfilters import slugify from ishtar_common.utils import ugettext_lazy as _, pgettext_lazy from ishtar_common.data_importer import post_importer_action from ishtar_common.model_managers import ExternalIdManager, UUIDModelManager from ishtar_common.models import Document, GeneralType, get_external_id, \ LightHistorizedItem, OwnPerms, Address, Person, post_save_cache, \ DashboardFormItem, ShortMenuItem, Organization, OrganizationType, \ document_attached_changed, SearchAltName, DynamicRequest, GeoItem, \ QRCodeItem, SearchVectorConfig, DocumentItem, QuickAction, MainItem, \ Merge from ishtar_common.model_merging import merge_model_objects from ishtar_common.utils import cached_label_changed, \ cached_label_and_geo_changed class WarehouseType(GeneralType): class Meta: verbose_name = _("Warehouse type") verbose_name_plural = _("Warehouse types") ordering = ('label',) post_save.connect(post_save_cache, sender=WarehouseType) post_delete.connect(post_save_cache, sender=WarehouseType) class Warehouse(Address, DocumentItem, GeoItem, QRCodeItem, DashboardFormItem, OwnPerms, MainItem): SLUG = 'warehouse' APP = "archaeological-warehouse" MODEL = "warehouse" SHOW_URL = 'show-warehouse' DELETE_URL = 'delete-warehouse' TABLE_COLS = ['name', 'warehouse_type__label'] NEW_QUERY_ENGINE = True BASE_SEARCH_VECTORS = [ SearchVectorConfig("name"), SearchVectorConfig("warehouse_type__label"), SearchVectorConfig("external_id"), SearchVectorConfig("town"), SearchVectorConfig("precise_town__name"), SearchVectorConfig("comment", "local"), ] EXTRA_REQUEST_KEYS = { "warehouse_type__label": "warehouse_type__label", # used by dynamic_table_documents "person_in_charge__pk": "person_in_charge__pk", } # alternative names of fields for searches ALT_NAMES = { 'name': SearchAltName( pgettext_lazy("key for text search", "name"), 'name__iexact' ), 'warehouse_type': SearchAltName( pgettext_lazy("key for text search", "type"), 'warehouse_type__label__iexact' ), 'town': SearchAltName( pgettext_lazy("key for text search", "town"), 'precise_town__cached_label__iexact' ), } GEO_LABEL = "name" DOWN_MODEL_UPDATE = ["containers"] CACHED_LABELS = [] QA_LOCK = QuickAction( url="warehouse-qa-lock", icon_class="fa fa-lock", text=_("Lock/Unlock"), target="many", rights=['change_warehouse', 'change_own_warehouse'] ) QUICK_ACTIONS = [QA_LOCK] objects = UUIDModelManager() uuid = models.UUIDField(default=uuid.uuid4) name = models.CharField(_("Name"), max_length=200) warehouse_type = models.ForeignKey(WarehouseType, verbose_name=_("Warehouse type")) person_in_charge = models.ForeignKey( Person, on_delete=models.SET_NULL, related_name='warehouse_in_charge', verbose_name=_("Person in charge"), null=True, blank=True) organization = models.ForeignKey( Organization, blank=True, null=True, related_name='warehouses', verbose_name=_("Organization"), on_delete=models.SET_NULL) comment = models.TextField(_("Comment"), null=True, blank=True) associated_divisions = models.ManyToManyField( 'WarehouseDivision', verbose_name=_("Divisions"), blank=True, through='WarehouseDivisionLink' ) documents = models.ManyToManyField( Document, related_name='warehouses', verbose_name=_("Documents"), blank=True) main_image = models.ForeignKey( Document, related_name='main_image_warehouses', on_delete=models.SET_NULL, verbose_name=_("Main image"), blank=True, null=True) external_id = models.TextField(_("External ID"), blank=True, null=True) auto_external_id = models.BooleanField( _("External ID is set automatically"), default=False) SUB_ADDRESSES = ["organization", "person_in_charge"] class Meta: verbose_name = _("Warehouse") verbose_name_plural = _("Warehouses") permissions = ( ("view_warehouse", "Can view all Warehouses"), ("view_own_warehouse", "Can view own Warehouse"), ("add_own_warehouse", "Can add own Warehouse"), ("change_own_warehouse", "Can change own Warehouse"), ("delete_own_warehouse", "Can delete own Warehouse"), ) indexes = [ GinIndex(fields=['data']), ] def __str__(self): return self.name def natural_key(self): return (self.uuid, ) def _get_base_image_path(self): return "{}/{}".format(self.SLUG, self.external_id) def create_attached_organization(self): """ Create an attached organization from warehouse fields """ dct_orga = {} for k in Address.FIELDS: dct_orga[k] = getattr(self, k) q = OrganizationType.objects.filter(txt_idx="warehouse") if q.count(): orga_type = q.all()[0] else: orga_type, __ = OrganizationType.objects.get_or_create( txt_idx="undefined", defaults={"label": _("Undefined")} ) dct_orga["organization_type"] = orga_type dct_orga["name"] = self.name orga = Organization.objects.create(**dct_orga) orga.save() # force duplicates self.organization = orga for k in Address.FIELDS: if k == "alt_address_is_prefered": setattr(self, k, False) else: setattr(self, k, None) self.save() @property def location_types(self): return [ wd.container_type.label for wd in WarehouseDivisionLink.objects.filter( warehouse=self).order_by('order').all() if wd.container_type ] @property def associated_filename(self): return datetime.date.today().strftime('%Y-%m-%d') + '-' + \ slugify(str(self)) @classmethod def get_query_owns(cls, ishtaruser): return cls._construct_query_own( '', cls._get_query_owns_dicts(ishtaruser)) @classmethod def _get_query_owns_dicts(cls, ishtaruser): return [{'person_in_charge__ishtaruser': ishtaruser}] @property def number_of_finds(self): from archaeological_finds.models import Find return Find.objects.filter(container_ref__location=self).count() @property def number_of_finds_hosted(self): from archaeological_finds.models import Find return Find.objects.filter(container__location=self).count() @property def number_of_containers(self): return Container.objects.filter(location=self).count() def _get_divisions(self, current_path, remaining_division, depth=0): if not remaining_division: return [current_path] current_division = remaining_division.pop(0) base_q = Container.objects.filter( container_type=current_division, location=self ) q = base_q for div, ref in current_path: q = base_q.filter( parent__container_type=div, parent__reference=ref) res = [] old_ref = None if not q.count(): return [current_path] for ref in q.values('reference').order_by('reference').all(): if ref['reference'] == old_ref: continue old_ref = ref['reference'] cpath = current_path[:] cpath.append((current_division, ref['reference'])) for r in self._get_divisions(cpath, remaining_division[:], depth + 1): res.append(r) return res @property def available_division_tuples(self): """ :return: ordered list of available paths. Each path is a list of tuple with the WarehouseDivisionLink and the reference. """ divisions = [ wd.container_type for wd in WarehouseDivisionLink.objects.filter( warehouse=self).order_by('order').all() ] return self._get_divisions([], divisions) def _number_of_items_by_place(self, model, division_key): res = {} paths = self.available_division_tuples[:] for path in paths: cpath = [] for division, ref in path: q = model.objects cpath.append(ref) attrs = { division_key + "container_type": division, division_key + "reference": ref } q = q.filter(**attrs) if tuple(cpath) not in res: res[tuple(cpath)] = q.distinct().count() res = [(k, res[k]) for k in res] final_res, current_res, depth = [], [], 1 len_divisions = WarehouseDivisionLink.objects.filter( warehouse=self).count() for path, nb in sorted(res, key=lambda x: (len(x[0]), x[0])): if depth != len(path): final_res.append(current_res[:]) current_res = [] depth = len(path) if path[-1] == '-': continue path = list(path) + ['' for __ in range(len_divisions - len(path))] current_res.append((path, nb)) final_res.append(current_res[:]) return final_res def _number_of_finds_by_place(self): from archaeological_finds.models import Find return self._number_of_items_by_place( Find, division_key='inside_container__container__') @property def number_of_finds_by_place(self, update=False): return self._get_or_set_stats('_number_of_finds_by_place', update) def _number_of_containers_by_place(self): return self._number_of_items_by_place( ContainerTree, 'container_parent__') @property def number_of_containers_by_place(self, update=False): return self._get_or_set_stats('_number_of_containers_by_place', update) def merge(self, item, keep_old=False): # do not recreate missing divisions available_divisions = [ wd.division for wd in WarehouseDivisionLink.objects.filter(warehouse=self) ] for container in list(item.containers.all()): container.location = self container.save() for loca in ContainerLocalisation.objects.filter( container=container).all(): if loca.division.division in available_divisions: div = WarehouseDivisionLink.objects.get( warehouse=self, division=loca.division.division ) ContainerLocalisation.objects.create( container=container, division=div, reference=loca.reference ) loca.delete() container.save() # force label regeneration for container in list(item.containers.all()): if Container.objects.filter(index=container.index, responsible=self).count(): container.index = Container.objects.filter( responsible=self).all().aggregate( Max("index"))["index__max"] + 1 container.responsible = self container.save() for wdiv in WarehouseDivisionLink.objects.filter(warehouse=item).all(): wdiv.delete() merge_model_objects(self, item, keep_old=keep_old) def save(self, *args, **kwargs): self.update_search_vector() super(Warehouse, self).save(*args, **kwargs) self.skip_history_when_saving = True if not self.external_id or self.auto_external_id: external_id = get_external_id('warehouse_external_id', self) if external_id != self.external_id: self.auto_external_id = True self.external_id = external_id self._cached_label_checked = False self.save() return m2m_changed.connect(document_attached_changed, sender=Warehouse.documents.through) post_save.connect(cached_label_and_geo_changed, sender=Warehouse) class WarehouseDivision(GeneralType): class Meta: verbose_name = _("Warehouse division type") verbose_name_plural = _("Warehouse division types") post_save.connect(post_save_cache, sender=WarehouseDivision) post_delete.connect(post_save_cache, sender=WarehouseDivision) class WarehouseDivisionLinkManager(models.Manager): def get_by_natural_key(self, warehouse, container_type): return self.get(warehouse__uuid=warehouse, container_type__txt_idx=container_type) class ContainerType(GeneralType): stationary = models.BooleanField( _("Stationary"), default=False, help_text=_("Container that usually will not be moved. Ex: building, " "room.")) length = models.IntegerField(_("Length (mm)"), blank=True, null=True) width = models.IntegerField(_("Width (mm)"), blank=True, null=True) height = models.IntegerField(_("Height (mm)"), blank=True, null=True) volume = models.FloatField(_("Volume (l)"), blank=True, null=True) reference = models.CharField(_("Ref."), max_length=300, blank=True, null=True) order = models.IntegerField(_("Order"), default=10) class Meta: verbose_name = _("Container type") verbose_name_plural = _("Container types") ordering = ('order', 'label',) post_save.connect(post_save_cache, sender=ContainerType) post_delete.connect(post_save_cache, sender=ContainerType) class WarehouseDivisionLink(models.Model): RELATED_SET_NAME = "divisions" RELATED_ATTRS = ["order", "container_type"] RELATIVE_MODELS = {Warehouse: 'warehouse'} warehouse = models.ForeignKey(Warehouse, related_name='divisions') container_type = models.ForeignKey(ContainerType, blank=True, null=True) division = models.ForeignKey( WarehouseDivision, help_text=_("Deprecated - do not use"), blank=True, null=True) order = models.IntegerField(_("Order"), default=10) objects = WarehouseDivisionLinkManager() class Meta: ordering = ('warehouse', 'order') unique_together = ('warehouse', 'division') def __str__(self): return "{} - {}".format(self.warehouse, self.container_type) def natural_key(self): return self.warehouse.uuid, self.container_type.txt_idx class ContainerTree(models.Model): CREATE_SQL = """ CREATE VIEW containers_tree AS WITH RECURSIVE rel_tree AS ( SELECT c.id AS container_id, c.parent_id AS container_parent_id, 1 AS level FROM archaeological_warehouse_container c WHERE c.parent_id is NOT NULL UNION ALL SELECT p.container_id AS container_id, c.parent_id as container_parent_id, p.level + 1 FROM archaeological_warehouse_container c, rel_tree p WHERE c.id = p.container_parent_id AND c.parent_id is NOT NULL AND p.level < 10 -- prevent recursive... ) SELECT DISTINCT container_id, container_parent_id, level FROM rel_tree; CREATE VIEW container_tree AS SELECT DISTINCT y.container_id, y.container_parent_id FROM (SELECT * FROM containers_tree) y ORDER BY y.container_id, y.container_parent_id; -- deactivate deletion CREATE RULE container_tree_del AS ON DELETE TO container_tree DO INSTEAD DELETE FROM archaeological_warehouse_container where id=NULL; CREATE RULE containers_tree_del AS ON DELETE TO container_tree DO INSTEAD DELETE FROM archaeological_warehouse_container where id=NULL; """ DELETE_SQL = """ DROP VIEW IF EXISTS container_tree; DROP VIEW IF EXISTS containers_tree; """ container = models.OneToOneField( "archaeological_warehouse.Container", verbose_name=_("Container"), related_name="container_tree_child", primary_key=True) container_parent = models.ForeignKey( "archaeological_warehouse.Container", verbose_name=_("Container parent"), related_name="container_tree_parent") class Meta: managed = False db_table = 'containers_tree' class Container(DocumentItem, Merge, LightHistorizedItem, QRCodeItem, GeoItem, OwnPerms, MainItem): SLUG = 'container' APP = "archaeological-warehouse" MODEL = "container" SHOW_URL = 'show-container' DELETE_URL = 'delete-container' NEW_QUERY_ENGINE = True TABLE_COLS = ['container_type__label', 'reference', 'location__name', 'cached_division', 'old_reference'] IMAGE_PREFIX = 'containers/' BASE_SEARCH_VECTORS = [ SearchVectorConfig("reference"), SearchVectorConfig("container_type__label"), SearchVectorConfig("cached_location"), SearchVectorConfig("old_reference"), SearchVectorConfig("comment", "local"), ] M2M_SEARCH_VECTORS = [ SearchVectorConfig("division__reference"), SearchVectorConfig("division__division__division__label"), ] PARENT_SEARCH_VECTORS = ["parent"] # search parameters EXTRA_REQUEST_KEYS = { 'location': 'location__pk', 'location__name': "location__name", 'location_id': 'location__pk', 'responsible_id': 'responsible__pk', 'container_type': 'container_type__pk', 'reference': 'reference__icontains', 'old_reference': 'old_reference__icontains', 'finds__base_finds__context_record__operation': 'finds__base_finds__context_record__operation', 'finds__base_finds__context_record': 'finds__base_finds__context_record', 'finds': 'finds', 'container_type__label': 'container_type__label', } COL_LABELS = { 'cached_location': _("Location - index"), 'cached_division': _("Precise localisation"), 'container_type__label': _("Type"), "location__name": _("Warehouse") } GEO_LABEL = "cached_label" CACHED_LABELS = ['cached_division', 'cached_label', 'cached_location', ] # alternative names of fields for searches ALT_NAMES = { 'location_name': SearchAltName( pgettext_lazy("key for text search", "location"), 'location__name__iexact' ), 'container_type': SearchAltName( pgettext_lazy("key for text search", "type"), 'container_type__label__iexact' ), 'reference': SearchAltName( pgettext_lazy("key for text search", "reference"), 'reference__iexact' ), 'old_reference': SearchAltName( pgettext_lazy("key for text search", "old-reference"), 'old_reference__iexact' ), 'comment': SearchAltName( pgettext_lazy("key for text search", "comment"), 'comment__iexact' ), 'operation_town': SearchAltName( pgettext_lazy("key for text search", "operation-town"), 'finds__base_finds__context_record__operation__' 'towns__cached_label__iexact' ), 'operation_scientist': SearchAltName( pgettext_lazy("key for text search", "operation-scientist"), 'finds__base_finds__context_record__operation__' 'scientist__cached_label__iexact' ), 'code_patriarche': SearchAltName( pgettext_lazy("key for text search", "code-patriarche"), 'finds__base_finds__context_record__operation__' 'code_patriarche__iexact' ), 'archaeological_sites': SearchAltName( pgettext_lazy("key for text search", "site"), 'finds__base_finds__context_record__operation__' 'archaeological_sites__cached_label__icontains'), 'archaeological_sites_name': SearchAltName( pgettext_lazy("key for text search", "site-name"), 'finds__base_finds__context_record__operation__' 'archaeological_sites__name__iexact'), 'archaeological_sites_context_record': SearchAltName( pgettext_lazy("key for text search", "context-record-site"), 'finds__base_finds__context_record__archaeological_site__' 'cached_label__icontains'), 'archaeological_sites_context_record_name': SearchAltName( pgettext_lazy("key for text search", "context-record-site-name"), 'finds__base_finds__context_record__archaeological_site__' 'name__iexact'), 'context_record': SearchAltName( pgettext_lazy("key for text search", "context-record"), 'finds__base_finds__context_record__cached_label__icontains'), 'find_label': SearchAltName( pgettext_lazy("key for text search", "find-label"), 'finds__label__icontains'), 'find_denomination': SearchAltName( pgettext_lazy("key for text search", "find-denomination"), 'finds__denomination__icontains'), 'material_types': SearchAltName( pgettext_lazy("key for text search", "material"), 'finds__material_types__label__iexact'), 'object_types': SearchAltName( pgettext_lazy("key for text search", "object-type"), 'finds__object_types__label__iexact'), 'preservation_to_considers': SearchAltName( pgettext_lazy("key for text search", "preservation"), 'finds__preservation_to_considers__label__iexact'), 'conservatory_state': SearchAltName( pgettext_lazy("key for text search", "conservatory"), 'finds__conservatory_state__label__iexact'), 'integrities': SearchAltName( pgettext_lazy("key for text search", "integrity"), 'finds__integrities__label__iexact'), 'remarkabilities': SearchAltName( pgettext_lazy("key for text search", "remarkability"), 'finds__remarkabilities__label__iexact'), 'alterations': SearchAltName( pgettext_lazy("key for text search", "alterations"), 'finds__alterations__label__iexact'), 'alteration_causes': SearchAltName( pgettext_lazy("key for text search", "alteration-causes"), 'finds__alteration_causes__label__iexact'), 'treatment_emergency': SearchAltName( pgettext_lazy("key for text search", "treatment-emergency"), 'finds__treatment_emergency__label__iexact'), 'description': SearchAltName( pgettext_lazy("key for text search", "find-description"), 'finds__description__iexact'), 'empty': SearchAltName( pgettext_lazy("key for text search", "empty"), 'finds' ), 'contain_containers': SearchAltName( pgettext_lazy("key for text search", "contain-containers"), 'children__isnull' ), 'is_stationary': SearchAltName( pgettext_lazy("key for text search", "is-stationary"), 'container_type__stationary' ) } REVERSED_BOOL_FIELDS = [ 'children__isnull', 'documents__image__isnull', 'documents__associated_file__isnull', 'documents__associated_url__isnull', ] BOOL_FIELDS = ['container_type__stationary'] REVERSED_MANY_COUNTED_FIELDS = ['finds', 'finds_ref'] ALT_NAMES.update(LightHistorizedItem.ALT_NAMES) ALT_NAMES.update(DocumentItem.ALT_NAMES) DYNAMIC_REQUESTS = { 'division': DynamicRequest( label=_("Division -"), app_name='archaeological_warehouse', model_name='WarehouseDivision', form_key='division', search_key=pgettext_lazy("key for text search", 'division'), type_query='division__division__division__txt_idx', search_query='division__reference__iexact' ), } QA_LOCK = QuickAction( url="container-qa-lock", icon_class="fa fa-lock", text=_("Lock/Unlock"), target="many", rights=['change_container', 'change_own_container'] ) QUICK_ACTIONS = [QA_LOCK] objects = UUIDModelManager() # fields uuid = models.UUIDField(default=uuid.uuid4) location = models.ForeignKey( Warehouse, verbose_name=_("Location (warehouse)"), related_name='containers') responsible = models.ForeignKey( Warehouse, verbose_name=_("Responsible warehouse"), related_name='owned_containers', blank=True, null=True, help_text=_("Deprecated - do not use") ) container_type = models.ForeignKey(ContainerType, verbose_name=_("Container type")) reference = models.TextField(_("Container ref.")) comment = models.TextField(_("Comment"), null=True, blank=True) cached_label = models.TextField(_("Localisation"), null=True, blank=True, db_index=True) cached_location = models.TextField(_("Cached location"), null=True, blank=True, db_index=True) cached_division = models.TextField(_("Cached division"), null=True, blank=True, db_index=True) parent = models.ForeignKey("Container", verbose_name=_("Parent container"), on_delete=models.SET_NULL, related_name="children", blank=True, null=True) index = models.IntegerField(_("Container ID"), default=0) old_reference = models.TextField(_("Old reference"), blank=True, null=True) external_id = models.TextField(_("External ID"), blank=True, null=True) auto_external_id = models.BooleanField( _("External ID is set automatically"), default=False) documents = models.ManyToManyField( Document, related_name='containers', verbose_name=_("Documents"), blank=True) main_image = models.ForeignKey( Document, related_name='main_image_containers', on_delete=models.SET_NULL, verbose_name=_("Main image"), blank=True, null=True) DISABLE_POLYGONS = False MERGE_ATTRIBUTE = "get_cached_division" class Meta: verbose_name = _("Container") verbose_name_plural = _("Containers") ordering = ('cached_label',) unique_together = [('index', 'responsible'), ('location', 'container_type', 'parent', 'reference')] permissions = ( ("view_container", "Can view all Containers"), ("view_own_container", "Can view own Container"), ("add_own_container", "Can add own Container"), ("change_own_container", "Can change own Container"), ("delete_own_container", "Can delete own Container"), ) indexes = [ GinIndex(fields=['data']), ] def __str__(self): return self.cached_label or "" @property def name(self): return "{} - {}".format(self.container_type.name, self.reference) @property def short_label(self): return "{} {}".format(self.container_type.label, self.reference) @property def parent_external_id(self): if not self.parent: return self.location.external_id return self.parent.external_id def natural_key(self): return (self.uuid, ) def _generate_cached_label(self): return self.precise_location def _generate_cached_location(self): items = [self.location.name, str(self.index)] cached_label = " - ".join(items) return cached_label @property def get_cached_division(self): return self._generate_cached_division() def _generate_cached_division(self): parents = [] parent = self.parent c_ids = [] while parent: if parent.id in c_ids: # prevent cyclic break c_ids.append(parent.id) parents.append(parent) parent = parent.parent locas = [ "{} {}".format(loca.container_type.name, loca.reference) for loca in reversed(parents) ] locas.append("{} {}".format(self.container_type.name, self.reference)) return " | ".join(locas) def _get_base_image_path(self): return self.location._get_base_image_path() + "/" + self.external_id def merge(self, item, keep_old=False): # TODO: change localisation management locas = [ cl.division.division.txt_idx for cl in ContainerLocalisation.objects.filter(container=self).all() ] for loca in ContainerLocalisation.objects.filter(container=item).all(): if loca.division.division.txt_idx not in locas: loca.container = self loca.save() else: loca.delete() super(Container, self).merge(item, keep_old=keep_old) @classmethod def get_query_owns(cls, ishtaruser): return Q(history_creator=ishtaruser.user_ptr) | \ Q(location__person_in_charge__ishtaruser=ishtaruser) def get_precise_points(self): precise_points = super(Container, self).get_precise_points() if precise_points: return precise_points return self.location.get_precise_points() def get_town_centroid(self): return self.location.get_town_centroid() def get_town_polygons(self): return self.location.get_town_polygons() @property def associated_filename(self): filename = datetime.date.today().strftime('%Y-%m-%d') filename += "-" + self.reference filename += "-" + self.location.name filename += "-" + str(self.index) if self.cached_division is None: self.skip_history_when_saving = True self.save() if self.cached_division: filename += "-" + self.cached_division return slugify(filename) @property def precise_location(self): location = self.location.name if self.cached_division: location += " " + self.cached_division return location def get_localisations(self): """ Get precise localisation of the container in the warehouse. :return: tuple of strings with localisations """ localisations = [] parent = self.parent while parent: localisations.append(parent) parent = parent.parent return reversed(localisations) def get_localisation(self, place): locas = self.get_localisations() if len(list(locas)) < (place + 1): return "" return locas[place] @property def localisation_1(self): return self.get_localisation(0) @property def localisation_2(self): return self.get_localisation(1) @property def localisation_3(self): return self.get_localisation(2) @property def localisation_4(self): return self.get_localisation(3) @property def localisation_5(self): return self.get_localisation(4) @property def localisation_6(self): return self.get_localisation(5) @property def localisation_7(self): return self.get_localisation(6) @property def localisation_8(self): return self.get_localisation(7) @property def localisation_9(self): return self.get_localisation(8) def set_localisation(self, place, value, static=False, return_errors=False): """ Set the reference for the localisation number "place" (starting from 0) :param place: the number of the localisation :param value: the reference to be set :param static: do not create new containers :param return_errors: return error message :return: the container location object or None if the place does not exist - return also error message if return_errors set to True """ value = value.strip() if not value or value == "-": if return_errors: return None, _("No value") return q = WarehouseDivisionLink.objects.filter( warehouse=self.location).order_by('order') current_container_type = None error_msg = str( _("The division number {} have not been set for the warehouse {}.") ).format(place + 1, self.location) previous_container_types = [] for idx, division_link in enumerate(q.all()): if idx == place: current_container_type = division_link.container_type break previous_container_types.append(division_link.container_type_id) else: if return_errors: return None, error_msg return if not current_container_type: # no division link set at this place if return_errors: return None, error_msg return # modify existing current_localisations = self.get_localisations() current_localisation, current_parent = None, None for loca in current_localisations: if loca.container_type == current_container_type: if loca.reference == value: current_localisation = loca break elif loca.container_type_id in previous_container_types: current_parent = loca if not current_localisation: dct = { "reference": value, "container_type": current_container_type, "parent": current_parent, "location": self.location } q = Container.objects.filter(**dct) if q.count(): current_localisation = q.all()[0] else: if static: if return_errors: error_msg = str( _("The division {} {} do not exist for the " "location {}.") ).format(current_container_type, value, self.location) return None, error_msg return current_localisation = Container.objects.create(**dct) self.parent = current_localisation self.save() if return_errors: return current_localisation, None return current_localisation def set_static_localisation(self, place, value): return self.set_localisation(place, value, static=True) @post_importer_action def set_localisation_1(self, context, value): return self.set_localisation(0, value) set_localisation_1.post_save = True @post_importer_action def set_localisation_2(self, context, value): return self.set_localisation(1, value) set_localisation_2.post_save = True @post_importer_action def set_localisation_3(self, context, value): return self.set_localisation(2, value) set_localisation_3.post_save = True @post_importer_action def set_localisation_4(self, context, value): return self.set_localisation(3, value) set_localisation_4.post_save = True @post_importer_action def set_localisation_5(self, context, value): return self.set_localisation(4, value) set_localisation_5.post_save = True @post_importer_action def set_localisation_6(self, context, value): return self.set_localisation(5, value) set_localisation_6.post_save = True @post_importer_action def set_localisation_7(self, context, value): return self.set_localisation(6, value) set_localisation_7.post_save = True @post_importer_action def set_localisation_8(self, context, value): return self.set_localisation(7, value) set_localisation_8.post_save = True @post_importer_action def set_localisation_9(self, context, value): return self.set_localisation(8, value) set_localisation_9.post_save = True @post_importer_action def set_static_localisation_1(self, context, value): return self.set_static_localisation(0, value) set_static_localisation_1.post_save = True @post_importer_action def set_static_localisation_2(self, context, value): return self.set_static_localisation(1, value) set_static_localisation_2.post_save = True @post_importer_action def set_static_localisation_3(self, context, value): return self.set_static_localisation(2, value) set_static_localisation_3.post_save = True @post_importer_action def set_static_localisation_4(self, context, value): return self.set_static_localisation(3, value) set_static_localisation_4.post_save = True @post_importer_action def set_static_localisation_5(self, context, value): return self.set_static_localisation(4, value) set_static_localisation_5.post_save = True @post_importer_action def set_static_localisation_6(self, context, value): return self.set_static_localisation(5, value) set_static_localisation_6.post_save = True @post_importer_action def set_static_localisation_7(self, context, value): return self.set_static_localisation(6, value) set_static_localisation_7.post_save = True @post_importer_action def set_static_localisation_8(self, context, value): return self.set_static_localisation(7, value) set_static_localisation_8.post_save = True @post_importer_action def set_static_localisation_9(self, context, value): return self.set_static_localisation(8, value) set_static_localisation_9.post_save = True def get_extra_actions(self, request): """ extra actions for the sheet template """ # url, base_text, icon, extra_text, extra css class, is a quick action actions = super(Container, self).get_extra_actions(request) can_edit_find = self.can_do(request, 'change_find') if can_edit_find: actions += [ (reverse('container-add-treatment', args=[self.pk]), _("Add treatment"), "fa fa-flask", "", "", False), ] return actions def pre_save(self): if self.index: return if settings.ISHTAR_CONTAINER_INDEX == "general": q = Container.objects if q.count(): self.index = int( q.aggregate(Max("index"))["index__max"] or 0) + 1 else: self.index = 1 elif self.responsible_id: # default is index by warehouse q = Container.objects.filter(responsible=self.responsible) if q.count(): self.index = int(q.aggregate(Max("index"))["index__max"]) + 1 else: self.index = 1 def save(self, *args, **kwargs): self.pre_save() super(Container, self).save(*args, **kwargs) updated = False if not self.index: self.skip_history_when_saving = True q = Container.objects.filter(responsible=self.responsible).exclude( pk=self.pk).order_by('-index') if q.count(): self.index = q.all()[0].index + 1 else: self.index = 1 updated = True self.skip_history_when_saving = True if not self.external_id or self.auto_external_id: external_id = get_external_id('container_external_id', self) if external_id != self.external_id: updated = True self.auto_external_id = True self.external_id = external_id if updated: self._cached_label_checked = False self.save() return # remove old location in warehouse q = ContainerLocalisation.objects.filter(container=self).exclude( division__warehouse=self.location) for loca in q.all(): loca.delete() def container_post_save(sender, **kwargs): cached_label_and_geo_changed(sender=sender, **kwargs) if not kwargs.get('instance'): return instance = kwargs.get('instance') #TODO: to be deleted??? for loca in ContainerLocalisation.objects.filter( container=instance).exclude( division__warehouse=instance.location).all(): q = WarehouseDivisionLink.objects.filter( warehouse=instance.location, division=loca.division.division ) if not q.count(): continue loca.division = q.all()[0] loca.save() post_save.connect(container_post_save, sender=Container) m2m_changed.connect(document_attached_changed, sender=Container.documents.through) class ContainerLocalisationManager(models.Manager): #TODO: to be deleted.... def get_by_natural_key(self, container, warehouse, division): return self.get(container__uuid=container, division__warehouse__uuid=warehouse, division__division__txt_idx=division) class ContainerLocalisation(models.Model): #TODO: to be deleted.... container = models.ForeignKey(Container, verbose_name=_("Container"), related_name='division') division = models.ForeignKey(WarehouseDivisionLink, verbose_name=_("Division")) reference = models.CharField(_("Reference"), max_length=200, default='') objects = ContainerLocalisationManager() class Meta: verbose_name = _("Container localisation") verbose_name_plural = _("Container localisations") unique_together = ('container', 'division') ordering = ('container', 'division__order') def __str__(self): lbl = " - ".join((str(self.container), str(self.division), self.reference)) return lbl def natural_key(self): return self.container.uuid, self.division.warehouse.uuid,\ self.division.division.txt_idx def save(self, *args, **kwargs): super(ContainerLocalisation, self).save(*args, **kwargs) self.container.skip_history_when_saving = True cached_label_changed(Container, instance=self.container, force_update=True)