Stored procedure helpers. Order repo and model. More SQLService updates.

* A generic stored procedure helper added, and support for calling them.
* Order and OrderItem tables added, including helpers and calls to SP for creation and updates.
* Minor updates to other repositories.
This commit is contained in:
Marcus Lindvall 2019-08-30 12:09:10 +02:00
parent b77a7069ce
commit 0af38e286e
9 changed files with 730 additions and 138 deletions

View file

@ -6,29 +6,31 @@
Global objects Global objects
""" """
from pyjeeves import logging, config from pyjeeves import logging, config
from weakref import WeakValueDictionary
from sqlalchemy import create_engine, orm from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, scoped_session, Query, aliased from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.orm.exc import UnmappedClassError # from sqlalchemy.orm.exc import UnmappedClassError
from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta from pymssql import OperationalError
from sqlservice import SQLClient, SQLQuery
logger = logging.getLogger("PyJeeves." + __name__) logger = logging.getLogger("PyJeeves." + __name__)
class BaseFilterQuery(Query): class BaseFilterQuery(SQLQuery):
def get(self, ident): def get(self, ident):
# Override get() so that the flag is always checked in the # Override get() so that the flag is always checked in the
# DB as opposed to pulling from the identity map. - this is optional. # DB as opposed to pulling from the identity map. - this is optional.
return Query.get(self.populate_existing(), ident) return SQLQuery.get(self.populate_existing(), ident)
def __iter__(self): def __iter__(self):
return Query.__iter__(self.private()) return SQLQuery.__iter__(self.private())
def from_self(self, *ent): def from_self(self, *ent):
# Override from_self() to automatically apply # Override from_self() to automatically apply
# the criterion to. this works with count() and # the criterion to. this works with count() and
# others. # others.
return Query.from_self(self.private(), *ent) return SQLQuery.from_self(self.private(), *ent)
def private(self): def private(self):
# Fetch the model name and column list and apply model-specific base filters # Fetch the model name and column list and apply model-specific base filters
@ -49,94 +51,58 @@ class BaseFilterQuery(Query):
return self return self
class Model(object):
"""Baseclass for custom user models."""
#: the query class used. The :attr:`query` attribute is an instance
#: of this class. By default a :class:`BaseQuery` is used.
query_class = BaseFilterQuery
#: an instance of :attr:`query_class`. Can be used to query the
#: database for instances of this model.
query = None
class MetaBaseModel(DeclarativeMeta):
""" Define a metaclass for the BaseModel
Implement `__getitem__` for managing aliases """
def __init__(cls, *args):
super().__init__(*args)
cls.aliases = WeakValueDictionary()
def __getitem__(cls, key):
try:
alias = cls.aliases[key]
except KeyError:
alias = aliased(cls)
cls.aliases[key] = alias
return alias
class _QueryProperty(object):
def __init__(self, sa):
self.sa = sa
def __get__(self, obj, type):
try:
mapper = orm.class_mapper(type)
if mapper:
if type.__module__ == 'pyjeeves.models.raw':
return type.query_class(mapper, session=self.sa.raw_session())
else:
return type.query_class(mapper, session=self.sa.meta_session())
except UnmappedClassError:
return None
class DBConnector(object): class DBConnector(object):
"""This class is used to control the SQLAlchemy integration""" """This class is used to control the SQLAlchemy integration"""
def __init__(self, enabled_sessions=['raw'], metadata=None):
def __init__(self, enabled_clients=['raw'], metadata=None):
logger.info("Creating engines and sessionmakers") logger.info("Creating engines and sessionmakers")
self.raw_session, self.meta_session = self.create_scoped_session(enabled_sessions) self.raw, self.raw_engine = (self.raw_session() if 'raw' in enabled_clients else {})
self.Model = self.make_declarative_base(metadata) self.meta = (self.meta_session() if 'meta' in enabled_clients else {})
# self.Query = Query
@property def callproc(self, procedure="", params=[]):
def metadata(self): conn = self.raw_engine.raw_connection()
"""Returns the metadata"""
return self.Model.metadata
# @property with conn.cursor() as cursor:
# def _config(self): try:
# """Returns the configuration""" retval = cursor.callproc(procedure, params)
# return config() try:
cursor.nextset()
retval = cursor.fetchall()
except OperationalError:
logger.debug("Executed statement has no resultset")
def make_declarative_base(self, metadata=None): conn.commit()
"""Creates the declarative base."""
base = declarative_base(cls=Model, name='Model',
metadata=metadata,
metaclass=MetaBaseModel)
base.query = _QueryProperty(self)
return base
def create_scoped_session(self, sessions=[]): finally:
RawSession, MetaSession = None, None conn.close()
if 'raw' in sessions:
raw_engine = create_engine(
'mssql+pymssql://{user}:{pw}@{host}:{port}/{db}?charset=utf8'.format(
**config.config['databases']['raw']),
implicit_returning=False)
RawSession = scoped_session(sessionmaker(bind=raw_engine)) return retval
if 'meta' in sessions: def execute(self, operation=""):
meta_engine = create_engine( conn = self.raw_engine.raw_connection()
'mysql+pymysql://{user}:{pw}@{host}:{port}/{db}?charset=utf8mb4'.format(
**config.config['databases']['meta']))
MetaSession = scoped_session(sessionmaker(bind=meta_engine)) with conn.cursor(as_dict=True) as cursor:
try:
cursor.execute(operation)
results = cursor.fetchall()
finally:
conn.close()
return results
return RawSession, MetaSession def raw_session(self):
uri = 'mssql+pymssql://{user}:{pw}@{host}:{port}/{db}?charset=utf8'.format(
**config.config['databases']['raw'])
sql_client_config = {'SQL_DATABASE_URI': uri}
db = SQLClient(sql_client_config, query_class=BaseFilterQuery)
return db.session, db.engine
def meta_session(self):
meta_engine = create_engine(
'mysql+pymysql://{user}:{pw}@{host}:{port}/{db}?charset=utf8mb4'.format(
**config.config['databases']['meta']))
return scoped_session(sessionmaker(bind=meta_engine))

