Started using 'sqlservice'. Added support for many new tables.

This commit is contained in:
Marcus Lindvall 2019-07-04 14:25:42 +02:00
parent 9b7d7db996
commit b77a7069ce
12 changed files with 390 additions and 125 deletions

View file

@ -1,11 +1,12 @@
"""
Define an Abstract Base Class (ABC) for models
"""
from datetime import datetime
from decimal import Decimal
from sqlalchemy import inspect
from sqlalchemy.sql.expression import and_
from sqlalchemy.orm.collections import InstrumentedList
from sqlalchemy.ext.hybrid import hybrid_property
from sqlservice import ModelBase
from pyjeeves import logging
@ -14,19 +15,25 @@ from . import db
logger = logging.getLogger("PyJeeves." + __name__)
class RawBaseModel():
class RawBaseModel(ModelBase):
""" Generalize __init__, __repr__ and to_json
Based on the models columns , ForetagKod=1"""
print_only = () # First filter
print_filter = () # Second filter
to_json_filter = () # Only json filter
column_map = {}
__to_dict_filter__ = []
__to_dict_only__ = ()
__column_map__ = {}
__table_args__ = {
'extend_existing': True
}
__dict_args__ = {
'adapters': {
# datetime: lambda value, col, *_: value.strftime('%Y-%m-%d'),
Decimal: lambda value, col, *_: "{:.2f}".format(value)
}
}
@classmethod
def _base_filters(self, obj, filters=and_()):
# This method provides base filtering, additional filtering can be done in subclasses
@ -38,63 +45,51 @@ class RawBaseModel():
filters
)
def __repr__(self):
""" Define a base way to print models
Columns inside `print_filter` are excluded """
return '%s(%s)' % (self.__class__.__name__, {
column: value
for column, value in self._to_dict().items()
if column not in self.print_filter
})
@staticmethod
def _to_json_types(value):
if isinstance(value, datetime):
return value.strftime('%Y-%m-%d')
if isinstance(value, Decimal):
return "%.2f" % value
try:
if isinstance(value, InstrumentedList):
return [x.json for x in value]
if type(value).__module__ != 'builtins': # Perhaps == builtin?
return value.json
except AttributeError:
logger.debug(str(type(value)) + " was not converted to jsonifyable type")
return None
return value
@property
def json(self):
""" Define a base way to jsonify models
Columns inside `to_json_filter` are excluded
Columns inside `to_json_only_filter` are only included """
return {
column: RawBaseModel._to_json_types(value)
# if not isinstance(value, datetime) else value.strftime('%Y-%m-%d')
# if type(value).__module__ != self.__module__ # Perhaps == builtin?
# else value.json # Convert instances to json if same module
for column, value in self._to_dict().items()
if column not in self.to_json_filter
}
def _to_dict(self):
""" This would more or less be the same as a `to_json`
But putting it in a "private" function
Allows to_json to be overriden without impacting __repr__
Or the other way around
And to add filter lists """
return {
self._map_columns(column.key): getattr(self, column.key)
for column in inspect(self.__class__).attrs
if not self.print_only or column.key in self.print_only
}
def _map_columns(self, key):
if key in self.column_map:
return self.column_map[key]
if key in self.__column_map__:
return self.__column_map__[key]
return key
def descriptors_to_dict(self):
"""Return a ``dict`` that maps data loaded in :attr:`__dict__` to this
model's descriptors. The data contained in :attr:`__dict__` represents
the model's state that has been loaded from the database. Accessing
values in :attr:`__dict__` will prevent SQLAlchemy from issuing
database queries for any ORM data that hasn't been loaded from the
database already.
Note:
The ``dict`` returned will contain model instances for any
relationship data that is loaded. To get a ``dict`` containing all
non-ORM objects, use :meth:`to_dict`.
Returns:
dict
"""
descriptors = self.descriptors()
return { # Expose hybrid_property extension
**{key: getattr(self, key) for key in descriptors.keys()
if isinstance(descriptors.get(key), hybrid_property)},
# and return all items included in descriptors
**{key: value for key, value in self.__dict__.items()
if key in descriptors}}
def to_dict(self):
rv = super().to_dict()
if self.__to_dict_only__:
return {
self._map_columns(key): rv[key]
for key in rv
if key in self.__to_dict_only__
}
for _filter in self.__to_dict_filter__:
rv.pop(_filter)
return rv
def merge(self):
db.raw_session.merge(self)
return self

