Example #1
0
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}
Example #2
0
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,
        }
Example #3
0
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,
        }
Example #4
0
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,
            }
Example #5
0
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)
Example #6
0
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())
Example #7
0
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,
        )
Example #8
0
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,
        )