View file

@ -2,11 +2,17 @@
Define an Abstract Base Class (ABC) for models Define an Abstract Base Class (ABC) for models
""" """
from decimal import Decimal from decimal import Decimal
from datetime import datetime
from sqlalchemy.sql.expression import and_ from sqlalchemy.sql.expression import and_
from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.exc import OperationalError
from sqlservice import ModelBase from sqlalchemy.schema import MetaData, Column
from sqlalchemy.types import Integer
from sqlalchemy.orm.collections import InstrumentedList
from sqlservice import ModelBase, as_declarative
from pyjeeves import logging from pyjeeves import logging
@ -14,7 +20,19 @@ from . import db
logger = logging.getLogger("PyJeeves." + __name__) logger = logging.getLogger("PyJeeves." + __name__)
logger.info("Reading Jeeves DB structure")
meta = MetaData()
try:
meta.reflect(bind=db.raw.connection(),
only=['ar', 'ars', 'xae', 'xare', 'fr', 'kus', 'x1k',
'oh', 'orp', 'lp', 'vg', 'xp', 'xm', 'prh', 'prl'])
except OperationalError as e:
logger.error("Failed to read Jeeves DB structure")
raise e
@as_declarative(metadata=meta)
class RawBaseModel(ModelBase): class RawBaseModel(ModelBase):
""" Generalize __init__, __repr__ and to_json """ Generalize __init__, __repr__ and to_json
Based on the models columns , ForetagKod=1""" Based on the models columns , ForetagKod=1"""
@ -22,6 +40,7 @@ class RawBaseModel(ModelBase):
__to_dict_filter__ = [] __to_dict_filter__ = []
__to_dict_only__ = () __to_dict_only__ = ()
__column_map__ = {} __column_map__ = {}
__reversed_column_map__ = lambda self: {v: k for k, v in self.__column_map__.items()} # noqa
__table_args__ = { __table_args__ = {
'extend_existing': True 'extend_existing': True
@ -29,11 +48,19 @@ class RawBaseModel(ModelBase):
__dict_args__ = { __dict_args__ = {
'adapters': { 'adapters': {
# datetime: lambda value, col, *_: value.strftime('%Y-%m-%d'), datetime: lambda value, col, *_: value.strftime('%Y-%m-%d %H:%M'),
Decimal: lambda value, col, *_: "{:.2f}".format(value) Decimal: lambda value, col, *_: float(value) # "{:.2f}".format(value)
} }
} }
ForetagKod = Column(Integer, primary_key=True)
def __init__(self, data=None, **kargs):
if data:
data = self._map_keys(data)
self.update(data, **kargs)
# super(RawBaseModel, self).__init__(data=None, **kargs)
@classmethod @classmethod
def _base_filters(self, obj, filters=and_()): def _base_filters(self, obj, filters=and_()):
# This method provides base filtering, additional filtering can be done in subclasses # This method provides base filtering, additional filtering can be done in subclasses
@ -50,6 +77,44 @@ class RawBaseModel(ModelBase):
return self.__column_map__[key] return self.__column_map__[key]
return key return key
def _map_keys(self, data={}):
rv = {}
for key, value in self.__reversed_column_map__().items():
if key in data:
rv[value] = data[key]
for key, value in data.items():
if hasattr(self, key):
if key in self.relationships().keys():
rv[key] = self._map_relationship_keys(key, value)
else:
rv[key] = value
return rv
def _map_relationship_keys(self, field, value):
"""Get model relationships fields value. Almost a copy from SQLService ModelBase"""
relation_attr = getattr(self.__class__, field)
uselist = relation_attr.property.uselist
relation_class = relation_attr.property.mapper.class_
if uselist:
if not isinstance(value, (list, tuple)): # pragma: no cover
value = [value]
# Convert each value instance to relationship class.
value = [relation_class(val) if not isinstance(val, relation_class)
else val
for val in value]
elif value and isinstance(value, dict):
# Convert single value object to relationship class.
value = relation_class(value)
elif not value and isinstance(value, dict):
# If value is {} and we're trying to update a relationship
# attribute, then we need to set to None to nullify relationship
# value.
value = None
return value
def descriptors_to_dict(self): def descriptors_to_dict(self):
"""Return a ``dict`` that maps data loaded in :attr:`__dict__` to this """Return a ``dict`` that maps data loaded in :attr:`__dict__` to this
model's descriptors. The data contained in :attr:`__dict__` represents model's descriptors. The data contained in :attr:`__dict__` represents
@ -90,6 +155,18 @@ class RawBaseModel(ModelBase):
return rv return rv
def from_dict(self, data={}):
for key, value in self.__reversed_column_map__().items():
if key in data:
self[value] = data[key]
for key, value in data.items():
if hasattr(self, key):
if isinstance(self[key], InstrumentedList):
pass
else:
self[key] = value
return self
def merge(self): def merge(self):
db.raw_session.merge(self) db.raw_session.merge(self)
return self return self

View file

@ -5,50 +5,38 @@
Jeeves raw data models Jeeves raw data models
""" """
# from sqlalchemy import Column, String
from sqlalchemy.schema import MetaData, ForeignKey, Column from sqlalchemy.schema import ForeignKey, Column
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.types import Integer, String from sqlalchemy.types import Integer, String
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.sql.expression import and_ from sqlalchemy.sql.expression import and_
from sqlalchemy.exc import OperationalError
from . import db
from pyjeeves import logging from pyjeeves import logging
from .abc import RawBaseModel from .abc import RawBaseModel
from .sp_classes import OrderHead, OrderRow, PlaceOrder
import re
logger = logging.getLogger("PyJeeves." + __name__) logger = logging.getLogger("PyJeeves." + __name__)
logger.info("Reading Jeeves DB structure")
meta = MetaData()
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): class ProductClass(RawBaseModel):
__tablename__ = 'xp' __tablename__ = 'xp'
__column_map__ = {'ArtProdKlass': 'ProductClassNumber', 'ArtProdklBeskr': 'ProductClassName'} __column_map__ = {'ArtProdKlass': 'ProductClassNumber', 'ArtProdklBeskr': 'ProductClassName'}
__to_dict_only__ = ('ArtProdKlass', 'ArtProdklBeskr') __to_dict_only__ = ('ArtProdKlass', 'ArtProdklBeskr')
# print_filter = ('Articles', 'articles_collection') # print_filter = ('Articles', 'articles_collection')
class ArticleClass(Base, RawBaseModel): class ArticleClass(RawBaseModel):
__tablename__ = 'xm' __tablename__ = 'xm'
__column_map__ = {'ArtKod': 'ArticleClassNumber', 'ArtTypBeskr': 'ArticleClassName'} __column_map__ = {'ArtKod': 'ArticleClassNumber', 'ArtTypBeskr': 'ArticleClassName'}
__to_dict_only__ = ('ArtKod', 'ArtTypBeskr') __to_dict_only__ = ('ArtKod', 'ArtTypBeskr')
# print_filter = ('Articles', 'articles_collection') # print_filter = ('Articles', 'articles_collection')
class CommodityGroup(Base, RawBaseModel): class CommodityGroup(RawBaseModel):
__tablename__ = 'vg' __tablename__ = 'vg'
__column_map__ = {'VaruGruppKod': 'CommodityGroupNumber', __column_map__ = {'VaruGruppKod': 'CommodityGroupNumber',
'VaruGruppBeskr': 'CommodityGroupName'} 'VaruGruppBeskr': 'CommodityGroupName'}
@ -61,14 +49,14 @@ class CommodityGroup(Base, RawBaseModel):
ArticleClass = relationship(ArticleClass) ArticleClass = relationship(ArticleClass)
class ArticleAlternativeUnit(Base, RawBaseModel): class ArticleAlternativeUnit(RawBaseModel):
__tablename__ = 'xae' __tablename__ = 'xae'
__column_map__ = {'AltEnhetKod': 'UnitCode', 'AltEnhetBeskr': 'UnitName', __column_map__ = {'AltEnhetKod': 'UnitCode', 'AltEnhetBeskr': 'UnitName',
'AltEnhetOmrFaktor': 'DefaultUnitConv'} 'AltEnhetOmrFaktor': 'DefaultUnitConv'}
__to_dict_only__ = ('AltEnhetBeskr', 'AltEnhetOmrFaktor') __to_dict_only__ = ('AltEnhetBeskr', 'AltEnhetOmrFaktor')
class ArticleUnit(Base, RawBaseModel): class ArticleUnit(RawBaseModel):
__tablename__ = 'xare' __tablename__ = 'xare'
__column_map__ = {'ArtNr': 'ArticleNumber', __column_map__ = {'ArtNr': 'ArticleNumber',
'AltEnhetKod': 'UnitCode', 'AltEnhetOmrFaktor': 'UnitConv', 'AltEnhetKod': 'UnitCode', 'AltEnhetOmrFaktor': 'UnitConv',
@ -82,7 +70,7 @@ class ArticleUnit(Base, RawBaseModel):
ArticleAlternativeUnit = relationship(ArticleAlternativeUnit) ArticleAlternativeUnit = relationship(ArticleAlternativeUnit)
class ArticleBalance(Base, RawBaseModel): class ArticleBalance(RawBaseModel):
__tablename__ = 'ars' __tablename__ = 'ars'
__column_map__ = {'LagSaldo': 'Balance', __column_map__ = {'LagSaldo': 'Balance',
'LagResAnt': 'ReservedBalance', 'LagResAnt': 'ReservedBalance',
@ -100,7 +88,7 @@ class ArticleBalance(Base, RawBaseModel):
ArtNr = Column(Integer, ForeignKey('ar.ArtNr'), primary_key=True) ArtNr = Column(Integer, ForeignKey('ar.ArtNr'), primary_key=True)
class Article(Base, RawBaseModel): class Article(RawBaseModel):
__tablename__ = 'ar' __tablename__ = 'ar'
__column_map__ = {'ArtNr': 'ArticleNumber', __column_map__ = {'ArtNr': 'ArticleNumber',
@ -178,7 +166,7 @@ class Article(Base, RawBaseModel):
) )
class Company(Base, RawBaseModel): class Company(RawBaseModel):
__tablename__ = 'fr' __tablename__ = 'fr'
__column_map__ = {'FtgNr': 'CompanyNumber', 'FtgNamn': 'CompanyName'} __column_map__ = {'FtgNr': 'CompanyNumber', 'FtgNamn': 'CompanyName'}
__to_dict_only__ = ('FtgNr', 'FtgNamn', 'Customer') __to_dict_only__ = ('FtgNr', 'FtgNamn', 'Customer')
@ -188,13 +176,17 @@ class Company(Base, RawBaseModel):
Customer = relationship('Customer', uselist=False, back_populates='Company', lazy='joined') Customer = relationship('Customer', uselist=False, back_populates='Company', lazy='joined')
class CustomerCategory(Base, RawBaseModel): class DelivLoc(RawBaseModel):
__tablename__ = 'lp'
class CustomerCategory(RawBaseModel):
__tablename__ = 'x1k' __tablename__ = 'x1k'
KundKategoriKod = Column(Integer, primary_key=True) KundKategoriKod = Column(Integer, primary_key=True)
class Customer(Base, RawBaseModel): class Customer(RawBaseModel):
__tablename__ = 'kus' __tablename__ = 'kus'
__column_map__ = {'FtgNr': 'CompanyNumber', 'kundkategorikod': 'CustomerCategoryCode', __column_map__ = {'FtgNr': 'CompanyNumber', 'kundkategorikod': 'CustomerCategoryCode',
'PrisListaKundSpec': 'PriceListPrimary', 'PrisLista': 'PriceListSecondary'} 'PrisListaKundSpec': 'PriceListPrimary', 'PrisLista': 'PriceListSecondary'}
@ -218,10 +210,10 @@ class Customer(Base, RawBaseModel):
@hybrid_property @hybrid_property
def CustomerCategory(self): def CustomerCategory(self):
return self.KundKategori.KundKatBeskr return self.KundKategori.KundKatBeskr if self.KundKategori else ""
class PriceList(Base, RawBaseModel): class PriceList(RawBaseModel):
__tablename__ = 'prh' __tablename__ = 'prh'
__column_map__ = {'PrisListaBeskr': 'Description', 'PrisLista': 'PriceListNumber', __column_map__ = {'PrisListaBeskr': 'Description', 'PrisLista': 'PriceListNumber',
'MarkUpBelopp': 'PriceFactor'} 'MarkUpBelopp': 'PriceFactor'}
@ -234,7 +226,7 @@ class PriceList(Base, RawBaseModel):
PriceListItems = relationship('PriceListItem', back_populates="PriceList", lazy='joined') PriceListItems = relationship('PriceListItem', back_populates="PriceList", lazy='joined')
class PriceListItem(Base, RawBaseModel): class PriceListItem(RawBaseModel):
__tablename__ = 'prl' __tablename__ = 'prl'
__column_map__ = {'ArtNr': 'ArticleNumber', 'vb_pris': 'UnitPrice', __column_map__ = {'ArtNr': 'ArticleNumber', 'vb_pris': 'UnitPrice',
'MarkUpBelopp': 'UnitPriceFactor', 'NollFaktor': 'NullPriceAllowed'} 'MarkUpBelopp': 'UnitPriceFactor', 'NollFaktor': 'NullPriceAllowed'}
@ -272,8 +264,123 @@ class PriceListItem(Base, RawBaseModel):
self.Article.get_unit_conv()) self.Article.get_unit_conv())
Base.prepare() class Order(RawBaseModel):
__tablename__ = 'oh'
__column_map__ = {'OrderNr': 'OrderNumber', 'FtgNr': 'CompanyNumber',
'OrdDatum': 'OrderDate', 'OrdStat': 'OrderStatusCode',
'OrdLevAdr1': 'AddrName', 'OrdLevAdr2': 'AddrCO',
'OrdLevAdr3': 'AddrStreet', 'OrdLevAdrLandsKod': 'AddrCountry',
'KundBestNr': 'CustomerContact', 'KundRef2': 'CustomerReference',
'GodsMarke1': 'ShippingInfo', 'GodsMarke2': 'InternalInfo',
'TA_MailNotified': 'ShippingEmail', 'TA_PhonNotifiedNo': 'ShippingPhone',
'TA_SMSNotifiedNo': 'ShippingSMS', 'LevSattKod': 'ShippingTypeCode'}
__to_dict_only__ = ('OrderNr', 'FtgNr', 'OrdDatum', 'OrdStat', 'CompanyName', 'LevSattKod',
'OrdLevAdr1', 'OrdLevAdr2', 'OrdLevAdr3',
'OrdLevAdrLandsKod', 'KundBestNr', 'KundRef2', 'GodsMarke1',
'GodsMarke2', 'OrderItems', 'AddrPostalCode', 'AddrCity',
'TA_MailNotified', 'TA_PhonNotifiedNo', 'TA_SMSNotifiedNo')
# Base companies for cusomters and suppliers __dict_args__ = {
Order = Base.classes.oh # Orders by customers 'adapters': {
DelivLoc = Base.classes.lp # Connections between a delivery company and customer company **{
'OrdDatum': lambda ord_date, *_: ord_date.strftime("%Y-%m-%d"),
},
**RawBaseModel.__dict_args__['adapters']
}
}
OrderNr = Column(Integer, primary_key=True)
FtgNr = Column(String, ForeignKey('fr.FtgNr'))
Company = relationship('Company', uselist=False)
OrderItems = relationship('OrderItem', uselist=True, back_populates="Order", lazy='joined')
@hybrid_property
def CompanyName(self):
return self.Company.FtgNamn if self.Company else ""
@CompanyName.setter
def CompanyName(self, value):
return
@hybrid_property
def AddrPostalCode(self):
if not self.OrdLevAdr4:
return
s = re.split('(?!\d)\s(?!\d)', self.OrdLevAdr4)
return s[0] if len(s) > 1 else ''
@AddrPostalCode.setter
def AddrPostalCode(self, value):
self.OrdLevAdr4 = value + (self.OrdLevAdr4 if self.OrdLevAdr4 else '')
@hybrid_property
def AddrCity(self):
if not self.OrdLevAdr4:
return
s = re.split('(?!\d)\s(?!\d)', self.OrdLevAdr4, maxsplit=1)
return s[1] if len(s) > 1 else ''
@AddrCity.setter
def AddrCity(self, value):
self.OrdLevAdr4 = (self.OrdLevAdr4 if self.OrdLevAdr4 else '') + ' ' + value
def create(self, webusername=None):
# TODO: Extend with additional functionlity if desired.
self['OrderNr'], invoicing_possible = OrderHead(self['FtgNr'], webusername).callproc()
return self, invoicing_possible
def save(self, invoiced=False):
payment_method = 'invoice'
if not invoiced:
payment_method = 'card'
PlaceOrder(
self['FtgNr'], self['OrderNr'], payment_method, data=self.to_dict()).callproc()
return self
class OrderItem(RawBaseModel):
__tablename__ = 'orp'
__column_map__ = {'OrdRadNr': 'OrderRowNumber', 'vb_pris': 'UnitPrice',
'ArtNr': 'ArticleNumber', 'OrdAntal': 'UnitAmount',
'OrdAntalAltEnh': 'AltUnitAmount', 'AltEnhetKod': 'AltUnit'}
__to_dict_only__ = ('OrdRadNr', 'vb_pris', 'ArtNr', 'ArticleName', 'OrdAntal',
'OrdAntalAltEnh', 'AltEnhetKod')
# Do not serialize order relationship
__dict_args__ = {
'adapters': {
**{
Order: None,
},
**RawBaseModel.__dict_args__['adapters']
}
}
OrderNr = Column(Integer, ForeignKey('oh.OrderNr'), primary_key=True)
OrdRadNr = Column(Integer, primary_key=True)
OrdRadNrStrPos = Column(Integer, primary_key=True)
OrdRestNr = Column(Integer, primary_key=True)
ArtNr = Column(Integer, ForeignKey('ar.ArtNr'))
Order = relationship('Order', uselist=False)
Article = relationship(Article)
@hybrid_property
def ArticleName(self):
return self.Article.ArtBeskr if self.Article else ""
@ArticleName.setter
def ArticleName(self, value):
return
def save(self):
# TODO: Additional information may be returned if desired.
row_no = OrderRow(
company_no=self['FtgNr'], order_no=self['OrderNr'], item_no=self['ArtNr'],
qty=self['OrdAntal'], qty_alt_unit=self['OrdAntalAltEnh'],
alt_unit=self['AltEnhetKod'], pers_sign='marlin').callproc()
self['OrdRadNr'] = row_no
return self