View file

@ -10,9 +10,10 @@ from sqlalchemy.schema import MetaData, ForeignKey, Column
from sqlalchemy.orm import relationship
from sqlalchemy.types import Integer, String
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.sql.expression import and_
from sqlalchemy.exc import OperationalError
# from pyjeeves.session import raw_engine
from . import db
from pyjeeves import logging
@ -22,64 +23,153 @@ logger = logging.getLogger("PyJeeves." + __name__)
logger.info("Reading Jeeves DB structure")
meta = MetaData()
meta.reflect(bind=db.raw_session.connection(), only=['ar', 'ars', 'fr', 'kus', 'oh', 'lp', 'vg'])
# Table('fr', meta, implicit_returning=False)
try:
meta.reflect(bind=db.raw_session.connection(),
only=['ar', 'ars', 'xae', 'xare', 'fr', 'kus', 'x1k',
'oh', 'lp', 'vg', 'xp', 'xm', 'prh', 'prl'])
except OperationalError as e:
logger.error("Failed to read Jeeves DB structure")
raise e
Base = automap_base(cls=db.Model, name='Model', metadata=meta)
class ProductClass(Base, RawBaseModel):
__tablename__ = 'xp'
__column_map__ = {'ArtProdKlass': 'ProductClassNumber', 'ArtProdklBeskr': 'ProductClassName'}
__to_dict_only__ = ('ArtProdKlass', 'ArtProdklBeskr')
# print_filter = ('Articles', 'articles_collection')
class ArticleClass(Base, RawBaseModel):
__tablename__ = 'xm'
__column_map__ = {'ArtKod': 'ArticleClassNumber', 'ArtTypBeskr': 'ArticleClassName'}
__to_dict_only__ = ('ArtKod', 'ArtTypBeskr')
# print_filter = ('Articles', 'articles_collection')
class CommodityGroup(Base, RawBaseModel):
__tablename__ = 'vg'
column_map = {'VaruGruppKod': 'CommodityGroupNumber', 'VaruGruppBeskr': 'CommodityGroupName'}
print_only = ('VaruGruppKod', 'VaruGruppBeskr')
__column_map__ = {'VaruGruppKod': 'CommodityGroupNumber',
'VaruGruppBeskr': 'CommodityGroupName'}
__to_dict_only__ = ('VaruGruppKod', 'VaruGruppBeskr', 'ArticleClass')
print_filter = ('Articles', 'articles_collection')
# to_json_filter = ('Articles', 'articles_collection')
ArtKod = Column(Integer, ForeignKey('xm.ArtKod'), primary_key=True)
ArticleClass = relationship(ArticleClass)
class ArticleAlternativeUnit(Base, RawBaseModel):
__tablename__ = 'xae'
__column_map__ = {'AltEnhetKod': 'UnitCode', 'AltEnhetBeskr': 'UnitName',
'AltEnhetOmrFaktor': 'DefaultUnitConv'}
__to_dict_only__ = ('AltEnhetBeskr', 'AltEnhetOmrFaktor')
class ArticleUnit(Base, RawBaseModel):
__tablename__ = 'xare'
__column_map__ = {'ArtNr': 'ArticleNumber',
'AltEnhetKod': 'UnitCode', 'AltEnhetOmrFaktor': 'UnitConv',
'AltEnhetOrderStd': 'DefaultSalesUnit'}
__to_dict_only__ = ('AltEnhetKod', 'AltEnhetOmrFaktor',
'AltEnhetOrderStd', 'ArticleAlternativeUnit')
ArtNr = Column(String, ForeignKey('ar.ArtNr'), primary_key=True)
AltEnhetKod = Column(Integer, ForeignKey('xae.AltEnhetKod'), primary_key=True)
ArticleAlternativeUnit = relationship(ArticleAlternativeUnit)
class ArticleBalance(Base, RawBaseModel):
__tablename__ = 'ars'
column_map = {'LagSaldo': 'Balance',
'LagResAnt': 'ReservedBalance',
'LagsaldoAltEnh': 'BalanceAlternative',
'LagResAntAltEnh': 'ReservedAlternativeBalance',
'LagStalle': 'StorageLocationNumber'}
print_only = ('LagSaldo',
'LagResAnt',
'LagsaldoAltEnh',
'LagResAntAltEnh',
'LagStalle')
# print_filter = ('Articles', 'articles_collection')
# to_json_filter = ('Articles', 'articles_collection')
__column_map__ = {'LagSaldo': 'Balance',
'LagResAnt': 'ReservedBalance',
'LagsaldoAltEnh': 'BalanceAlternative',
'LagResAntAltEnh': 'ReservedAlternativeBalance',
'LagStalle': 'StorageLocationNumber'}
__to_dict_only__ = ('LagSaldo',
'LagResAnt',
'LagsaldoAltEnh',
'LagResAntAltEnh',
'LagStalle')
# print_filter = ('Article', 'articles_collection')
# to_json_filter = ('Article', 'articles_collection')
ArtNr = Column(Integer, ForeignKey('ar.ArtNr'), primary_key=True)
class Articles(Base, RawBaseModel):
class Article(Base, RawBaseModel):
__tablename__ = 'ar'
column_map = {'ArtNr': 'ArticleNumber',
'ArtBeskr': 'ArticleName',
'LagSaldoArtikel': 'Balance',
'EnhetsKod': 'Unit',
'ArtListPris': 'ListPrice'}
print_only = (
__column_map__ = {'ArtNr': 'ArticleNumber',
'ArtBeskr': 'ArticleName',
'ArtBeskrSpec': 'ArticleSpec',
'Edit': 'ArticleLongSpec',
'LagSaldoArtikel': 'UnitBalance',
'EnhetsKod': 'Unit',
'ArtListPris': 'UnitListPrice',
'Extra1': 'WholeSaleUnit'}
__to_dict_only__ = (
'ArtNr',
'ArtBeskr',
'ArtBeskrSpec',
'Edit',
'CommodityGroup',
'ProductClass',
'ArticleClass',
'ArticleBalance',
'EnhetsKod',
'LagSaldoArtikel',
'RowCreatedDt',
'ArtListPris')
'ArtListPris',
'PictureFileName',
'UnitListPrice',
'Extra1',
'ListPrice',
'Balance')
ArtNr = Column(Integer, primary_key=True)
VaruGruppKod = Column(Integer, ForeignKey('vg.VaruGruppKod'), primary_key=True)
ArtProdKlass = Column(Integer, ForeignKey('xp.ArtProdKlass'), primary_key=True)
ArtKod = Column(Integer, ForeignKey('xm.ArtKod'), primary_key=True)
CommodityGroup = relationship(CommodityGroup)
CommodityGroup = relationship(CommodityGroup, lazy='joined')
ProductClass = relationship(ProductClass, lazy='joined')
ArticleClass = relationship(ArticleClass, lazy='joined')
ArticleBalance = relationship(ArticleBalance)
ArticleUnit = relationship(ArticleUnit)
def get_unit_conv(self):
if self.ArtFsgForp:
return self.ArtFsgForp
for unit in self.ArticleUnit:
if unit.AltEnhetOrderStd == "1":
if unit.AltEnhetOmrFaktor:
return unit.AltEnhetOmrFaktor
else:
return unit.ArticleAlternativeUnit.AltEnhetOmrFaktor
return 1
@hybrid_property
def ListPrice(self):
try:
return self.ArtListPris * self.get_unit_conv()
except TypeError:
logger.debug("NoneType error, %s" % self.ArtNr)
@hybrid_property
def Balance(self):
try:
return self.LagSaldoArtikel / self.get_unit_conv()
except TypeError:
logger.debug("NoneType error, %s" % self.ArtNr)
@classmethod
def _base_filters(self, obj):
return RawBaseModel._base_filters(
@ -88,17 +178,102 @@ class Articles(Base, RawBaseModel):
)
class Companies(Base, RawBaseModel):
class Company(Base, RawBaseModel):
__tablename__ = 'fr'
column_map = {'FtgNr': 'CompanyNumber', 'FtgNamn': 'CompanyName'}
print_only = ('CompanyNumber', 'CompanyName')
__column_map__ = {'FtgNr': 'CompanyNumber', 'FtgNamn': 'CompanyName'}
__to_dict_only__ = ('FtgNr', 'FtgNamn', 'Customer')
FtgNr = Column(String, primary_key=True)
Customer = relationship('Customer', uselist=False, back_populates='Company', lazy='joined')
class CustomerCategory(Base, RawBaseModel):
__tablename__ = 'x1k'
KundKategoriKod = Column(Integer, primary_key=True)
class Customer(Base, RawBaseModel):
__tablename__ = 'kus'
__column_map__ = {'FtgNr': 'CompanyNumber', 'kundkategorikod': 'CustomerCategoryCode',
'PrisListaKundSpec': 'PriceListPrimary', 'PrisLista': 'PriceListSecondary'}
__to_dict_only__ = ('kundkategorikod', 'PriceList', 'PriceListCommon', 'CustomerCategory',
'PrisLista', 'PrisListaKundSpec')
FtgNr = Column(String, ForeignKey('fr.FtgNr'), primary_key=True)
KundKategoriKod = Column(Integer, ForeignKey('x1k.KundKategoriKod'))
PrisLista = Column(Integer, ForeignKey('prh.PrisLista'))
PrisListaKundSpec = Column(Integer, ForeignKey('prh.PrisLista'))
Company = relationship("Company", back_populates="Customer")
PriceList = relationship("PriceList", uselist=False,
lazy='joined', foreign_keys='PriceList.FtgNr')
PriceListCommon = relationship("PriceList", uselist=False,
foreign_keys='PriceList.PrisLista',
primaryjoin="Customer.PrisLista==PriceList.PrisLista")
KundKategori = relationship("CustomerCategory")
@hybrid_property
def CustomerCategory(self):
return self.KundKategori.KundKatBeskr
class PriceList(Base, RawBaseModel):
__tablename__ = 'prh'
__column_map__ = {'PrisListaBeskr': 'Description', 'PrisLista': 'PriceListNumber',
'MarkUpBelopp': 'PriceFactor'}
__to_dict_only__ = ('PrisListaBeskr', 'PrisLista', 'PriceListItems', 'MarkUpBelopp')
PrisLista = Column(Integer, primary_key=True)
FtgNr = Column(String, ForeignKey('kus.FtgNr'))
Customer = relationship('Customer', uselist=False, foreign_keys='Customer.PrisListaKundSpec')
PriceListItems = relationship('PriceListItem', back_populates="PriceList", lazy='joined')
class PriceListItem(Base, RawBaseModel):
__tablename__ = 'prl'
__column_map__ = {'ArtNr': 'ArticleNumber', 'vb_pris': 'UnitPrice',
'MarkUpBelopp': 'UnitPriceFactor', 'NollFaktor': 'NullPriceAllowed'}
__to_dict_only__ = ('ArtNr', 'vb_pris', 'MarkUpBelopp', 'NollFaktor', 'Price')
__to_dict_filter__ = ['PriceList']
# Do not serialize price list relationship
__dict_args__ = {
'adapters': {
**{
PriceList: None
},
**RawBaseModel.__dict_args__['adapters']
}
}
PrisLista = Column(Integer, ForeignKey('prh.PrisLista'), primary_key=True)
ArtNr = Column(Integer, ForeignKey('ar.ArtNr'), primary_key=True)
PriceList = relationship('PriceList', uselist=False)
Article = relationship(Article)
# TODO: Could likely be optimized by getting all articles in one query and mangled in repo
@hybrid_property
def Price(self):
if not self.vb_pris and not self.MarkUpBelopp:
return (
(self.Article.ArtListPris + self.PriceList.MarkUpBelopp) *
self.Article.get_unit_conv())
if self.vb_pris:
return self.vb_pris * self.Article.get_unit_conv()
else:
return (
(self.Article.ArtListPris + self.MarkUpBelopp) *
self.Article.get_unit_conv())
Base.prepare()
# Base companies for cusomters and suppliers
Customers = Base.classes.kus # Customer information
Orders = Base.classes.oh # Orders by customers
Order = Base.classes.oh # Orders by customers
DelivLoc = Base.classes.lp # Connections between a delivery company and customer company