#!/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.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 django.utils.translation 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 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 = _(u"Warehouse type") verbose_name_plural = _(u"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("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' ), } GEO_LABEL = "name" DOWN_MODEL_UPDATE = ["containers"] CACHED_LABELS = [] QA_LOCK = QuickAction( url="warehouse-qa-lock", icon_class="fa fa-lock", text=_(u"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(_(u"Name"), max_length=200) warehouse_type = models.ForeignKey(WarehouseType, verbose_name=_(u"Warehouse type")) person_in_charge = models.ForeignKey( Person, on_delete=models.SET_NULL, related_name='warehouse_in_charge', verbose_name=_(u"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(_(u"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=_(u"Documents"), blank=True) main_image = models.ForeignKey( Document, related_name='main_image_warehouses', on_delete=models.SET_NULL, verbose_name=_(u"Main image"), blank=True, null=True) external_id = models.TextField(_(u"External ID"), blank=True, null=True) auto_external_id = models.BooleanField( _(u"External ID is set automatically"), default=False) SUB_ADDRESSES = ["organization", "person_in_charge"] class Meta: verbose_name = _(u"Warehouse") verbose_name_plural = _(u"Warehouses") permissions = ( ("view_warehouse", u"Can view all Warehouses"), ("view_own_warehouse", u"Can view own Warehouse"), ("add_own_warehouse", u"Can add own Warehouse"), ("change_own_warehouse", u"Can change own Warehouse"), ("delete_own_warehouse", u"Can delete own Warehouse"), ) def __str__(self): return self.name def natural_key(self): return (self.uuid, ) def _get_base_image_path(self): return u"{}/{}".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.division.label for wd in WarehouseDivisionLink.objects.filter( warehouse=self).order_by('order').all() ] @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__responsible=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) q = ContainerLocalisation.objects.filter( division=current_division, ) for div, ref in current_path: q = q.filter( container__division__division=div, container__division__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 = list( WarehouseDivisionLink.objects.filter(warehouse=self ).order_by('order').all()) return self._get_divisions([], divisions) def _number_of_items_by_place(self, model, division_key='division'): res = {} paths = self.available_division_tuples[:] for path in paths: q = model.objects cpath = [] for division, ref in path: cpath.append(ref) attrs = { division_key + "__division": division, division_key + "__reference": ref } q = q.filter(**attrs) if tuple(cpath) not in res: res[tuple(cpath)] = q.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 idx 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='container__division') @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(Container) @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.owned_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 Collection(LightHistorizedItem): name = models.CharField(_(u"Name"), max_length=200, null=True, blank=True) description = models.TextField(_(u"Description"), null=True, blank=True) warehouse = models.ForeignKey(Warehouse, verbose_name=_(u"Warehouse"), related_name='collections') class Meta: verbose_name = _(u"Collection") verbose_name_plural = _(u"Collection") ordering = ('name',) def __str__(self): return self.name class WarehouseDivision(GeneralType): class Meta: verbose_name = _(u"Warehouse division type") verbose_name_plural = _(u"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, division): return self.get(warehouse__uuid=warehouse, division__txt_idx=division) class WarehouseDivisionLink(models.Model): RELATED_SET_NAME = "divisions" RELATED_ATTRS = ["order"] RELATIVE_MODELS = {Warehouse: 'warehouse'} warehouse = models.ForeignKey(Warehouse, related_name='divisions') division = models.ForeignKey(WarehouseDivision) order = models.IntegerField(_("Order"), default=10) objects = WarehouseDivisionLinkManager() class Meta: ordering = ('warehouse', 'order') unique_together = ('warehouse', 'division') def __str__(self): return u"{} - {}".format(self.warehouse, self.division) def natural_key(self): return self.warehouse.uuid, self.division.txt_idx class ContainerType(GeneralType): length = models.IntegerField(_(u"Length (mm)"), blank=True, null=True) width = models.IntegerField(_(u"Width (mm)"), blank=True, null=True) height = models.IntegerField(_(u"Height (mm)"), blank=True, null=True) volume = models.FloatField(_(u"Volume (l)"), blank=True, null=True) reference = models.CharField(_(u"Ref."), max_length=300, blank=True, null=True) class Meta: verbose_name = _(u"Container type") verbose_name_plural = _(u"Container types") ordering = ('label',) post_save.connect(post_save_cache, sender=ContainerType) post_delete.connect(post_save_cache, sender=ContainerType) class Container(DocumentItem, 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 = ['reference', 'container_type__label', 'cached_location', '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"), ] # search parameters EXTRA_REQUEST_KEYS = { 'location': 'location__pk', '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': _(u"Location - index"), 'cached_division': _(u"Precise localisation"), 'container_type__label': _(u"Type") } 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' ), 'responsible_name': SearchAltName( pgettext_lazy("key for text search", "responsible-warehouse"), 'responsible__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' ), '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'), '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", u"empty"), 'finds' ), 'no_finds': SearchAltName( pgettext_lazy("key for text search", u"no-associated-finds"), 'finds_ref' ), } REVERSED_MANY_COUNTED_FIELDS = ['finds', 'finds_ref'] ALT_NAMES.update(LightHistorizedItem.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=_(u"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=_(u"Location (warehouse)"), related_name='containers') responsible = models.ForeignKey( Warehouse, verbose_name=_(u"Responsible warehouse"), related_name='owned_containers') container_type = models.ForeignKey(ContainerType, verbose_name=_("Container type")) reference = models.TextField(_(u"Container ref.")) comment = models.TextField(_(u"Comment"), null=True, blank=True) cached_label = models.TextField(_(u"Localisation"), null=True, blank=True, db_index=True) cached_location = models.TextField(_(u"Cached location"), null=True, blank=True, db_index=True) cached_division = models.TextField(_(u"Cached division"), null=True, blank=True, db_index=True) index = models.IntegerField(u"Container ID", default=0) old_reference = models.TextField(_(u"Old reference"), blank=True, null=True) external_id = models.TextField(_(u"External ID"), blank=True, null=True) auto_external_id = models.BooleanField( _(u"External ID is set automatically"), default=False) documents = models.ManyToManyField( Document, related_name='containers', verbose_name=_(u"Documents"), blank=True) main_image = models.ForeignKey( Document, related_name='main_image_containers', on_delete=models.SET_NULL, verbose_name=_(u"Main image"), blank=True, null=True) class Meta: verbose_name = _(u"Container") verbose_name_plural = _(u"Containers") ordering = ('cached_label',) unique_together = ('index', 'responsible') permissions = ( ("view_container", u"Can view all Containers"), ("view_own_container", u"Can view own Container"), ("add_own_container", u"Can add own Container"), ("change_own_container", u"Can change own Container"), ("delete_own_container", u"Can delete own Container"), ) def __str__(self): return self.cached_label or "" def natural_key(self): return (self.uuid, ) def _generate_cached_label(self): items = [self.reference, self.precise_location] cached_label = u" | ".join(items) return cached_label def _generate_cached_location(self): items = [self.location.name, str(self.index)] cached_label = u" - ".join(items) return cached_label def _generate_cached_division(self): locas = [ u"{} {}".format(loca.division.division, loca.reference) for loca in ContainerLocalisation.objects.filter( container=self) ] return u" | ".join(locas) def _get_base_image_path(self): return self.responsible._get_base_image_path() + u"/" + self.external_id def merge(self, item, keep_old=False): 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) | \ Q(responsible__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 += u'-' + self.reference filename += u"-" + self.location.name filename += u"-" + str(self.index) if self.cached_division is None: self.skip_history_when_saving = True self.save() if self.cached_division: filename += u"-" + self.cached_division return slugify(filename) @property def precise_location(self): location = self.location.name if self.cached_division: location += u" " + self.cached_division return location def get_localisations(self): """ Get precise localisation of the container in the warehouse. :return: tuple of strings with localisations """ return tuple(( loca.reference for loca in ContainerLocalisation.objects.filter( container=self).order_by('division__order') )) def get_localisation(self, place): locas = self.get_localisations() if len(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): """ 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 :return: the container location object or None if the place does not exist """ q = WarehouseDivisionLink.objects.filter( warehouse=self.location).order_by('order') for idx, division_link in enumerate(q.all()): if idx == place: break else: return dct = {'container': self, 'division': division_link} if not value: if ContainerLocalisation.objects.filter(**dct).count(): c = ContainerLocalisation.objects.filter(**dct).all()[0] c.delete() return dct['defaults'] = {'reference': value} obj, created = ContainerLocalisation.objects.update_or_create(**dct) return obj @post_importer_action def set_localisation_1(self, context, value): return self.set_localisation(0, value) @post_importer_action def set_localisation_2(self, context, value): return self.set_localisation(1, value) @post_importer_action def set_localisation_3(self, context, value): return self.set_localisation(2, value) @post_importer_action def set_localisation_4(self, context, value): return self.set_localisation(3, value) @post_importer_action def set_localisation_5(self, context, value): return self.set_localisation(4, value) @post_importer_action def set_localisation_6(self, context, value): return self.set_localisation(5, value) @post_importer_action def set_localisation_7(self, context, value): return self.set_localisation(6, value) @post_importer_action def set_localisation_8(self, context, value): return self.set_localisation(7, value) @post_importer_action def set_localisation_9(self, context, value): return self.set_localisation(8, value) 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]), _(u"Add treatment"), "fa fa-flask", "", "", False), ] return actions def pre_save(self): if not self.index and self.responsible_id: 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() post_save.connect(cached_label_and_geo_changed, sender=Container) m2m_changed.connect(document_attached_changed, sender=Container.documents.through) class ContainerLocalisationManager(models.Manager): 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): container = models.ForeignKey(Container, verbose_name=_(u"Container"), related_name='division') division = models.ForeignKey(WarehouseDivisionLink, verbose_name=_(u"Division")) reference = models.CharField(_(u"Reference"), max_length=200, default='') objects = ContainerLocalisationManager() class Meta: verbose_name = _(u"Container localisation") verbose_name_plural = _(u"Container localisations") unique_together = ('container', 'division') ordering = ('container', 'division__order') def __str__(self): lbl = u" - ".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)