View file

@ -0,0 +1,250 @@
# -*- coding: utf-8 -*-
import pymssql
from collections import OrderedDict
from pyjeeves.models import db
from pyjeeves import logging
# from datetime import datetime
# from decimal import Decimal
logger = logging.getLogger("PyJeeves." + __name__)
class StoredProcedure(OrderedDict):
__raw_params = {}
# https://www.mssqltips.com/sqlservertip/1669/generate-a-parameter-list-for-all-sql-server-stored-procedures-and-functions/ # noqa
query = """SELECT SCHEMA_NAME(SCHEMA_ID) AS [Schema],
SO.name AS [ObjectName],
SO.Type_Desc AS [ObjectType (UDF/SP)],
P.parameter_id AS [ParameterID],
P.name AS [ParameterName],
TYPE_NAME(P.user_type_id) AS [ParameterDataType],
P.max_length AS [ParameterMaxBytes],
P.is_output AS [IsOutPutParameter]
FROM sys.objects AS SO
INNER JOIN sys.parameters AS P
ON SO.OBJECT_ID = P.OBJECT_ID
WHERE SO.name LIKE '%Jeeves_Esales_%' OR
SO.name LIKE '%JAPP_spr_LogTrade_%' AND
SO.OBJECT_ID IN ( SELECT OBJECT_ID
FROM sys.objects
WHERE TYPE IN ('P','FN'))
ORDER BY [Schema], SO.name, P.parameter_id"""
logger.debug("Getting information about stored procedures from database")
for param in db.execute(query):
if param['ObjectName'] not in __raw_params:
__raw_params[param['ObjectName']] = OrderedDict()
param_name = param['ParameterName'][1:]
__raw_params[param['ObjectName']][param_name] = param
@classmethod
def get_params_for(cls, procedure_name):
rv = OrderedDict()
for key in cls.__raw_params[procedure_name]:
param = cls.__raw_params[procedure_name][key]
if 'int' in param['ParameterDataType'].lower():
param_type = int
elif ('money' in param['ParameterDataType'].lower() or
'decimal' in param['ParameterDataType'].lower() or
'float' in param['ParameterDataType'].lower() or
'qty' in param['ParameterDataType'].lower()):
param_type = float
else:
# TODO: Format datetime and perhaps decimal?
param_type = str
if param['IsOutPutParameter'] == 1:
param_type = pymssql.output(param_type)
else:
param_type = param_type()
rv[key] = param_type
return rv
def __init__(self, procedure_name):
super(StoredProcedure, self).__init__()
self.procedure = procedure_name
self.update(StoredProcedure.get_params_for(self.procedure))
def _set_output(self, data=(), ret_resultset=False):
if ret_resultset:
return data
if len(self) != len(data):
raise
for p, k in enumerate(self):
if isinstance(self[k], pymssql.output):
self[k] = data[p]
return self
# Should the original object be unmodified? Return a new object:
# return [(k, data[p]) for p, k in enumerate(self)]
def callproc(self, resultset=False):
return self._set_output(db.callproc(
self.procedure,
self.values()),
resultset)
def values(self):
return [value if value else None
for value in super(StoredProcedure, self).values()]
def __setitem__(self, key, obj):
if (key in self and type(self[key]) is not type(obj) and
obj is not None and not isinstance(self[key], pymssql.output)):
raise TypeError
super(StoredProcedure, self).__setitem__(key, obj)
class OrderHead(StoredProcedure):
"""Mapping for the Jeeves_Esales_CreateOrder stored procedure parameters
webapp031 and WEBAPP003 determines default order status"""
# TODO: Extend with additional functionlity if desired.
def __init__(self, company_no, web_user_name):
super(OrderHead, self).__init__('Jeeves_Esales_CreateOrder')
self['c_CompanyNo'] = company_no
# Some defaults:
self['c_ForetagKod'] = 1 # Hardcoded to LK
self['c_PersSign'] = 'marlin' # From API profile, or default
# self['c_OrderType'] = None # Default set by WEBAPP008
# self['c_TemplateRowID'] = None # No template used
# self['c_Saljare'] = None # 600 # From API profile, or default
# Unique ID added to 'kpw' when invoicing is allowed.
print(web_user_name)
self['c_webUserName'] = web_user_name
# self['LangID'] = 0 # Default to Swedish
# self['BatchId'] = '' # unused
# self['Run_Type'] = None # Could be 'R', but doesn't work
# self['Edit'] = None # Custom ordertext, currently not used in procedure
# self['EditExt'] = None # Custom ordertext, currently not used in procedure
# self['Lagstalle'] = None # '1' # Used to override customer default
# self['OverrideCreditLimit'] = 0 # Set to a char to override credit limit
# self['OrderNumber'] = pymssql.output(int)
def callproc(self):
super(OrderHead, self).callproc()
# If call succeeded, then order is allowed to be invoiced.
return self['o_OrderNumber'], bool(self['c_webUserName'])
class OrderRow(StoredProcedure):
"""Mapping for the Jeeves_Esales_AddOrderRow stored procedure parameters
AltEnhetKod logic needs to have been added to the procedure"""
def __init__(self, company_no, order_no, item_no,
qty=None, qty_alt_unit=None, alt_unit='', pers_sign='biz'):
super(OrderRow, self).__init__('Jeeves_Esales_AddOrderRow')
self['c_CompanyNo'] = str(company_no)
self['c_OrderNumber'] = int(order_no)
self['c_ItemNo'] = str(item_no)
self['c_Qty'] = float(qty) if qty else None
self['c_QtyAltEnh'] = float(qty_alt_unit) if qty_alt_unit else None
self['c_AltEnhetKod'] = str(alt_unit)
self['c_PersSign'] = str(pers_sign)
# Used to set date for delivery (c_OrdBegLevDat) and (c_OrdBerLevDat)
self['c_RequestedDate'] = None
# Some defaults:
self['c_ForetagKod'] = 1 # Hardcoded to LK
# self['OrderNumber'] = 0 # Required, ordernumber to add row to
# self['webUserName'] = order_head['webUserName']
# self['CompanyNo'] = order_head['CompanyNo']
# self['PersSign'] = order_head['PersSign']
# self['LangID'] = order_head['LangID']
# self['ItemNo'] = '' # Required, item to create row for
# self['c_Qty'] = None # Only one of qty or qtyaltenh may be used
# self['QtyAltEnh'] = None
# self['RequestedDate'] = '' # unused
# self['BatchId'] = order_head['BatchId']
# self['ArtSerieNr'] = '' # unused
# self['c_OrderType'] = None
# self['Run_Type'] = None # Could be 'R', but doesn't work
# self['c_TemplateRowID'] = None # No template used
# self['Edit'] = None # Custom order row text
# self['EditExt'] = None # Custom extended order row text
# self['Lagstalle'] = None # str: use default
# self['AltEnhetKod'] = '' # Override default alternative unit if desired
# self['AllocateAvailable'] = 0 # unused
# self['OverrideCreditLimit'] = 0 # Set to a char to override credit limit
# self['o_OrderRow'] = pymssql.output(int)
# self['o_NextQty'] = pymssql.output(float)
# self['o_NextDate'] = pymssql.output(str)
# self['o_LastQty'] = pymssql.output(float)
# self['o_LastDate'] = pymssql.output(str)
# self['o_AllocatedQty'] = pymssql.output(float)
# self['o_AllocatedDate'] = pymssql.output(str)
def callproc(self):
super(OrderRow, self).callproc()
return self['o_OrderRow']
class PlaceOrder(StoredProcedure):
"""Mapping for the Jeeves_Esales_PlaceOrder stored procedure parameters
webapp031 and WEBAPP003 determines default order status"""
def __init__(self, company_no, order_no, payment_method='card', data={}):
super(PlaceOrder, self).__init__('Jeeves_Esales_PlaceOrder')
self['c_CompanyNo'] = str(company_no)
self['c_OrderNumber'] = int(order_no)
self['c_kundref2'] = data.get('CustomerContact') # Er ref, kontaktperson
self['c_kundbestnr'] = data.get('CustomerReference')
self['c_editext'] = data.get('ExtraText') # Extern text
self['c_CoName'] = data.get('AddrName')
self['c_Addr1'] = data.get('AddrCO') # Lev.adress, c/o
self['c_Addr2'] = data.get('AddrStreet')
self['c_PostalCode'] = data.get('AddrPostalCode')
self['c_City'] = data.get('AddrCity')
self['c_CountryCode'] = data.get('AddrCountry', 'SE') # Ex: SE, FI etc.
self['c_godsmarke1'] = data.get('ShippingInfo')
self['c_godsmarke2'] = data.get('InternalInfo') # Kundspecifikt
notify_info = NotifyInfo(company_no).callproc()
self['c_TA_MailNotified'] = data.get('ShippingEmail', notify_info.get('email'))
self['c_TA_PhonNotifiedNo'] = data.get('ShippingPhone', notify_info.get('phone'))
self['c_TA_SMSNotifiedNo'] = data.get('ShippingSMS', notify_info.get('sms'))
# 1 = card, else invoice. Card requires manual update.
self['c_PaymentType'] = '1' if payment_method is 'card' else '0'
self['c_LevSattKod'] = 2 # 2 = Schenker, 4 = Collect
self['c_orderStatus'] = None # Override orderStatusCode when using invoicing
self['c_ForetagKod'] = 1 # Hardcoded to LK
self['c_orderStatus'] = None
self['c_ProvinceCode'] = None # For US customers etc.
class NotifyInfo(StoredProcedure):
"""Mapping for the JAPP_spr_LogTrade_Get_NotifyInfo stored procedure parameters
webapp031 and WEBAPP003 determines default order status"""
def __init__(self, company_no):
super(NotifyInfo, self).__init__('JAPP_spr_LogTrade_Get_NotifyInfo')
self['c_FtgNr'] = str(company_no)
self['c_ForetagKod'] = 1 # Hardcoded to LK
def callproc(self):
result = super(NotifyInfo, self).callproc(resultset=True)
ret = {'email': None, 'sms': None, 'phone': None}
if isinstance(result, list):
for r in result:
if r[1][7:].lower() in ret:
ret[r[1][7:].lower()] = r[0]
return ret

