class InventorySession(db.Model): """ InventorySession is the representation of inventory at a certain moment. Typically inventories at warehouse are done at regular intervals. During an inventory session the current stock for each part in the warehouse get logged. Those inventory sessions are used to re-count the stock in case some stock movement didn't get logged in the system correctly. The creation of a new InventorySession will restart all the previous inventory listing done in the past but not delete them. New Inventory are always associated to the most recent InventorySession. """ __tablename__ = "inventory_session" id = db.Column(db.Integer, primary_key=True, autoincrement=True) entries = relationship("Inventory", back_populates="session", passive_deletes="ALL") """List of every inventory entry related to the session.""" created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow) @staticmethod def last(): """ Return the last session added to the database. """ return InventorySession.query.order_by( InventorySession.created_at.desc()).first() def to_dict(self): return {"id": self.id, "created_at": self.created_at}
class PrinterTransaction(db.Model): """ The `PrinterTransaction` model code representing the messages sent to a :class:`despinassy.Printer.Printer`. The transaction of a printer can either be control messages or print query to output content like parts from the printer. """ __tablename__ = "printer_transaction" id = db.Column(db.Integer, primary_key=True, autoincrement=True) printer_id = db.Column(db.Integer, db.ForeignKey("printer.id")) printer = relationship("Printer") """:class:`despinassy.Printer.Printer` where the transaction happened""" # part_id = db.Column(db.Integer, db.ForeignKey('part.id'), unique=True) # part = relationship('Part') destination = db.Column(db.String(50)) origin = db.Column(db.Enum(IpcOrigin), nullable=False) """ Device that created this transaction. See :class:`despinassy.ipc.IpcOrigin` for more information. """ device = db.Column(db.String(50)) """ String precising the origin of the originator of the transaction. """ msg_type = db.Column(db.Integer, default=IpcMessageType.PRINT) """ Type of the message received by the printer. See :class:`despinassy.ipc.IpcOrigin` for more information. """ barcode = db.Column(db.String(50), nullable=False) """Barcode of the part the message refer to""" name = db.Column(db.String(120), nullable=False) """Name of the part the message refer to""" number = db.Column(db.Integer, default=1) """Number of output required by the printer""" created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow) def to_dict(self): return { "id": self.id, "barcode": self.barcode, "name": self.name, "number": self.number, "origin": self.origin, "device": self.device, "created_at": self.created_at, }
class Channel(db.Model): __tablename__ = "channel" id = db.Column(db.Integer, primary_key=True, autoincrement=True) name = db.Column(db.String(128), unique=True, index=True) printers = relationship("Printer", back_populates="redis") scanners = relationship("Scanner", back_populates="redis") created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow) updated_at = db.Column(db.DateTime, onupdate=datetime.datetime.utcnow) def __repr__(self): return "<Channel %r>" % (self.name) def __print__(self): return self.name def __str__(self): return self.name def to_dict(self): return { "id": self.id, "name": self.name, }
class ScannerTransaction(db.Model): """ The `ScannerTransaction` model code representing the messages sent by the :class:`despinassy.Scanner.Scanner`. """ __tablename__ = "scanner_transaction" id = db.Column(db.Integer, primary_key=True, autoincrement=True) scanner_id = db.Column(db.Integer, db.ForeignKey("scanner.id")) scanner = relationship("Scanner") """:class:`despinassy.Scanner.Scanner` origin of the transaction""" mode = db.Column(db.Enum(ScannerModeEnum), nullable=False) """ Mode of the :class:`despinassy.Scanner.Scanner` at the moment of the transaction """ quantity = db.Column(db.Float, default=1) """Quantity attached to the value logged by this transaction""" value = db.Column(db.String(50), nullable=False) """Input or scanned value""" created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow) def to_dict(self, full=False): if full: return { "id": self.id, "scanner": self.scanner_id, "mode": int(self.mode), "quantity": self.quantity, "value": self.value, "created_at": self.created_at, } else: return { "id": self.id, "mode": int(self.mode), "quantity": self.quantity, "value": self.value, "created_at": self.created_at, }
class Part(db.Model): """ The `Part` model code. Each part get associated with a unique `barcode` and a familiar `name`. """ __tablename__ = "part" id = db.Column(db.Integer, primary_key=True, autoincrement=True) barcode = db.Column(db.String(128), unique=True, index=True) """Unique barcode representation of the part""" name = db.Column(db.String(256)) """Familiar name of the part""" counter = db.Column(db.Integer, default=0) """Count of the number of time the `Part` has been printed""" inventories = relationship( "Inventory", back_populates="part", passive_deletes="ALL", ) """ List of related inventory entries for this part. See :class:`despinassy.Inventory` for more information about inventory. More than one entry can exist for the same part for different :class:`despinassy.InventorySession`. """ hidden = db.Column(db.Boolean, default=False) """ Is the part hidden. This is used to differentiate the currently in use parts from the one that got deleted without having to delete the :class:`despinassy.Inventory` entry related to this part. This boolean value allow API to list all the existing non hidden parts easily. """ created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow) updated_at = db.Column(db.DateTime, onupdate=datetime.datetime.utcnow) @validates("barcode") def validate_barcode(self, key, value): max_len = getattr(self.__class__, key).prop.columns[0].type.length if value and len(value) > max_len: raise ArgumentError('"barcode" value is too long') return value @validates("name") def validate_name(self, key, value): max_len = getattr(self.__class__, key).prop.columns[0].type.length if value and len(value) > max_len: return value[:max_len] return value def __repr__(self): return "<Part %r:%r>" % (self.name, self.barcode) def printed(self, number=1): self.counter += number def to_dict(self): return { "id": self.id, "barcode": self.barcode, "name": self.name, "counter": self.counter, } @staticmethod def _import_csv_content(strio: io.TextIOWrapper, csv_map=None, delimiter=",", **kwargs): def _get_column_max_len(col): # TODO verify column is of string type return getattr(Part, col).prop.columns[0].type.length def _truncate_column_content(column, content): max_len = _get_column_max_len(column) if len(content) > max_len: return content[:max_len] return content csv_reader = csv.DictReader(strio, delimiter=delimiter, **kwargs) if csv_map is None: # if no csv_map use the column with matching name. csv_map = {} for column in inspect(Part).columns.keys(): if column in csv_reader.fieldnames: csv_map[column] = column if ("name" not in csv_map) or ("barcode" not in csv_map): # TODO Log error return # TODO For now only importing 'name' and 'barcode' is supported _col_name = csv_map["name"] _col_barcode = csv_map["barcode"] tuple_parts = {(x[_col_name], x[_col_barcode]) for x in csv_reader if x.get(_col_name) and x.get(_col_barcode)} # retrieve the 'parts' present in the db and in the '.csv' # cond = tuple_(Part.name, Part.barcode).in_(list(tuple_parts)) # TODO if postgres cond = or_(*(and_(Part.name == x, Part.barcode == y) for (x, y) in tuple_parts)) in_db = Part.query.filter(cond) in_db.update({"hidden": False}) to_remove = Part.query.filter(~cond) to_remove.update({"hidden": True}) if in_db.count() > 0: # TODO add the part tuple_parts = tuple_parts - {(x.name, x.barcode) for x in in_db.all()} parts = [{ "name": _truncate_column_content("name", name), "barcode": _truncate_column_content("barcode", barcode), } for (name, barcode) in tuple_parts] db.session.bulk_insert_mappings(Part, parts) db.session.commit() @staticmethod def import_csv(filename, csv_map=None, encoding="latin1"): """Perform a mass import of a '.csv' file containing parts. :param filename: The location of the '.csv' file that containing the parts. This '.csv' file will be read and should contain a list of parts with a header containing the name of the column. :param csv_map: dictionary containing the name of the :class:`despinassy.Part` column as key and the name of the '.csv' header column name as attribute. This argument is also used to select the column of the '.csv' that will be imported. If no csv_map is provided the function will assume the '.csv' header thath match a column name present in the :class:`despinassy.Part` table will be used. :param encoding: The encoding of the filename that will be read. The encoding 'latin1' is used by default. """ if not os.path.exists(filename): raise FileNotFoundError with open(filename, mode="r", encoding=encoding, errors="ignore") as csv_file: Part._import_csv_content(csv_file, csv_map)
class Inventory(db.Model): """ The Inventory model code associate a number and a unit to an existing :class:`despinassy.Part`. Inventory represent the current stock of a :class:`despinassy.Part` at a given time. """ __tablename__ = "inventory" __table_args__ = (db.UniqueConstraint("part_id", "session_id"), ) id = db.Column(db.Integer, primary_key=True, autoincrement=True) quantity = db.Column(db.Float, default=0) """Current quantity of a :class:`despinassy.Part` in stock""" unit = db.Column(db.Enum(InventoryUnitEnum), default=InventoryUnitEnum.PIECES) """The unit of quantity""" part_id = db.Column(db.Integer, db.ForeignKey("part.id", ondelete="CASCADE")) part = relationship("Part") """Part associated with this inventory entry""" session_id = db.Column( db.Integer, db.ForeignKey("inventory_session.id", ondelete="CASCADE")) session = relationship("InventorySession") """Session associated with this inventory entry""" created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow) def __init__(self, **kwargs): kwargs["session"] = InventorySession.query.order_by( InventorySession.created_at.desc()).first() super().__init__(**kwargs) def __repr__(self): return "<Inventory id=%i count=%i barcode='%s'>" % ( self.id, self.counter, self.part.barcode, ) def add(self, number=1): self.quantity += int(number) def to_dict(self): return { "id": self.id, "session": self.session_id, "part": self.part.to_dict(), "quantity": self.quantity, "unit": str(self.unit), } @staticmethod def last_session_entries(): """ Return the inventory entries from the last session. """ last_session = InventorySession.last() return Inventory.query.filter(Inventory.session == last_session) @staticmethod def archive(): """ Archive the current inventory by creating a new :class:`despinassy.inventory.InventorySession`. """ db.session.add(InventorySession()) db.session.commit() @staticmethod def retrieve_inventory_from_barcode(barcode): return (db.session.query(Inventory).join(Part).filter( Part.barcode == barcode).first()) @staticmethod def _export_csv(delimiter=","): strio = io.StringIO(newline=None) columns = [ "id", "part_name", "part_barcode", "quantity", "unit", "created_at", "updated_at", ] writer = csv.DictWriter(strio, fieldnames=columns, delimiter=delimiter, lineterminator="\n") writer.writeheader() for i in Inventory.last_session_entries().all(): row = { "id": i.id, "part_name": i.part.name, "part_barcode": i.part.barcode, "quantity": i.quantity, "unit": i.unit, "created_at": str(i.created_at), "updated_at": str(i.updated_at), } writer.writerow(row) return strio @staticmethod def export_csv(path): """Export the inventory entries to a '.csv' file :param path: The location of the '.csv' file to save """ with open(path, "w") as csvfile: csvfile.write(Inventory._export_csv().getvalue())
class Printer(db.Model): """ The `Printer` model code. Printers entry are devices that can output parts in a defined dialect. This model holds the information about this output device. A `Printer` can either be something virtual that will just output the result to a console or a physical device like a Zebra sticker printer. """ __tablename__ = "printer" id = db.Column(db.Integer, primary_key=True, autoincrement=True) type = db.Column(db.Enum(PrinterTypeEnum), nullable=False) """ Type of printer device. See :class:`despinassy.Printer.PrinterTypeEnum` for more information. """ available = db.Column(db.Boolean) """ Whether or not the `Printer` is currently available to print something. For instance if a printer of type `PrinterTypeEnum.STATIC` is not connected this boolean will be listed as false. """ width = db.Column(db.Integer) """Width of the output""" height = db.Column(db.Integer) """Height of the output""" dialect = db.Column(db.Enum(PrinterDialectEnum), nullable=False) """ Print form of the output of the printer. See :class:`despinassy.Printer.PrinterDialectEnum` for more information. """ name = db.Column(db.String(50), nullable=False) """User defined common name for this printer""" redis_id = db.Column(db.Integer, db.ForeignKey("channel.id")) redis = relationship("Channel") """Channel the printer listen for incoming message""" settings = db.Column(db.JSON) """Settings dependant on printer type""" transactions = relationship( "PrinterTransaction", order_by="desc(PrinterTransaction.created_at)", back_populates="printer", ) """List of transaction sent to this printer""" hidden = db.Column(db.Boolean, default=False) """Is the printer hidden to the user.""" created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow) updated_at = db.Column(db.DateTime, onupdate=datetime.datetime.utcnow) @validates("redis") def validate_redis(self, key, value): c = Channel.query.filter(Channel.name == value) if c.count(): c = c.first() else: try: c = Channel(name=value) db.session.add(c) db.session.commit() except IntegrityError: db.session.rollback() c = Channel.query.filter(Channel.name == value).first() return c def to_dict(self, full=False): if full: return { "id": self.id, "type": self.type, "available": self.available, "width": self.width, "height": self.height, "dialect": self.dialect, "name": self.name, "redis": str(self.redis), "settings": json.loads(self.settings), "transactions": [t.to_dict() for t in self.transactions], "created_at": self.created_at, "updated_at": self.updated_at, "hidden": self.hidden, } else: return { "id": self.id, "type": self.type, "available": self.available, "width": self.width, "height": self.height, "dialect": self.dialect, "name": self.name, "redis": str(self.redis), "settings": json.loads(self.settings), "created_at": self.created_at, "updated_at": self.updated_at, "hidden": self.hidden, } def add_transaction(self, **kwargs): """Helper to create a new :class:`despinassy.Printer.PrinterTransaction` Someone should always use this helper function to create a new :class:`despinassy.Printer.PrinterTransaction` instead of creating one by hand. """ self.updated_at = datetime.datetime.utcnow() pt = PrinterTransaction(printer=self, **kwargs) return pt def __repr__(self): return "<Printer id=%i type=%i name='%s' redis='%s' settings='%s'>" % ( self.id, self.type, self.name, str(self.redis), self.settings, )
class Scanner(db.Model): """ The `Scanner` model code. Scanners are devices that can input/scan barcodes. Scanners can be physical device (USB, bluetooth) or virtual like the input from STDIN or the input from a webapp. """ __tablename__ = "scanner" id = db.Column(db.Integer, primary_key=True, autoincrement=True) type = db.Column(db.Enum(ScannerTypeEnum), nullable=False) """ Type of scanner device. See :class:`despinassy.Scanner.ScannerTypeEnum` for more information. """ mode = db.Column(db.Enum(ScannerModeEnum), default=ScannerModeEnum.PRINTMODE, nullable=False) """ Current mode of the scanner device. See :class:`despinassy.Scanner.ScannerModeEnum` for more information. """ available = db.Column(db.Boolean) """ Whether or not the `Scanner` is currently available to scan. For instance if a usb scanner is disconnected this boolean will be set to false. """ name = db.Column(db.String(50), unique=True) """User defined common name for this scanner""" redis_id = db.Column(db.Integer, db.ForeignKey("channel.id")) redis = relationship("Channel") """Channel the scanner send message to""" settings = db.Column(db.JSON) """Settings dependant on scanner type""" transactions = relationship( "ScannerTransaction", order_by="desc(ScannerTransaction.created_at)", back_populates="scanner", ) """List of transaction made by this scanner""" hidden = db.Column(db.Boolean, default=False) """Is the scanner hidden to the user.""" created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow) updated_at = db.Column(db.DateTime, onupdate=datetime.datetime.utcnow) @validates("redis") def validate_redis(self, key, value): if isinstance(value, str): c = Channel.query.filter_by(name=value).first() if c is None: try: c = Channel(name=value) db.session.add(c) db.session.commit() except IntegrityError: db.session.rollback() c = Channel.query.filter(Channel.name == value).first() elif isinstance(value, Channel): c = value else: raise Exception("Not valid redis") return c def to_dict(self, full=False): if full: return { "id": self.id, "type": self.type, "name": self.name, "redis": str(self.redis), "settings": json.loads(self.settings), "mode": self.mode, "available": self.available, "created_at": self.created_at, "updated_at": self.updated_at, "transactions": [t.to_dict() for t in self.transactions], "hidden": self.hidden, } else: return { "id": self.id, "type": self.type, "name": self.name, "redis": str(self.redis), "settings": json.loads(self.settings), "mode": self.mode, "available": self.available, "created_at": self.created_at, "updated_at": self.updated_at, "hidden": self.hidden, } def add_transaction(self, **kwargs): """Helper to create a new :class:`despinassy.Scanner.ScannerTransaction` Always use this helper function to create a new :class:`despinassy.Scanner.ScannerTransaction` instead of creating one by hand. """ self.updated_at = datetime.datetime.utcnow() st = ScannerTransaction(scanner=self, **kwargs) return st def __repr__(self): return "<Scanner id=%i type=%i name='%s' redis='%s' settings='%s'>" % ( self.id, self.type, self.name, str(self.redis), self.settings, )