class WeatherStation(Model.Iot.State): """Weather state from APRS-IS packet""" STATE_TYPE = "WEATHER_STATION" uuid: uuid4 = UUID( primary_key=True, default=uuid4, binary=False, foreign_key=Model.Iot.State.use("uuid").options(ondelete="CASCADE"), ) sensor_date: DateTime = DateTime( label="Sensor timestamp", index=True, ) station_code: str = String(label="Station code", unique=False, index=True, nullable=False) wind_direction: D = Decimal(label="Wind direction") wind_speed: D = Decimal(label="Wind Speed (km/h ?)") wind_gust: D = Decimal(label="Wind gust (km/h ?)") temperature: D = Decimal(label="Thermometer (°C)") rain_1h: D = Decimal(label="rain (mm/1h)") rain_24h: D = Decimal(label="rain (mm/24h)") rain_since_midnight: D = Decimal(label="rain (mm/since midnight)") humidity: D = Decimal(label="Humidity (%)") pressure: D = Decimal(label="Pressure (hPa)") luminosity: D = Decimal(label="Luminosity/irradiation (W/m2)") @classmethod def define_table_args(cls): res = super().define_table_args() return res + (UniqueConstraint(cls.sensor_date, cls.station_code), ) @classmethod def get_device_state( cls, code: str, ) -> Union["registry.Model.Iot.State.Relay", "registry.Model.Iot.State.DesiredRelay", "registry.Model.Iot.State.Thermometer", "registry.Model.Iot.State.FuelGauge", "registry.Model.Iot.State.WeatherStation", ]: """Overwrite parent method to sort on sensor date and set fake date while creating empty state """ Device = cls.registry.Iot.Device state = (cls.query().join(Device).filter(Device.code == code).order_by( cls.registry.Iot.State.WeatherStation.sensor_date.desc()).first()) if not state: device = Device.query().filter_by(code=code).one() # We don't want to instert a new state here, just creating # a default instance state = cls(device=device, sensor_date=datetime.now()) cls.registry.flush() return state
class DecrementCommitment(Model.REA.Entity): """ Commitment is a promise or obligation of economic agents to perform an economic event in the future. For example, line items on a sales order represent commitments to sell goods. # Model-Driven Design Using Business Patterns # Authors: Hruby, Pavel # ISBN-10 3-540-30154-2 Springer Berlin Heidelberg New York # ISBN-13 978-3-540-30154-7 Springer Berlin Heidelberg New York """ id = Integer(primary_key=True, foreign_key=Model.REA.Entity.use('id')) recipient = Many2One(label="Agent recipient", model=Model.REA.Agent, nullable=False) resource = Many2One(label="Reservation Resource", model=Model.REA.Resource, nullable=False) value = Decimal(label="Value decrement", default=decimalType(0)) fulfilled = Boolean(label="Is Fulfilled", default=False) def fulfill(self): """ :return: True if commitment is fulfilled """ if not self.fulfilled: self.registry.REA.DecrementEvent.create_event_from_commitment(self) self.fulfilled = True return True return False
class IncrementEvent(Model.REA.Entity): """ Economic Event represents either an increment or a decrement in the value of economic resources that are under the control of the enterprise. Some economic events occur instantaneously, such as sales of goods; some occur over time, such as rentals, labor acquisition, and provision and use of services. # Model-Driven Design Using Business Patterns # Authors: Hruby, Pavel # ISBN-10 3-540-30154-2 Springer Berlin Heidelberg New York # ISBN-13 978-3-540-30154-7 Springer Berlin Heidelberg New York """ id = Integer(primary_key=True, foreign_key=Model.REA.Entity.use('id')) provider = Many2One(label="Agent provider", model=Model.REA.Agent, nullable=False) resource = Many2One(label='Resource Inflow', model=Model.REA.Resource, nullable=False) value = Decimal(label="increment value", default=decimalType(0)) date = DateTime(label="Event Date", default=lambda **kwargs: datetime.now())
class Item(Mixin.UuidColumn, Mixin.TrackModel): SCHEMA = PriceListItemSchema @classmethod def get_schema_definition(cls, **kwargs): return cls.SCHEMA(**kwargs) price_list = Many2One(label="Pricelist", model=Declarations.Model.Sale.PriceList, nullable=False, one2many="price_list_items") item = Many2One(label="Product Item", model=Declarations.Model.Product.Item, nullable=False, unique=True, one2many="prices") unit_price_untaxed = Decimal(label="Price untaxed", default=D(0)) unit_price = Decimal(label="Price", default=D(0)) unit_tax = Decimal(label="Tax", default=D(0)) def __str__(self): return "{self.item.code} {self.unit_price_untaxed}".format(self=self) def __repr__(self): return ("<PriceListItem(uuid={self.uuid}, item.code={self.item.code}, " "unit_price_untaxed={self.unit_price_untaxed}>").format( self=self) @classmethod def create(cls, keep_gross=True, **kwargs): data = kwargs.copy() if cls.get_schema_definition: sch = cls.get_schema_definition(registry=cls.registry) data = sch.load(kwargs) net = data.get('unit_price_untaxed') or D(0) gross = data.get('unit_price') or D(0) tax = data.get('unit_tax') or D(0) price = compute_price(net=net, gross=gross, tax=tax, keep_gross=keep_gross) data['unit_price_untaxed'] = price.net.amount data['unit_price'] = price.gross.amount data['unit_tax'] = compute_tax(tax) return cls.insert(**data)
class PhysObj: """Override to add the :attr:`quantity` field. """ quantity = Decimal(label="Quantity", default=1) """Quantity Depending on the PhysObj Type, this represents in reality some physical measure (length of wire, weight of wheat) for PhysObj stored and handled in bulk, or a number of identical items, if goods are kept as individual pieces. There is no corresponding idea of a unit of measure for bulk PhysObj, as we believe it to be enough to represent it in the PhysObj Type already (which would be, e.g, respectively a meter of wire, a ton of wheat). Note that bulk PhysObj can be the result of some :ref:`op_unpack`, with the packaged version being itself handled as an individual piece (imagine spindles of 100m for the wire example) and further packable (pallets, containers…) This field has been defined as Decimal to cover a broad scope of applications. However, for performance reasons, applications are free to overload this column with other numeric types (i.e., supporting transparently all the usual operations with the same syntax, both in Python and SQL). Examples : + applications not caring about fractional quantities might want to overload this column with an Integer column for extra performance (if that really makes a difference). + applications having to deal with fractional quantities not well behaved in decimal notation (e.g., thirds of cherry pies) may want to switch to a rational number type, such as ``mpq`` type on the PostgreSQL side), although it's probably a better idea if there's an obvious common denominator to just use integers (following on the example, simply have PhysObj Types representing those thirds of pies alongside those representing the whole pies, and represent the first cutting of a slice as an Unpack) """ @classmethod def define_table_args(cls): return super(PhysObj, cls).define_table_args() + (CheckConstraint( 'quantity > 0', name='positive_qty'), ) def __str__(self): return ("(id={self.id}, type={self.type}, " "quantity={self.quantity})").format(self=self) def __repr__(self): return ("Wms.PhysObj(id={self.id}, type={self.type!r}, " "quantity={self.quantity!r})".format(self=self))
class Range(Mixin.UuidColumn, Mixin.TrackModel): code: str = String(label="Code", index=True, nullable=False) start: time = Time(default=time(hour=0, minute=0)) end: time = Time(default=time(hour=23, minute=59)) celsius: D = Decimal(label="Thermometer (°C)") @classmethod def get_desired_living_room_temperature(cls, date: datetime) -> Optional[D]: if not date: date = datetime.now() subquery = (cls.query().distinct( cls.code).filter(cls.create_date < date).order_by( cls.code.desc()).order_by(cls.create_date.desc()).subquery()) range_ = (cls.registry.query( subquery.c.celsius).select_from(subquery).filter( subquery.c.start <= date.time(), subquery.c.end > date.time()).first()) if range_: return range_.celsius return None
class Arrival: """Override to add the :attr:`quantity` field. """ quantity = Decimal(default=1) """Quantity of the PhysObj to be created. It defaults to 1 to help adding ``wms-quantity`` to a codebase. """ @classmethod def define_table_args(cls): return super(Arrival, cls).define_table_args() + (CheckConstraint( 'quantity > 0', name='positive_qty'), ) def specific_repr(self): return ("physobj_type={self.physobj_type!r}, " "location={self.location!r}, " "quantity={self.quantity}").format(self=self) def after_insert(self): # TODO reduce duplication 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, quantity=self.quantity, 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, )
class Thermometer(Model.Iot.State): STATE_TYPE = "TEMPERATURE" uuid: uuid4 = UUID( primary_key=True, default=uuid4, binary=False, foreign_key=Model.Iot.State.use("uuid").options(ondelete="CASCADE"), ) celsius: D = Decimal(label="Thermometer (°C)") @classmethod def get_last_value(cls, device_code: str): Device = cls.registry.Iot.Device last = (cls.query().join(Device).filter( Device.code == device_code).order_by( cls.registry.Iot.State.create_date.desc()).first()) if last: return last.celsius return 0 @classmethod def wait_min_return_desired_temperature(cls, device_sensor: DEVICE, device_min: DEVICE, device_max: DEVICE): # wait return temperature to get the DEVICE.MIN_RET_DESIRED.value # temperature before start burner again Device = cls.registry.Iot.Device State = cls.registry.Iot.State min_temp = cls.get_last_value(device_min.value) max_temp = cls.get_last_value(device_max.value) last_pick = (cls.query().join(Device).filter( Device.code == device_sensor.value).filter( cls.celsius >= max_temp).order_by( State.create_date.desc()).first()) if not last_pick: return False last_curve = (cls.query().join(Device).filter( Device.code == device_sensor.value).filter( cls.celsius <= min_temp).filter( State.create_date > last_pick.create_date).order_by( State.create_date.desc()).first()) if not last_curve: return True return False @classmethod def get_living_room_avg(cls, date: datetime = None, minutes=5): avg = cls.get_sensor_avg(DEVICE.LIVING_ROOM_SENSOR.value, date=date, minutes=minutes) if not avg: return 0 return avg @classmethod def get_sensor_avg(cls, device_code: str, date: datetime = None, minutes: int = 15): if not date: date = datetime.now() Device = cls.registry.Iot.Device return (cls.query(func.avg( cls.celsius).label("average")).join(Device).filter( Device.code == device_code).filter( cls.create_date > date - timedelta(minutes=minutes), cls.create_date <= date, ).group_by(Device.code).scalar())
class Order(Mixin.UuidColumn, Mixin.TrackModel, Mixin.WorkFlow): """Sale.Order model """ SCHEMA = OrderBaseSchema @classmethod def get_schema_definition(cls, **kwargs): return cls.SCHEMA(**kwargs) @classmethod def get_workflow_definition(cls): return { 'draft': { 'default': True, 'allowed_to': ['quotation', 'cancelled'] }, 'quotation': { 'allowed_to': ['order', 'cancelled'], 'validators': SchemaValidator( cls.get_schema_definition(exclude=['price_list'])) }, 'order': { 'validators': SchemaValidator( cls.get_schema_definition(exclude=['price_list'])) }, 'cancelled': {}, } code = String(label="Code", nullable=False) channel = String(label="Sale Channel", nullable=False) price_list = Many2One(label="Price list", model=Declarations.Model.Sale.PriceList) delivery_method = String(label="Delivery Method") amount_untaxed = Decimal(label="Amount Untaxed", default=D(0)) amount_tax = Decimal(label="Tax amount", default=D(0)) amount_total = Decimal(label="Total", default=D(0)) def __str__(self): return "{self.uuid} {self.channel} {self.code} {self.state}".format( self=self) def __repr__(self): return "<Sale(id={self.uuid}, code={self.code}," \ " amount_untaxed={self.amount_untaxed},"\ " amount_tax={self.amount_tax},"\ " amount_total={self.amount_total},"\ " channel={self.channel} state={self.state})>".format( self=self) @classmethod def create(cls, price_list=None, **kwargs): data = kwargs.copy() if cls.get_schema_definition: sch = cls.get_schema_definition(registry=cls.registry, exclude=['lines']) if price_list: data["price_list"] = price_list.to_primary_keys() data = sch.load(data) data['price_list'] = price_list return cls.insert(**data) def compute(self): """Compute order total amount""" amount_untaxed = D(0) amount_tax = D(0) amount_total = D(0) for line in self.lines: amount_untaxed += line.amount_untaxed amount_tax += line.amount_tax amount_total += line.amount_total self.amount_untaxed = amount_untaxed self.amount_tax = amount_tax self.amount_total = amount_total
class Line(Mixin.UuidColumn, Mixin.TrackModel): """Sale.Order.Line Model """ SCHEMA = OrderLineBaseSchema @classmethod def get_schema_definition(cls, **kwargs): return cls.SCHEMA(**kwargs) order = Many2One(label="Order", model=Declarations.Model.Sale.Order, nullable=False, one2many="lines") item = Many2One(label="Product Item", model=Declarations.Model.Product.Item, nullable=False) properties = Jsonb(label="Item properties", default=dict()) unit_price_untaxed = Decimal(label="Price untaxed", default=D(0)) unit_price = Decimal(label="Price", default=D(0)) unit_tax = Decimal(label="Tax", default=D(0)) quantity = Integer(label="Quantity", default=1, nullable=False) amount_untaxed = Decimal(label="Amount untaxed", default=D(0)) amount_tax = Decimal(label="Tax amount", default=D(0)) amount_total = Decimal(label="Total", default=D(0)) amount_discount_percentage_untaxed = Decimal( label="Amount discount percentage untaxed", default=D(0)) amount_discount_percentage = Decimal(label="Amount discount percentage", default=D(0)) amount_discount_untaxed = Decimal(label="Amount discount untaxed", default=D(0)) amount_discount = Decimal(label="Amount discount", default=D(0)) def __str__(self): return "{self.uuid} : {self.amount_total}".format(self=self) def __repr__(self): return "<Sale.Order.Line(uuid={self.uuid},"\ " amount_untaxed={self.amount_untaxed},"\ " amount_tax={self.amount_tax},"\ " amount_total={self.amount_total})>".format(self=self) def check_unit_price(self): """Ensure consistency between unit_price_untaxed, unit_price and unit_tax TODO: Move this to a specialized marshmallow validation method """ if (self.unit_price_untaxed < D(0) or self.unit_price < D(0) or self.unit_tax < D(0)): raise LineException( """Negative Value forbidden on unit_price_untaxed, unit_price or unit_tax""") if (self.unit_price_untaxed != self.unit_price and self.unit_tax == D(0)): raise LineException( """Inconsistency between unit_price_untaxed, unit_price and unit_tax""") if self.unit_tax != D(0): if (self.unit_price_untaxed >= self.unit_price and self.unit_price != D(0)): raise LineException( """unit_price_untaxed can not be greater than unit_price""" ) def compute(self): """Compute order line total amount * check unit_price consistency * compute tax if any * compute line total amount TODO: maybe add configuration options for computation behaviours, for example computation based on unit_price or unit_price_untaxed """ if not self.order.price_list: self.check_unit_price() if self.unit_price != D(0) and self.unit_price_untaxed == D(0): # compute unit_price_untaxed based on unit_price price = compute_price(net=self.unit_price, gross=self.unit_price, tax=compute_tax(self.unit_tax), keep_gross=True) elif self.unit_price_untaxed != D(0) and self.unit_price == D(0): # compute unit_price based on unit_price_untaxed price = compute_price(net=self.unit_price_untaxed, gross=self.unit_price_untaxed, tax=compute_tax(self.unit_tax), keep_gross=False) elif self.unit_price_untaxed != D(0) and self.unit_price != D(0): # compute unit_price_untaxed based on unit_price price = compute_price(net=self.unit_price, gross=self.unit_price, tax=compute_tax(self.unit_tax), keep_gross=True) else: raise LineException( """Can not find a strategy to compute price""") self.unit_price_untaxed = price.net.amount self.unit_price = price.gross.amount self.unit_tax = compute_tax(self.unit_tax) else: # compute unit price based on price list price_list_item = self.registry.Sale.PriceList.Item.query( ).filter_by(price_list=self.order.price_list).filter_by( item=self.item).one_or_none() if price_list_item: self.unit_price = price_list_item.unit_price self.unit_price_untaxed = price_list_item.unit_price_untaxed self.unit_tax = price_list_item.unit_tax else: raise LineException("""Can not find a price for %r on %r""" % (self.item, self.order.price_list)) # compute total amount self.amount_total = D(self.unit_price * self.quantity) self.amount_untaxed = D(self.unit_price_untaxed * self.quantity) self.amount_tax = self.amount_total - self.amount_untaxed # compute total amount after discount if self.amount_discount_untaxed != D('0'): price = compute_price(net=self.amount_untaxed, tax=self.unit_tax, keep_gross=False) discount = compute_discount( price=price, tax=self.unit_tax, discount_amount=self.amount_discount_untaxed, from_gross=False) self.amount_total = discount.gross.amount self.amount_untaxed = discount.net.amount self.amount_tax = discount.tax.amount return if self.amount_discount_percentage_untaxed != D('0'): price = compute_price(net=self.amount_untaxed, tax=self.unit_tax, keep_gross=False) discount = compute_discount( price=price, tax=self.unit_tax, discount_percent=self.amount_discount_percentage_untaxed, from_gross=False) self.amount_total = discount.gross.amount self.amount_untaxed = discount.net.amount self.amount_tax = discount.tax.amount return if self.amount_discount != D('0'): price = compute_price(gross=self.amount_total, tax=self.unit_tax, keep_gross=True) discount = compute_discount(price=price, tax=self.unit_tax, discount_amount=self.amount_discount, from_gross=True) self.amount_total = discount.gross.amount self.amount_untaxed = discount.net.amount self.amount_tax = discount.tax.amount return if self.amount_discount_percentage != D('0'): price = compute_price(gross=self.amount_total, tax=self.unit_tax, keep_gross=True) discount = compute_discount( price=price, tax=self.unit_tax, discount_percent=self.amount_discount_percentage, from_gross=True) self.amount_total = discount.gross.amount self.amount_untaxed = discount.net.amount self.amount_tax = discount.tax.amount return @classmethod def create(cls, order=None, item=None, **kwargs): data = kwargs.copy() if order is None: raise TypeError if item is None: raise TypeError if cls.get_schema_definition: sch = cls.get_schema_definition( registry=cls.registry, required_fields=["order", "item", "quantity"]) data['item'] = item.to_primary_keys() data['order'] = order.to_primary_keys() data = sch.load(data) data['item'] = item data['order'] = order line = cls.insert(**data) line.compute() return line @classmethod def before_update_orm_event(cls, mapper, connection, target): if cls.get_schema_definition: sch = cls.get_schema_definition( registry=cls.registry, required_fields=["order", "item", "quantity"]) sch.load(sch.dump(target)) if (target.properties and cls.registry.System.Blok.is_installed('product_family') and target.item.template.family.custom_schemas): props = target.item.template.family.custom_schemas.get( target.item.code.lower()).get('schema') props_sch = props(context={"registry": cls.registry}) props_sch.load(target.properties) target.compute()
class WmsSplitterOperation: """Mixin for operations on a single input that can split. This is to be applied after :class:`Mixin.WmsSingleInputOperation <anyblok_wms_base.core.operation.single_input.WmsSingleInputOperation>`. Use :class:`WmsSplitterSingleInputOperation` to get both at once. It defines the :attr:`quantity` field to express that the Operation only works on some of the quantity held by the PhysObj of the single input. In case the Operation's :attr:`quantity` is less than in the PhysObj record, a :class:`Split <.split.Split>` will be inserted properly in history, and the Operation implementation can ignore quantities completely, as it will always, in truth, work on the whole of the input it will see. Subclasses can use the :attr:`partial` field if they need to know if that happened, but this should be useful only in special cases. """ quantity = Decimal(default=1) """The quantity this Operation will work on. Can be less than the quantity of our single input. """ partial = Boolean(label="Operation induced a split") """Record if a Split will be or has been inserted in the history. Such insertions should happen if the operation's original PhysObj have greater quantity than the operation needs. This is useful because once the Split is executed, this information can't be deduced from the quantities involved any more. """ @classmethod def define_table_args(cls): return super(WmsSplitterOperation, cls).define_table_args() + ( CheckConstraint('quantity > 0', name='positive_qty'), ) def specific_repr(self): return ("input={self.input!r}, " "quantity={self.quantity}").format(self=self) @classmethod def check_create_conditions(cls, state, dt_execution, inputs=None, quantity=None, **kwargs): super(WmsSplitterOperation, cls).check_create_conditions(state, dt_execution, inputs=inputs, quantity=quantity, **kwargs) phobj = inputs[0].obj if quantity is None: raise OperationMissingQuantityError( cls, "The 'quantity keyword argument must be passed to " "the create() method") if quantity > phobj.quantity: raise OperationQuantityError( cls, "Can't split a greater quantity ({op_quantity}) than held in " "{input} (which have quantity={input.obj.quantity})", op_quantity=quantity, input=inputs[0]) def check_execute_conditions(self): """Check that the quantity (after possible Split) is as on the input. If a Split has been inserted, then this calls the base class for the input of the Split, instead of ``self``, because the input of ``self`` is in that case the outcome of the Split, and it's normal that it's in state ``future``: the Split will be executed during ``self.execute()``, which comes once the present method has agreed. """ if self.quantity != self.input.obj.quantity: raise OperationQuantityError( self, "Can't execute planned for a different quantity {op_quantity} " "than held in its input {input} " "(which have quantity={input.obj.quantity}). " "If it's less, a Split should have occured first ", input=input) if self.partial: self.input.outcome_of.check_execute_conditions() else: super(WmsSplitterOperation, self).check_execute_conditions() @classmethod def before_insert(cls, state='planned', follows=None, inputs=None, quantity=None, dt_execution=None, dt_start=None, **kwargs): """Override to introduce a Split if needed In case the value of :attr:`quantity` does not match the one from the ``goods`` field, a :class:`Split <.split.Split>` is inserted transparently in the history, as if it'd been there in the first place: subclasses can implement :meth:`after_insert` as if the quantities were matching from the beginning. """ avatar = inputs[0] partial = quantity < avatar.obj.quantity if not partial: return inputs, None Split = cls.registry.Wms.Operation.Split split = Split.create(input=avatar, quantity=quantity, state=state, dt_execution=dt_execution) return [split.wished_outcome], dict(partial=partial) def execute_planned(self): """Execute the :class:`Split <.split.Split>` if any, then self.""" if self.partial: split_op = next(iter(self.follows)) split_op.execute(dt_execution=self.dt_execution) super(WmsSplitterOperation, self).execute_planned() self.registry.flush()
class Split(SingleInput, InPlace, Operation): """A split of PhysObj record in two. Splits replace their input's :class:`PhysObj <anyblok_wms_base.quantity.physobj.PhysObj>` record with two of them, one having the wished :attr:`quantity`, along with Avatars at the same location, while keeping the same properties and the same total quantity. This is therefore destructive for the input's PhysObj, which is not conceptually satisfactory, but will be good enough at this stage of development. While non trivial in the database, they may have no physical counterpart in the real world. We call them *formal* in that case. Formal Splits are operations of a special kind, that have to be considered internal details of ``wms-core``, that are not guaranteed to exist in the future. Formal Splits can always be reverted with :class:`Aggregate <.aggregate.Aggregate>` Operations, but only some physical Splits can be reverted, depending on the PhysObj Type. .. seealso:: :class:`Model.Wms.PhysObj.Type <anyblok_wms_base.quantity.physobj.Type>` for a full discussion including use-cases of formal and physical splits and reversal of the latter. In the formal case, we've decided to represent this as an Operation for the sake of consistency, and especially to avoid too much special cases in implementation of various concrete Operations. The benefit is that Splits appear explicitely in the history, and this helps implementing :ref:`history manipulating methods <op_cancel_revert_obliviate>` a lot. The drawback is that we get a proliferation of PhysObj records, some of them even with a zero second lifespan, but even those could be simplified only for executed Splits. Splits are typically created and executed from :class:`Splitter Operations <.splitter.WmsSplitterOperation>`, and that explains the above-mentioned zero lifespans. """ TYPE = 'wms_split' """Polymorphic key""" id = Integer(label="Identifier", primary_key=True, autoincrement=False, foreign_key=Operation.use('id').options(ondelete='cascade')) quantity = Decimal() """The quantity to split.""" def specific_repr(self): return ("input={self.input!r}, " "quantity={self.quantity}").format(self=self) def after_insert(self): self.registry.flush() avatar = self.input phobj = avatar.obj qty = self.quantity new_phobj = dict( type=phobj.type, code=phobj.code, properties=phobj.properties, ) new_avatars = dict( location=avatar.location, outcome_of=self, dt_from=self.dt_execution, dt_until=avatar.dt_until, ) avatar.dt_until = self.dt_execution if self.state == 'done': avatar.update(state='past') new_avatars['state'] = 'present' else: new_avatars['state'] = 'future' return tuple( avatar.insert( obj=phobj.insert(quantity=new_qty, **new_phobj), **new_avatars) for new_qty in (qty, phobj.quantity - qty)) @property def wished_outcome(self): """Return the PhysObj record with the wished quantity. This is only one of :attr:`outcomes <anyblok_wms_base.core.operation.base.Operation.outcomes>` :rtype: :class:`Wms.PhysObj <anyblok_wms_base.core.physobj.PhysObj>` """ PhysObj = self.registry.Wms.PhysObj Avatar = PhysObj.Avatar # in case the split is exactly in half, there's no difference # between the two records we created, let's pick any. outcome = (Avatar.query().join(Avatar.obj) .filter(Avatar.outcome_of == self, PhysObj.quantity == self.quantity) .first()) if outcome is None: raise OperationError(self, "The split outcomes have disappeared") return outcome def check_execute_conditions(self): """Call the base class's version and check that quantity is suitable. """ super(Split, self).check_execute_conditions() phobj = self.input.obj if self.quantity > phobj.quantity: raise OperationQuantityError( self, "Can't execute {op}, whose quantity {op.quantity} is greater " "than on its input {phobj}, " "although it's been successfully planned.", op=self, phobj=self.input) def execute_planned(self): for outcome in self.outcomes: outcome.update(state='present', dt_from=self.dt_execution) self.registry.flush() self.input.update(state='past', dt_until=self.dt_execution) self.registry.flush() def is_reversible(self): """Reversibility depends on the relevant PhysObj Type. See :meth:`on Model.PhysObj.Type <anyblok_wms_base.core.physobj.Type.is_split_reversible>` """ return self.input.obj.type.is_split_reversible() def plan_revert_single(self, dt_execution, follows=()): Wms = self.registry.Wms Avatars = Wms.PhysObj.Avatar # here in that case, that's for multiple operations # in_ is not implemented for Many2Ones reason_ids = set(f.id for f in follows) to_aggregate = (Avatars.query() .filter(Avatars.outcome_of_id.in_(reason_ids)) .all()) to_aggregate.extend(self.leaf_outcomes()) return Wms.Operation.Aggregate.create(inputs=to_aggregate, dt_execution=dt_execution, state='planned') def obliviate_single(self): """Remove the created PhysObj in addition to base class operation. The base class would only take care of the created Avatars """ outcomes_objs = [o.obj for o in self.outcomes] super(Split, self).obliviate_single() for obj in outcomes_objs: obj.delete()