View file

@ -1,4 +1,5 @@
from .location import Location from .location import Location
from .article import Article, ArticleCategory from .article import Article, ArticleCategory
from .company import Company from .company import Company
from .pricelist import PriceList from .pricelist import PriceList
from .order import Order

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from pyjeeves.models.raw import Article as ArticleModel, ProductClass, ArticleClass, CommodityGroup from pyjeeves.models.raw import Article as ArticleModel, ProductClass, ArticleClass, CommodityGroup
from pyjeeves.models import db
from sqlalchemy.sql.expression import and_ from sqlalchemy.sql.expression import and_
from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.exc import NoResultFound
@ -10,14 +11,14 @@ logger = logging.getLogger("PyJeeves." + __name__)
# Relocate Jeeves modules to separate folder and let a "master" module handle imports, and setup. # Relocate Jeeves modules to separate folder and let a "master" module handle imports, and setup.
class Article(): class Article():
"""Handles articles in Jeeves""" """Handles articles in Jeeves, currently filters out all articles with class = 2"""
@staticmethod @staticmethod
def get(art_no): def get(art_no):
""" Query an article by number """ """ Query an article by number """
try: try:
return ArticleModel.query.filter_by( return db.raw.query(ArticleModel).filter_by(
ArtNr=art_no ArtNr=str(art_no)
).one() ).one()
except NoResultFound: except NoResultFound:
raise KeyError raise KeyError
@ -25,7 +26,29 @@ class Article():
@staticmethod @staticmethod
def get_all(filter_=and_(ArticleModel.ItemStatusCode == 0, ArticleModel.ArtKod != 2)): def get_all(filter_=and_(ArticleModel.ItemStatusCode == 0, ArticleModel.ArtKod != 2)):
# .filter_by(ItemStatusCode=0, ArtKod=2) # .filter_by(ItemStatusCode=0, ArtKod=2)
return ArticleModel.query.filter(filter_).all() return db.raw.query(ArticleModel).filter(filter_).all()
@staticmethod
def is_salable(art_no_list=[]):
""" Returns true if all articles are salable,
else false with error information """
articles = db.raw.query(ArticleModel).filter(
and_(ArticleModel.ArtNr.in_(art_no_list))).all()
blocked_articles = [article.ArtNr for article in articles
if article.ArtKod == 2 or article.ItemStatusCode != 0]
unknown_articles = [x for x in art_no_list
if x not in set([article.ArtNr for article in articles])]
if blocked_articles or unknown_articles:
errors = {}
if blocked_articles:
errors['blocked_articles'] = blocked_articles
if unknown_articles:
errors['unknown_articles'] = unknown_articles
return False, errors
return True, {}
class ArticleCategory(): class ArticleCategory():
@ -34,9 +57,9 @@ class ArticleCategory():
@staticmethod @staticmethod
def get_all(): def get_all():
# .filter_by(ItemStatusCode=0, ArtKod=2) # .filter_by(ItemStatusCode=0, ArtKod=2)
prod_classes = ProductClass.query.all() prod_classes = db.raw.query(ProductClass).all()
art_classes = ArticleClass.query.all() art_classes = db.raw.query(ArticleClass).all()
com_groups = CommodityGroup.query.all() com_groups = db.raw.query(CommodityGroup).all()
return {'ProductClasses': prod_classes, return {'ProductClasses': prod_classes,
'ArticleClasses': art_classes, 'CommodityGroups': com_groups} 'ArticleClasses': art_classes, 'CommodityGroups': com_groups}

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from pyjeeves.models.raw import Company as CompanyModel, Customer as CustomerModel from pyjeeves.models.raw import Company as CompanyModel, Customer as CustomerModel
from pyjeeves.models import db
from sqlalchemy.sql.expression import and_ from sqlalchemy.sql.expression import and_
from pyjeeves import logging from pyjeeves import logging
@ -14,18 +15,18 @@ class Company():
@staticmethod @staticmethod
def get(ftg_nr): def get(ftg_nr):
""" Query an article by number """ """ Query an article by number """
return CompanyModel.query.filter_by( return db.raw.query(CompanyModel).filter_by(
FtgNr=ftg_nr FtgNr=ftg_nr
).one() ).one()
@staticmethod @staticmethod
def get_all_active_customers(): def get_all_active_customers():
cust = CustomerModel.query.filter(and_(CustomerModel.Makulerad == 0)).all() cust = db.raw.query(CustomerModel).filter(and_(CustomerModel.Makulerad == 0)).all()
return [c.CompanyModel for c in cust] return [c.CompanyModel for c in cust]
@staticmethod @staticmethod
def get_list(ftg_nr=[]): def get_list(ftg_nr=[]):
return CompanyModel.query.filter( return db.raw.query(CompanyModel).filter(
CompanyModel.FtgNr.in_(ftg_nr) CompanyModel.FtgNr.in_(ftg_nr)
).all() ).all()

