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:
parent
b77a7069ce
commit
0af38e286e
9 changed files with 730 additions and 138 deletions
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
250
pyjeeves/models/sp_classes.py
Normal file
250
pyjeeves/models/sp_classes.py
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
166
pyjeeves/repositories/order.py
Normal file
166
pyjeeves/repositories/order.py
Normal 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)
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue