class VersionedDocument: versioned_document_uuid = UUID() versioned_document_version = String() versioned_document = Function(fget='get_versioned_document', fset='set_versioned_document', fdel='del_versioned_document') def get_versioned_document(self): Document = self.registry.Attachment.Document query = Document.query() query = query.filter( Document.uuid == self.versioned_document_uuid, Document.version == self.versioned_document_version) return query.one_or_none() def set_versioned_document(self, document): if document.uuid is None: raise NoneValueException("Uuid value is None") self.versioned_document_uuid = document.uuid self.versioned_document_version = document.version def del_versioned_document(self): self.versioned_document_uuid = None self.versioned_document_version = None @hybrid_method def is_versioned_document(self, document): return (self.versioned_document_uuid == document.uuid and self.versioned_document_version == document.version)
class Role: """Role, allow to group some authorization for an user""" name = String(primary_key=True, nullable=False) label = String(nullable=False) children = Many2Many(model='Model.Pyramid.Role', many2many="parents") users = Many2Many(model=Pyramid.User, many2many="roles") roles_name = Function(fget="get_all_roles_name") def get_all_roles_name(self): """Return all the name of the roles self and dependencies """ names = [self.name] for child in self.children: if child.name in names: continue # pragma: no cover names.extend(child.roles_name) return list(set(names)) @classmethod def before_update_orm_event(cls, mapper, connection, target): """Check if the role has not any cyclical dependencies """ try: target.get_all_roles_name() except RecursionError: raise RecursionRoleError()
class LatestDocument: latest_document_uuid = UUID() latest_document = Function(fget='get_latest_document', fset='set_latest_document', fdel='del_latest_document') def get_latest_document(self): Document = self.registry.Attachment.Document.Latest query = Document.query() query = query.filter(Document.uuid == self.latest_document_uuid) return query.one_or_none() def set_latest_document(self, document): if document.uuid is None: raise NoneValueException("Uuid value is None") if document.type != 'latest': raise NotLatestException( "You try to set a versioned document, this action is " "forbidden") self.latest_document_uuid = document.uuid def del_latest_document(self): self.latest_document_uuid = None @hybrid_method def is_latest_document(self, document): if document.type != 'latest': raise NotLatestException( "You try to compare the latest document with a versioned " "document, this action is forbidden") return self.latest_document_uuid == document.uuid
class Reservation: physobj = Many2One(model=Wms.PhysObj, primary_key=True, index=True) quantity = Integer() """The quantity that this Reservation provides. If the PhysObj in the application have ``quantity`` field (see :ref:`improvement_no_quantities`), this is not necessarily its value within :attr:`goods`. Instead, it is the quantity within the :attr:`request_item` that the current Reservation provides. Use-case some PhysObj being sold either as packs of 10 or by the unit. If one wants to reserve 13 of them, it should be expressable as one pack of 10 and 3 units. Then maybe (depending on the needs), would it be actually smarter of the application to not issue an Unpack. """ request_item = Many2One(model=Wms.Reservation.RequestItem, index=True) goods = Function(fget='_goods_get', fset='_goods_set', fexpr='_goods_expr') """Compatibility wrapper. Before the merge of Goods and Locations as PhysObj, :attr:`physobj` was ``goods``. This does not extend to compatibility of the former low level ``goods_id`` column. """ def _goods_get(self): deprecation_warn_goods() return self.physobj def _goods_set(self, value): deprecation_warn_goods() self.physobj = value @classmethod def _goods_expr(cls): deprecation_warn_goods() return cls.physobj @classmethod def define_table_args(cls): return super(Reservation, cls).define_table_args() + (CheckConstraint( 'quantity > 0', name='positive_qty'), ) def is_transaction_owner(self): """Check that the current transaction is the owner of the reservation. """ return self.request_item.request.is_txn_reservations_owner() def is_transaction_allowed(self, opcls, state, dt_execution, inputs=None, **kwargs): """TODO add allowances, like a Move not far.""" return self.is_transaction_owner()
class Test: id = Integer(primary_key=True) val1 = Integer() val2 = Function(fget='fget', fset='fset') def fget(self): return 2 * self.val1
class User: """User declaration for identity""" first_name = String(nullable=False) last_name = String(nullable=False) name = Function(fget='get_name') def get_name(self): """Return the name of the user""" return self.first_name + ' ' + self.last_name.upper()
class Employee: full_name = Function(fget='get_full_name') def get_full_name(self): sub_name = '' if self.type_entity == 'manager': sub_name = self.manager_name elif self.type_entity == 'engineer': sub_name = self.engineer_name return self.name + ' - ' + sub_name
class Test: id = Integer(primary_key=True) first_name = String() last_name = String() name = Function(fget='fget', fset='fset', fdel='fdel', fexpr='fexpr') def fget(self): return '{0} {1}'.format(self.first_name, self.last_name) def fset(self, value): self.first_name, self.last_name = value.split(' ', 1) def fdel(self): self.first_name = self.last_name = None @classmethod def fexpr(cls): return func.concat(cls.first_name, ' ', cls.last_name)
class User: active = Function(fget='fget_active', fexpr='fexpr_active') lang = Selection(selections='get_languages') @classmethod def get_languages(cls): return { 'en': 'English', 'fr': 'French', } def fget_active(self): credential = self.anyblok.Pyramid.CredentialStore.query().filter_by( login=self.login).one_or_none() return True if credential else False @classmethod def fexpr_active(cls): return cls.anyblok.Pyramid.CredentialStore.login == cls.login
class Test: id = Integer(primary_key=True) _name = String() name = Function(fget='fget', fset='fset', fdel='fdel', fexpr='fexpr') def fget(self): return self._name def fset(self, value): self._name = value def fdel(self): self._name = None @classmethod def fexpr(cls): return cls._name
class Manager: full_name = Function(fget='get_full_name') def get_full_name(self): return self.name + ' - ' + self.manager_name
class User: """User declaration need for Auth""" login = String(primary_key=True, nullable=False) first_name = String(nullable=False) last_name = String(nullable=False) name = Function(fget='get_name') def get_name(self): """Return the name of the user""" return self.first_name + ' ' + self.last_name.upper() @classmethod def get_roles(cls, login): """Return the roles of an user :param login: str, login attribute of the user :rtype: list of str (name of the roles) """ # cache the method roles = [login] user = cls.query().filter(cls.login == login).one_or_none() if user: for role in user.roles: roles.extend(role.roles_name) return list(set(roles)) @classmethod def get_acl(cls, login, resource, params=None): """Retun the ACL for a ressource and a user Auth, does not implement any rule to compute ACL, This method allow all user to use the resource ask by controllers. For other configuration, this method must be overwrite :param login: str, login attribute of the user :param resource: str, name of a resource :param params: all options need to compute ACL """ # cache the method return [(Allow, login, ALL_PERMISSIONS)] @classmethod def format_login_params(cls, request): """Return the login and password from query By default the query come from json_body and are named **login** and **password** If the entries come from another place, this method must be overwrite :param request: the request from the controllers """ return request.json_body @classmethod def check_login(cls, login=None, password=None, **kwargs): """Check login / password This method raise an exception, because any credential is stored in this bloks .. warning:: This method must be overwriting by anycredential blok :param login: str, the login attribute of the user :param password: str :param kwargs: any options need to validate credential """ raise HTTPUnauthorized() @classmethod def get_login_location_to(cls, login, request): """Return the default path after the login""" return '/' @classmethod def get_logout_location_to(cls, request): """Return the default path after the logout""" return '/'
class Document: DOCUMENT_TYPE = None uuid = UUID(primary_key=True, binary=False, nullable=False) version_number = Integer(primary_key=True, nullable=False) version = Function(fget="get_version") created_at = DateTime(default=datetime.now, nullable=False) historied_at = DateTime() data = Json(default=dict) file_added_at = DateTime() filename = String(size=256) contenttype = String() filesize = Integer() file = LargeBinary() hash = String(size=256) type = Selection(selections={ 'latest': 'Latest', 'history': 'History' }, nullable=False) previous_doc_uuid = UUID() previous_doc_version_number = Integer() previous_version = Function(fget="get_previous_version") next_version = Function(fget="get_next_version") previous_versions = Function(fget="get_previous_versions") @classmethod def get_file_fields(cls): return [ 'file', 'file_added_at', 'contenttype', 'filename', 'hash', 'filesize' ] def get_file(self): return self.to_dict(*self.get_file_fields()) def set_file(self, file_): self.file = file_ def get_version(self): return "V-%06d" % self.version_number def get_previous_version(self): Doc = self.anyblok.Attachment.Document query = Doc.query() query = query.filter(Doc.uuid == self.previous_doc_uuid) query = query.filter( Doc.version_number == self.previous_doc_version_number) return query.one_or_none() def get_next_version(self): Doc = self.anyblok.Attachment.Document query = Doc.query() query = query.filter(Doc.previous_doc_uuid == self.uuid) query = query.filter( Doc.previous_doc_version_number == self.version_number) return query.one_or_none() def get_previous_versions(self): res = [] current = self while current.previous_version: current = current.previous_version res.append(current) return res @classmethod def define_mapper_args(cls): mapper_args = super(Document, cls).define_mapper_args() if cls.__registry_name__ == 'Model.Attachment.Document': mapper_args.update({'polymorphic_on': cls.type}) mapper_args.update({'polymorphic_identity': cls.DOCUMENT_TYPE}) return mapper_args @classmethod def insert(cls, *args, **kwargs): if cls.__registry_name__ == 'Model.Attachment.Document': return cls.anyblok.Attachment.Document.Latest.insert( *args, **kwargs) return super(Document, cls).insert(*args, **kwargs) @classmethod def query(cls, *args, **kwargs): query = super(Document, cls).query(*args, **kwargs) if cls.__registry_name__ != 'Model.Attachment.Document': query = query.filter(cls.type == cls.DOCUMENT_TYPE) return query def has_file(self): if self.file: return True return False @classmethod def filter_has_not_file(cls): return cls.file == None # noqa
class Avatar: """PhysObj Avatar. See in :ref:`Core Concepts <physobj_avatar>` for a functional description. """ id = Integer(label="Identifier", primary_key=True) """Primary key.""" obj = Many2One(model=Model.Wms.PhysObj, index=True, nullable=False) """The PhysObj of which this is an Avatar.""" state = Selection(label="State of existence", selections=AVATAR_STATES, nullable=False, index=True) """State of existence in the premises. see :mod:`anyblok_wms_base.constants`. This may become an ENUM once Anyblok supports them. """ location = Many2One(model=Model.Wms.PhysObj, nullable=False, index=True) """Where the PhysObj are/will be/were. See :class:`Location <anyblok_wms_base.core.location.Location>` for a discussion of what this should actually mean. """ dt_from = DateTime(label="Exist (or will) from this date & time", nullable=False) """Date and time from which the Avatar is meaningful, inclusively. Functionally, even though the default in creating Operations will be to use the current date and time, this is not to be confused with the time of creation in the database, which we don't care much about. The actual meaning really depends on the value of the :attr:`state` field: + In the ``past`` and ``present`` states, this is supposed to be a faithful representation of reality. + In the ``future`` state, this is completely theoretical, and ``wms-core`` doesn't do much about it, besides using it to avoid counting several :ref:`physobj_avatar` of the same physical goods while :meth:`peeking at quantities in the future <anyblok_wms_base.core.location.Location.quantity>`. If the end application does serious time prediction, it can use it freely. In all cases, this doesn't mean that the very same PhysObj aren't present at an earlier time with the same state, location, etc. That earlier time range would simply be another Avatar (use case: moving back and forth). """ dt_until = DateTime(label="Exist (or will) until this date & time") """Date and time until which the Avatar record is meaningful, exclusively. Like :attr:`dt_from`, the meaning varies according to the value of :attr:`state`: + In the ``past`` state, this is supposed to be a faithful representation of reality: apart from the special case of formal :ref:`Splits and Aggregates <op_split_aggregate>`, the goods really left this location at these date and time. + In the ``present`` and ``future`` states, this is purely theoretical, and the same remarks as for the :attr:`dt_from` field apply readily. In all cases, this doesn't mean that the very same goods aren't present at an later time with the same state, location, etc. That later time range would simply be another Avatar (use case: moving back and forth). """ outcome_of = Many2One(index=True, model=Model.Wms.Operation, nullable=False) """The Operation that created this Avatar. """ goods = Function(fget='_goods_get', fset='_goods_set', fexpr='_goods_expr') """Compatibility wrapper. Before the merge of Goods and Locations as PhysObj, :attr:`obj` was ``goods``. This does not extend to compatibility of the former low level ``goods_id`` column. """ @classmethod def define_table_args(cls): return super().define_table_args() + ( CheckConstraint('dt_until IS NULL OR dt_until >= dt_from', name='dt_range_valid'), Index("idx_avatar_present_unique", cls.obj_id, unique=True, postgresql_where=(cls.state == 'present')) ) def _goods_get(self): deprecation_warn_goods() return self.obj def _goods_set(self, value): deprecation_warn_goods() self.obj = value @classmethod def _goods_expr(cls): deprecation_warn_goods() return cls.obj def __str__(self): return ("(id={self.id}, obj={self.obj}, state={self.state!r}, " "location={self.location}, " "dt_range=[{self.dt_from}, " "{self.dt_until}])".format(self=self)) def __repr__(self): return ("Wms.PhysObj.Avatar(id={self.id}, " "obj={self.obj!r}, state={self.state!r}, " "location={self.location!r}, " "dt_range=[{self.dt_from!r}, {self.dt_until!r}])").format( self=self) def get_property(self, k, default=None): return self.obj.get_property(k, default=default)
class Shipment(Mixin.UuidColumn, Mixin.TrackModel): """ Shipment """ statuses = dict(new="New", label="Label", transit="Transit", delivered="Delivered", exception="Exception", error="Error") service = Many2One(label="Shipping service", model=Declarations.Model.Delivery.Carrier.Service, one2many='shipments', nullable=False) sender_address = Many2One(label="Sender address", model=Declarations.Model.Address, column_names=["sender_address_uuid"], nullable=False) recipient_address = Many2One(label="Recipient address", model=Declarations.Model.Address, column_names=["recipient_address_uuid"], nullable=False) reason = String(label="Reason reference") pack = String(label="Pack reference") status = Selection(label="Shipping status", selections=statuses, default='new', nullable=False) properties = Jsonb(label="Properties") document_uuid = UUID(label="Carrier slip document reference") document = Function(fget='get_latest_document') cn23_document_uuid = UUID(label="Carrier slip document reference") cn23_document = Function(fget='get_latest_cn23_document') tracking_number = String(label="Carrier tracking number") def _get_latest_cocument(self, document_uuid): Document = self.registry.Attachment.Document.Latest query = Document.query().filter_by(uuid=document_uuid) return query.one_or_none() def get_latest_document(self): return self._get_latest_cocument(self.document_uuid) def get_latest_cn23_document(self): return self._get_latest_cocument(self.cn23_document_uuid) def create_label(self): """Retrieve a shipping label from shipping service """ if not self.status == 'new': return return self.service.create_label(shipment=self) def get_label_status(self): """Retrieve a shipping label from shipping service """ if self.status in ('new', 'delivered', 'error'): return return self.service.get_label_status(shipment=self) @classmethod def get_labels_status(cls): status = ['label', 'transit', 'exception'] shipments = cls.query().filter(cls.status.in_(status)).all() for shipment in shipments: shipment.get_label_status() def _save_document(self, document, binary_file, content_type): document.set_file(binary_file) document.filesize = len(binary_file) document.contenttype = content_type hash = hashlib.sha256() hash.update(binary_file) document.hash = hash.digest() self.registry.flush() # flush to update version in document def save_document(self, binary_file, content_type): document = self.document if document is None: document = self.registry.Attachment.Document.insert( data={'shipment': str(self.uuid)}) self.document_uuid = document.uuid self._save_document(document, binary_file, content_type) def save_cn23_document(self, binary_file, content_type): document = self.cn23_document if document is None: document = self.registry.Attachment.Document.insert( data={'shipment': str(self.uuid)}) self.cn23_document_uuid = document.uuid self._save_document(document, binary_file, content_type)
class Arrival(Mixin.WmsSingleOutcomeOperation, Operation): """Operation to describe physical arrival of goods in some location. Arrivals store data about the expected or arrived physical objects: properties, code… These are copied over to the corresponding PhysObj records in all cases and stay inert after the fact. In case the Arrival state is ``planned``, these are obviously only unchecked values, but in case it is ``done``, the actual meaning can depend on the application: - maybe the application won't use the ``planned`` state at all, and will only create Arrival after checking them, - maybe the application will inspect the Arrival properties, compare them to reality, update them on the created PhysObj and cancel downstream operations if needed, before calling :meth:`execute`. TODO maybe provide higher level facilities for validation scenarios. """ TYPE = 'wms_arrival' id = Integer(label="Identifier", primary_key=True, autoincrement=False, foreign_key=Operation.use('id').options(ondelete='cascade')) """Primary key.""" physobj_type = Many2One(model='Model.Wms.PhysObj.Type') """Expected :class:`PhysObj Type <anyblok_wms_base.core.physobj.Type>`. """ physobj_properties = Jsonb(label="Properties of arrived PhysObj") """Expected :class:`Properties <anyblok_wms_base.core.physobj.Properties>`. They are copied over to the newly created :class:`PhysObj <anyblok_wms_base.core.physobj.PhysObj>` as soon as the Arrival is planned, and aren't updated by :meth:`execute`. Matching them with reality is the concern of separate validation processes, and this field can serve for later assessments after the fact. """ physobj_code = Text(label="Code to set on arrived PhysObj") """Expected :attr:`PhysObj code <anyblok_wms_base.core.physobj.PhysObj.code>`. Can be ``None`` in case the arrival process issues the code only at the time of actual arrival. """ location = Many2One(model='Model.Wms.PhysObj') """Will be the location of the initial Avatar.""" goods_type = Function(fget='_goods_type_get', fset='_goods_type_set', fexpr='_goods_type_expr') """Compatibility wrapper. Before version 0.9.0, :attr:`physobj_type` was ``goods_type``. This does not extend to compatibility of the former low level ``goods_type_id`` column. """ goods_properties = Function(fget='_goods_properties_get', fset='_goods_properties_set', fexpr='_goods_properties_expr') """Compatibility wrapper. Before version 0.9.0, :attr:`physobj_properties` was ``goods_properties``. """ goods_code = Function(fget='_goods_code_get', fset='_goods_code_set', fexpr='_goods_code_expr') """Compatibility wrapper. Before version 0.9.0, :attr:`physobj_code` was ``goods_code``. """ inputs_number = 0 """This Operation is a purely creative one.""" destination_field = 'location' def specific_repr(self): return ("physobj_type={self.physobj_type!r}, " "location={self.location!r}").format(self=self) def _goods_col_get(self, suffix): deprecation_warn_goods_col(self, suffix) return getattr(self, 'physobj_' + suffix) def _goods_col_set(self, suffix, value): deprecation_warn_goods_col(self, suffix) setattr(self, 'physobj_' + suffix, value) @classmethod def _goods_col_expr(cls, suffix): deprecation_warn_goods_col(cls, suffix) return getattr(cls, 'physobj_' + suffix) def _goods_type_get(self): return self._goods_col_get('type') def _goods_type_set(self, value): self._goods_col_set('type', value) @classmethod def _goods_type_expr(cls): return cls._goods_col_expr('type') def _goods_properties_get(self): return self._goods_col_get('properties') def _goods_properties_set(self, value): self._goods_col_set('properties', value) @classmethod def _goods_properties_expr(cls): return cls._goods_col_expr('properties') def _goods_code_get(self): return self._goods_col_get('code') def _goods_code_set(self, value): self._goods_col_set('code', value) @classmethod def _goods_code_expr(cls): return cls._goods_col_expr('code') @classmethod def check_create_conditions(cls, state, dt_execution, location=None, **kwargs): """Ensure that ``location`` is indeed a container.""" super(Arrival, cls).check_create_conditions(state, dt_execution, **kwargs) if location is None or not location.is_container(): raise OperationContainerExpected( cls, "location field value {offender}", offender=location) def after_insert(self): PhysObj = self.registry.Wms.PhysObj self_props = self.physobj_properties if self_props is None: props = None else: props = PhysObj.Properties.create(**self_props) goods = PhysObj.insert(type=self.physobj_type, properties=props, code=self.physobj_code) PhysObj.Avatar.insert( obj=goods, location=self.location, outcome_of=self, state='present' if self.state == 'done' else 'future', dt_from=self.dt_execution, ) def execute_planned(self): self.outcome.update(state='present', dt_from=self.dt_execution) @classmethod def refine_with_trailing_unpack(cls, arrivals, pack_type, dt_pack_arrival=None, dt_unpack=None, pack_properties=None, pack_code=None): """Replace some Arrivals by the Arrival of a pack followed by an Unpack. This is useful in cases where it is impossible to predict ahead how incoming goods will actually be packed: the arrivals of individual items can first be planned, and once more is known about the form of delivery, this classmethod can replace some of them with the Arrival of a parcel and the subsequent Unpack. Together with :meth:`refine_with_trailing_move <anyblok_wms_base.core.operation.base.Operation.refine_with_trailing_move>`, this can handle the use case detailed in :ref:`improvement_operation_superseding`. :param arrivals: the Arrivals considered to be superseded by the Unpack. It is possible that only a subset of them are superseded, and conversely that the Unpack has more outcomes than the superseded Arrivals. For more details about the matching, see :meth:`Unpack.plan_for_outcomes <anyblok_wms_base.core.operation.unpack.Unpack.plan_for_outcomes>` :param pack_type: :attr:`anyblok_wms_base.core.PhysObj.main.PhysObj.type` of the expected pack. :param pack_properties: optional properties of the expected Pack. This optional parameter is of great importance in the case of parcels with variable contents, since it allows to set the ``contents`` Property. :param str pack_code: Optional code of the expected Pack. :param datetime dt_pack_arrival: expected date/time for the Arrival of the pack. If not specified, a default one will be computed. :param datetime dt_unpack: expected date/time for the Unpack Operation. If not specified, a default one will be computed. """ # noqa (unbreakable meth crosslink) for arr in arrivals: arr.check_alterable() if not arrivals: raise OperationError(cls, "got empty collection of arrivals " "to refine: {arrivals!r}", arrivals=arrivals) arr_iter = iter(arrivals) location = next(arr_iter).location if not all(arr.location == location for arr in arr_iter): raise OperationError(cls, "can't rewrite arrivals to different " "locations, got {nb_locs} different ones in " "{arrivals}", nb_locs=len(set(arr.location for arr in arrivals)), arrivals=arrivals) Wms = cls.registry.Wms Unpack = Wms.Operation.Unpack # check that the arrivals happen in the same locations if dt_pack_arrival is None: # max minimizes the number of date/time shifts to perform # upon later execution, min is more optimistic dt_pack_arrival = min(arr.dt_execution for arr in arrivals) pack_arr = cls.create(location=location, dt_execution=dt_pack_arrival, physobj_type=pack_type, physobj_properties=pack_properties, physobj_code=pack_code, state='planned') arrivals_outcomes = {arr.outcome: arr for arr in arrivals} unpack, attached_avatars = Unpack.plan_for_outcomes( pack_arr.outcomes, arrivals_outcomes.keys(), dt_execution=dt_unpack) for att in attached_avatars: arrivals_outcomes[att].delete() return unpack
class Family: """Product.Family class """ FAMILY_CODE = None family_schema = None template_schema = None item_schema = None id = Integer(label="Identifier", primary_key=True) create_date = DateTime(default=datetime.now, nullable=False) edit_date = DateTime(default=datetime.now, nullable=False, auto_update=True) code = String(label="Family code", unique=True, nullable=False) name = String(label="Family name", size=256) description = Text(label="Family description") properties = Jsonb(label="Family properties") family_code = Selection(selections='get_family_codes') items = Function(fget="fget_items") @classmethod def get_family_codes(cls): return dict() def fget_items(self): """Returns a list of products instance from this family """ return self.registry.InstrumentedList( set([i for t in self.templates for i in t.items])) @classmethod def create(cls, **kwargs): data = kwargs.copy() if cls.family_schema: sch = cls.family_schema(registry=cls.registry) data = sch.load(kwargs) return cls.insert(**data) def amend(self, **kwargs): data = kwargs.copy() properties = data.pop('properties', dict()) if properties: for k, v in properties.items(): self.properties[k] = v if self.family_schema: sch = self.family_schema(registry=self.registry) data.update(dict(properties=self.properties)) data = sch.load(data) self.update(**data) return self @classmethod def query(cls, *args, **kwargs): query = super(Family, cls).query(*args, **kwargs) if cls.__registry_name__ != 'Model.Product.Family': query = query.filter(cls.family_code == cls.FAMILY_CODE) return query @classmethod def define_mapper_args(cls): mapper_args = super(Family, cls).define_mapper_args() if cls.__registry_name__ == 'Model.Product.Family': mapper_args.update({'polymorphic_on': cls.family_code}) mapper_args.update({'polymorphic_identity': cls.FAMILY_CODE}) return mapper_args def __str__(self): return "%s : %s" % (self.code, self.name) def __repr__(self): return "<Product.Family(code=%s, name=%s)>" % (self.code, self.name)
class Model: """Models assembled""" def __str__(self): if self.description: return self.description return self.name name = String(size=256, primary_key=True) table = String(size=256) schema = String() is_sql_model = Boolean(label="Is a SQL model") description = Function(fget='get_model_doc_string') def get_model_doc_string(self): """ Return the docstring of the model """ m = self.registry.get(self.name) if hasattr(m, '__doc__'): return m.__doc__ or '' return '' @listen('Model.System.Model', 'Update Model') def listener_update_model(cls, model): cls.registry.System.Cache.invalidate(model, '_fields_description') cls.registry.System.Cache.invalidate(model, 'getFieldType') cls.registry.System.Cache.invalidate(model, 'get_primary_keys') cls.registry.System.Cache.invalidate( model, 'find_remote_attribute_to_expire') cls.registry.System.Cache.invalidate(model, 'find_relationship') cls.registry.System.Cache.invalidate(model, 'get_hybrid_property_columns') @classmethod def get_field_model(cls, field): ftype = field.property.__class__.__name__ if ftype == 'ColumnProperty': return cls.registry.System.Column elif ftype == 'RelationshipProperty': return cls.registry.System.RelationShip else: raise Exception('Not implemented yet') @classmethod def get_field(cls, model, cname): if cname in model.loaded_fields.keys(): field = model.loaded_fields[cname] Field = cls.registry.System.Field else: field = getattr(model, cname) Field = cls.get_field_model(field) return field, Field @classmethod def update_fields(cls, model, table): fsp = cls.registry.loaded_namespaces_first_step m = cls.registry.get(model) # remove useless column Field = cls.registry.System.Field query = Field.query() query = query.filter(Field.model == model) query = query.filter(Field.name.notin_(m.loaded_columns)) for model_ in query.all(): if model_.entity_type == 'Model.System.RelationShip': if model_.remote: continue RelationShip = cls.registry.System.RelationShip Q = RelationShip.query() Q = Q.filter(RelationShip.name == model_.remote_name) Q = Q.filter(RelationShip.model == model_.remote_model) Q.delete() model_.delete() # add or update new column for cname in m.loaded_columns: ftype = fsp[model][cname].__class__.__name__ field, Field = cls.get_field(m, cname) cname = Field.get_cname(field, cname) query = Field.query() query = query.filter(Field.model == model) query = query.filter(Field.name == cname) if query.count(): Field.alter_field(query.first(), field, ftype) else: Field.add_field(cname, field, model, table, ftype) @classmethod def add_fields(cls, model, table): fsp = cls.registry.loaded_namespaces_first_step m = cls.registry.get(model) is_sql_model = len(m.loaded_columns) > 0 cls.insert(name=model, table=table, schema=m.__db_schema__, is_sql_model=is_sql_model) for cname in m.loaded_columns: field, Field = cls.get_field(m, cname) cname = Field.get_cname(field, cname) ftype = fsp[model][cname].__class__.__name__ Field.add_field(cname, field, model, table, ftype) @classmethod def update_list(cls): """ Insert and update the table of models :exception: Exception """ for model in cls.registry.loaded_namespaces.keys(): try: # TODO need refactor, then try except pass whenever refactor # not apply m = cls.registry.get(model) table = '' if hasattr(m, '__tablename__'): table = m.__tablename__ _m = cls.query('name').filter(cls.name == model) if _m.count(): cls.update_fields(model, table) else: cls.add_fields(model, table) if m.loaded_columns: cls.fire('Update Model', model) except Exception as e: logger.exception(str(e)) # remove model and field which are not in loaded_namespaces query = cls.query() query = query.filter( cls.name.notin_(cls.registry.loaded_namespaces.keys())) Field = cls.registry.System.Field for model_ in query.all(): Q = Field.query().filter(Field.model == model_.name) for field in Q.all(): field.delete() model_.delete()
def test_forbid_function_field(self): with pytest.raises(FieldException): Contextual(field=Function(fget='foo'), identity='foo')
class Blok: STATES = { 'uninstalled': 'Uninstalled', 'installed': 'Installed', 'toinstall': 'To install', 'toupdate': 'To update', 'touninstall': 'To uninstall', 'undefined': 'Undefined', } name = String(primary_key=True, nullable=False) state = Selection(selections=STATES, default='uninstalled', nullable=False) author = String() order = Integer(default=-1, nullable=False) short_description = Function(fget='get_short_description') long_description = Function(fget='get_long_description') logo = Function(fget='get_logo') version = String(nullable=False) installed_version = String() def get_short_description(self): """ fget of the ``short_description`` Column.Selection :rtype: the docstring of the blok """ blok = BlokManager.get(self.name) if hasattr(blok, '__doc__'): return blok.__doc__ or '' return '' def get_long_description(self): """ fget of the ``long_description`` Column.Selection :rtype: the readme file of the blok """ blok = BlokManager.get(self.name) path = BlokManager.getPath(self.name) readme = getattr(blok, 'readme', 'README.rst') if readme == '__doc__': return blok.__doc__ file_path = join(path, readme) description = '' if isfile(file_path): with open(file_path, 'r') as fp: description = fp.read() return description def get_logo(self): """fget of ``logo`` return the path in the blok of the logo :rtype: absolute path or None if unexiste logo """ blok = BlokManager.get(self.name) blok_path = BlokManager.getPath(blok.name) file_path = join(blok_path, blok.logo) if isfile(file_path): return file_path return None def __repr__(self): return "%s (%s)" % (self.name, self.state) @classmethod def list_by_state(cls, *states): """ Return the blok name in function of the wanted states :param states: list of the state :rtype: list if state is a state, dict if the states is a list """ if not states: return None res = {state: [] for state in states} bloks = cls.query().filter(cls.state.in_(states)).order_by(cls.order) for blok in bloks.all(): res[blok.state].append(blok.name) if len(states) == 1: return res[states[0]] return res @classmethod def update_list(cls): """ Populate the bloks list and update the state of existing bloks """ # Do not remove blok because 2 or More AnyBlok api may use the same # Database for order, blok in enumerate(BlokManager.ordered_bloks): b = cls.query().filter(cls.name == blok).one_or_none() Blok = BlokManager.bloks[blok] version = Blok.version author = Blok.author is_undefined = issubclass(Blok, UndefinedBlok) if b is None: cls.insert( name=blok, order=order, version=version, author=author, state='undefined' if is_undefined else 'uninstalled') else: values = dict(order=order, version=version, author=author) if b.state == 'undefined' and not is_undefined: values['state'] = 'uninstalled' elif is_undefined: if b.state not in ('uninstalled', 'undefined'): raise BlokManagerException( ("Change state %r => 'undefined' for %s is " "forbidden") % (b.state, blok)) values['state'] = 'undefined' b.update(**values) @classmethod def apply_state(cls, *bloks): """ Call the rigth method is the blok state change .. warning:: for the uninstallation the method called is ``uninstall_all`` :param bloks: list of the blok name load by the registry """ for blok in bloks: # Make the query in the loop to be sure to keep order b = cls.query().filter(cls.name == blok).first() if b.state in ('uninstalled', 'toinstall'): b.install() elif b.state == 'toupdate': b.upgrade() uninstalled_bloks = cls.query().filter( cls.state == 'uninstalled').all().name conditional_bloks_to_install = [] for blok in uninstalled_bloks: if cls.check_if_the_conditional_are_installed(blok): conditional_bloks_to_install.append(blok) if conditional_bloks_to_install: for b in conditional_bloks_to_install: cls.query().filter(cls.name == b).update( {cls.state: 'toinstall'}) return True return False @classmethod def uninstall_all(cls, *bloksname): """ Search and call the uninstall method for all the uninstalled bloks .. warning:: Use the ``desc order`` to uninstall because we can't uninstall a dependancies before :param bloksname: list of the blok name to uninstall """ if not bloksname: return query = cls.query().filter(cls.name.in_(bloksname)) query = query.order_by(cls.order.desc()) bloks = query.all() if bloks: bloks.uninstall() @classmethod def check_if_the_conditional_are_installed(cls, blok): """ Return True if all the conditions to install the blok are satisfied :param blok: blok name :rtype: boolean """ if blok in BlokManager.bloks: conditional = BlokManager.bloks[blok].conditional if conditional: query = cls.query().filter(cls.name.in_(conditional)) query = query.filter( cls.state.in_(['installed', 'toinstall', 'toupdate'])) if len(conditional) == query.count(): return True return False def install(self): """ Method to install the blok """ logger.info("Install the blok %r" % self.name) self.fire('Update installed blok') entry = self.registry.loaded_bloks[self.name] entry.update(None) if self.registry.System.Parameter.get("with-demo", False): entry.update_demo(None) self.state = 'installed' self.installed_version = self.version def upgrade(self): """ Method to update the blok """ logger.info("Update the blok %r" % self.name) self.fire('Update installed blok') entry = self.registry.loaded_bloks[self.name] parsed_version = (parse_version(self.installed_version) if self.installed_version is not None else None) entry.update(parsed_version) if self.registry.System.Parameter.get("with-demo", False): entry.update_demo(parsed_version) self.state = 'installed' self.installed_version = self.version def uninstall(self): """ Method to uninstall the blok """ logger.info("Uninstall the blok %r" % self.name) self.fire('Update installed blok') entry = BlokManager.bloks[self.name](self.registry) if self.registry.System.Parameter.get("with-demo", False): entry.uninstall_demo() entry.uninstall() self.state = 'uninstalled' self.installed_version = None def load(self): """ Method to load the blok when the registry is completly loaded """ name = self.name blok_cls = BlokManager.get(name) if blok_cls is None: logger.warning( "load(): class of Blok %r not found, " "Blok can't be loaded", name) return logger.info("Loading Blok %r", name) blok_cls(self.registry).load() logger.debug("Succesfully loaded Blok %r", name) @classmethod def load_all(cls): """ Load all the installed bloks """ query = cls.query().filter(cls.state == 'installed') bloks = query.order_by(cls.order).all() if bloks: bloks.load() @classmethod_cache() def is_installed(cls, blok_name): return cls.query().filter_by(name=blok_name, state='installed').count() != 0 @listen('Model.System.Blok', 'Update installed blok') def listen_update_installed_blok(cls): cls.registry.System.Cache.invalidate(cls.__registry_name__, 'is_installed')
class Apparition(Mixin.WmsInventoryOperation, Operation): """Inventory Operation to record unexpected physical objects. This is similar to Arrival, but has a distinct functional meaning. Apparitions can exist only in the ``done`` :ref:`state <op_states>`. Another difference with Arrivals is that Apparitions have a :attr:`quantity` field. """ TYPE = 'wms_apparition' id = Integer(label="Identifier", primary_key=True, autoincrement=False, foreign_key=Operation.use('id').options(ondelete='cascade')) """Primary key.""" physobj_type = Many2One(model='Model.Wms.PhysObj.Type') """Observed :class:`PhysObj Type <anyblok_wms_base.core.physobj.Type>`. """ quantity = Integer() """The number of identical PhysObj that have appeared. Here, identical means "same type, code and properties" """ physobj_properties = Jsonb() """Observed :class:`Properties <anyblok_wms_base.core.physobj.Properties>`. They are copied over to the newly created :class:`PhysObj <anyblok_wms_base.core.physobj.PhysObj>`. Then the Properties can evolve on the PhysObj, while this Apparition field will keep the exact values that were observed during inventory. """ physobj_code = Text() """Observed :attr:`PhysObj code <anyblok_wms_base.core.physobj.PhysObj.code>`. """ location = Many2One(model='Model.Wms.PhysObj') """Location of appeared PhysObj. This will be the location of the initial Avatars. """ goods_type = Function(fget='_goods_type_get', fset='_goods_type_set', fexpr='_goods_type_expr') """Compatibility wrapper. Before version 0.9.0, :attr:`physobj_type` was ``goods_type``. This does not extend to compatibility of the former low level ``goods_type_id`` column. """ goods_properties = Function(fget='_goods_properties_get', fset='_goods_properties_set', fexpr='_goods_properties_expr') """Compatibility wrapper. Before version 0.9.0, :attr:`physobj_properties` was ``goods_properties``. """ goods_code = Function(fget='_goods_code_get', fset='_goods_code_set', fexpr='_goods_code_expr') """Compatibility wrapper. Before version 0.9.0, :attr:`physobj_code` was ``goods_code``. """ inputs_number = 0 """This Operation is a purely creative one.""" def specific_repr(self): return ("physobj_type={self.physobj_type!r}, " "location={self.location!r}").format(self=self) def _goods_col_get(self, suffix): deprecation_warn_goods_col(self, suffix) return getattr(self, 'physobj_' + suffix) def _goods_col_set(self, suffix, value): deprecation_warn_goods_col(self, suffix) setattr(self, 'physobj_' + suffix, value) @classmethod def _goods_col_expr(cls, suffix): deprecation_warn_goods_col(cls, suffix) return getattr(cls, 'physobj_' + suffix) def _goods_type_get(self): return self._goods_col_get('type') def _goods_type_set(self, value): self._goods_col_set('type', value) @classmethod def _goods_type_expr(cls): return cls._goods_col_expr('type') def _goods_properties_get(self): return self._goods_col_get('properties') def _goods_properties_set(self, value): self._goods_col_set('properties', value) @classmethod def _goods_properties_expr(cls): return cls._goods_col_expr('properties') def _goods_code_get(self): return self._goods_col_get('code') def _goods_code_set(self, value): self._goods_col_set('code', value) @classmethod def _goods_code_expr(cls): return cls._goods_col_expr('code') @classmethod def check_create_conditions(cls, state, dt_execution, location=None, **kwargs): """Forbid creation with wrong states, check location is a container. :raises: :class:`OperationForbiddenState <anyblok_wms_base.exceptions.OperationForbiddenState>` if state is not ``'done'`` :class:`OperationContainerExpected <anyblok_wms_base.exceptions.OperationContainerExpected>` if location is not a container. """ if location is None or not location.is_container(): raise OperationContainerExpected(cls, "location field value {offender}", offender=location) super().check_create_conditions(state, dt_execution, **kwargs) def after_insert(self): """Create the PhysObj and their Avatars. In the ``wms-core`` implementation, the :attr:`quantity` field gives rise to as many PhysObj records. """ PhysObj = self.registry.Wms.PhysObj self_props = self.physobj_properties if self_props is None: props = None else: props = PhysObj.Properties.create(**self_props) for _ in range(self.quantity): PhysObj.Avatar.insert(obj=PhysObj.insert(type=self.physobj_type, properties=props, code=self.physobj_code), location=self.location, outcome_of=self, state='present', dt_from=self.dt_execution)