View file

@ -0,0 +1,166 @@
# -*- coding: utf-8 -*-
from pyjeeves.models.raw import Order as OrderModel, OrderItem as OrderItemModel
from pyjeeves.models import db
from sqlalchemy.sql.expression import and_
from sqlalchemy.orm.exc import NoResultFound
from pyjeeves import logging
logger = logging.getLogger("PyJeeves." + __name__)
# Relocate Jeeves modules to separate folder and let a "master" module handle imports, and setup.
class Order():
"""Handles orders in Jeeves"""
@staticmethod
def get(order_no):
""" Query an order by number """
try:
return db.raw.query(OrderModel).filter_by(
OrderNr=order_no
).one()
except NoResultFound:
raise KeyError
@staticmethod
def get_all_unregistered_order():
order = OrderModel.query.filter(and_(OrderModel.OrderStatusCode == 00)).all()
return order
@staticmethod
def get_list(order_no=[]):
return db.raw.query(OrderModel).filter(
OrderModel.OrderNr.in_(order_no)
).all()
@staticmethod
def get_all_by_company(ftg_nr=None):
if not ftg_nr:
raise KeyError
return db.raw.query(OrderModel).filter(
OrderModel.FtgNr == str(ftg_nr)
).all()
@staticmethod
def create(customer_no, head={}, items=[], web_user_name=None):
head['CompanyNumber'] = str(customer_no)
# Create order from head dict to get an order number
order, invoice_possible = OrderModel(head).create(web_user_name)
if not order['OrderNr']:
raise
# Go through order items, if any, and save them to DB.
order['OrderItems'] = Order.create_rows(order['FtgNr'], order['OrderNr'], items)
# Save the information in the order object
# Boolean argument deceides if order has contact person, and should be set 'registered'
order.save(invoice_possible)
return order
@staticmethod
def create_rows(company_no, order_no, items=[]):
rv = []
for item in items:
if not isinstance(item, OrderItemModel):
item = OrderItemModel(item)
item['OrderNr'] = order_no
item['FtgNr'] = company_no
rv.append(item.save())
return rv
if __name__ == '__main__':
logger.info("Starting TEST")
logger.info("Testing getting an order")
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
# import os
# os.environ['TDSDUMP'] = 'stdout'
# Jeeves_Esales_CreateOrder
# All "Jeeves_Esales_" procedures may perhaps be used?
# select *
# from LKTest.information_schema.routines
# where routine_type = 'PROCEDURE'
# AND SPECIFIC_NAME LIKE '%Jeeves_Esales_%';
# data = {
# 'OrderNr': 500500,
# 'OrdDatum': datetime.now(),
# 'OrdTyp': 1,
# 'FtgNr': customer_no,
# 'OrdBerLevDat': datetime(2019, 7, 10),
# 'ValKod': 'SEK',
# 'OrderItems': [
# {'ArtNr': '2005',
# 'OrdAntal': 5.4}
# ],
# 'Saljare': '500',
# 'PersSign': 'marlin',
# 'MomsKod': 2,
# 'BetKod': '10',
# 'LevVillkKod': 3,
# 'LevSattKod': 2,
# 'LagStalle': 0,
# 'ForetagKod': 1
# }
order_head = {
'AddrCO': '',
'AddrCity': 'Uppsala',
'AddrCountry': 'SE',
'AddrName': 'Lindvalls Kaffe',
'AddrPostalCode': '751 82',
'AddrStreet': 'Kungsgatan 60',
'CompanyName': 'Lindvalls Kaffe AB (övrigt)',
'CompanyNumber': '179580',
'CustomerContact': 'Test beställning',
'CustomerReference': 'no po number',
'InternalInfo': 'Test order',
'OrderNumber': 419040,
'OrderStatusCode': 13,
'ShippingEmail': 'order@lindvallskaffe.se',
'ShippingInfo': 'Lev till godsmottagning',
'ShippingPhone': '018-480 20 00',
'ShippingSMS': '0703 25 25 02',
'ShippingTypeCode': 4}
order_items = [
{'AltEnhetKod': 'Bricka5,4',
'ArticleName': 'Lindvalls Mellanrost',
'ArticleNumber': '2003',
'OrdAntalAltEnh': '1.00',
'OrderRowNumber': 10,
'UnitAmount': '5.40',
'UnitPrice': '92.00'},
{'AltEnhetKod': 'Bricka5,4',
'ArticleName': 'Lindvalls Mellanrost',
'ArticleNumber': '2003',
'OrdAntalAltEnh': 1.666666667,
'OrderRowNumber': 20,
'UnitAmount': '9.00',
'UnitPrice': '92.00'},
{'AltEnhetKod': 'Bricka5,4',
'ArticleName': 'Lindvalls Mellanrost',
'ArticleNumber': '2003',
'OrdAntalAltEnh': '5.00',
'OrderRowNumber': 30,
'UnitAmount': '27.00',
'UnitPrice': '92.00'}]
# print(Order.get_list(['406569', '179580', '2440070', '179584']))
from pprint import pprint
# pprint(Order.get(7000028).to_dict())
pprint(Order.create('179584', order_head, order_items).to_dict())
# pprint(Order.get('419033').to_dict())
# c1 = CompanyModel.query.filter_by(FtgNr="406569").first()
# print(c1)
# logger.info(c1.json)

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from pyjeeves.models.raw import PriceList as PriceListModel from pyjeeves.models.raw import PriceList as PriceListModel
from pyjeeves.models import db
from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.exc import NoResultFound
from pyjeeves import logging from pyjeeves import logging
@ -15,7 +16,7 @@ class PriceList():
def get(price_list_no): def get(price_list_no):
""" Query a price list by number """ """ Query a price list by number """
try: try:
return PriceListModel.query.filter_by( return db.raw.query(PriceListModel).filter_by(
PrisLista=price_list_no PrisLista=price_list_no
).one() ).one()
except NoResultFound: except NoResultFound: