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
"""
from pyjeeves import logging, config
from weakref import WeakValueDictionary
from sqlalchemy import create_engine, orm
from sqlalchemy.orm import sessionmaker, scoped_session, Query, aliased
from sqlalchemy.orm.exc import UnmappedClassError
from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, scoped_session
# from sqlalchemy.orm.exc import UnmappedClassError
from pymssql import OperationalError
from sqlservice import SQLClient, SQLQuery
logger = logging.getLogger("PyJeeves." + __name__)
class BaseFilterQuery(Query):
class BaseFilterQuery(SQLQuery):
def get(self, ident):
# Override get() so that the flag is always checked in the
# 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):
return Query.__iter__(self.private())
return SQLQuery.__iter__(self.private())
def from_self(self, *ent):
# Override from_self() to automatically apply
# the criterion to. this works with count() and
# others.
return Query.from_self(self.private(), *ent)
return SQLQuery.from_self(self.private(), *ent)
def private(self):
# Fetch the model name and column list and apply model-specific base filters
@ -49,94 +51,58 @@ class BaseFilterQuery(Query):
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):
"""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")
self.raw_session, self.meta_session = self.create_scoped_session(enabled_sessions)
self.Model = self.make_declarative_base(metadata)
# self.Query = Query
self.raw, self.raw_engine = (self.raw_session() if 'raw' in enabled_clients else {})
self.meta = (self.meta_session() if 'meta' in enabled_clients else {})
@property
def metadata(self):
"""Returns the metadata"""
return self.Model.metadata
def callproc(self, procedure="", params=[]):
conn = self.raw_engine.raw_connection()
# @property
# def _config(self):
# """Returns the configuration"""
# return config()
with conn.cursor() as cursor:
try:
retval = cursor.callproc(procedure, params)
try:
cursor.nextset()
retval = cursor.fetchall()
except OperationalError:
logger.debug("Executed statement has no resultset")
def make_declarative_base(self, metadata=None):
"""Creates the declarative base."""
base = declarative_base(cls=Model, name='Model',
metadata=metadata,
metaclass=MetaBaseModel)
base.query = _QueryProperty(self)
return base
conn.commit()
def create_scoped_session(self, sessions=[]):
RawSession, MetaSession = None, None
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)
finally:
conn.close()
RawSession = scoped_session(sessionmaker(bind=raw_engine))
return retval
if 'meta' in sessions:
meta_engine = create_engine(
'mysql+pymysql://{user}:{pw}@{host}:{port}/{db}?charset=utf8mb4'.format(
**config.config['databases']['meta']))
def execute(self, operation=""):
conn = self.raw_engine.raw_connection()
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
"""
from decimal import Decimal
from datetime import datetime
from sqlalchemy.sql.expression import and_
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
@ -14,7 +20,19 @@ from . import db
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):
""" Generalize __init__, __repr__ and to_json
Based on the models columns , ForetagKod=1"""
@ -22,6 +40,7 @@ class RawBaseModel(ModelBase):
__to_dict_filter__ = []
__to_dict_only__ = ()
__column_map__ = {}
__reversed_column_map__ = lambda self: {v: k for k, v in self.__column_map__.items()} # noqa
__table_args__ = {
'extend_existing': True
@ -29,11 +48,19 @@ class RawBaseModel(ModelBase):
__dict_args__ = {
'adapters': {
# datetime: lambda value, col, *_: value.strftime('%Y-%m-%d'),
Decimal: lambda value, col, *_: "{:.2f}".format(value)
datetime: lambda value, col, *_: value.strftime('%Y-%m-%d %H:%M'),
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
def _base_filters(self, obj, filters=and_()):
# 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 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):
"""Return a ``dict`` that maps data loaded in :attr:`__dict__` to this
model's descriptors. The data contained in :attr:`__dict__` represents
@ -90,6 +155,18 @@ class RawBaseModel(ModelBase):
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):
db.raw_session.merge(self)
return self

View file

@ -5,50 +5,38 @@
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.types import Integer, String
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.sql.expression import and_
from sqlalchemy.exc import OperationalError
from . import db
from pyjeeves import logging
from .abc import RawBaseModel
from .sp_classes import OrderHead, OrderRow, PlaceOrder
import re
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'
__column_map__ = {'ArtProdKlass': 'ProductClassNumber', 'ArtProdklBeskr': 'ProductClassName'}
__to_dict_only__ = ('ArtProdKlass', 'ArtProdklBeskr')
# print_filter = ('Articles', 'articles_collection')
class ArticleClass(Base, RawBaseModel):
class ArticleClass(RawBaseModel):
__tablename__ = 'xm'
__column_map__ = {'ArtKod': 'ArticleClassNumber', 'ArtTypBeskr': 'ArticleClassName'}
__to_dict_only__ = ('ArtKod', 'ArtTypBeskr')
# print_filter = ('Articles', 'articles_collection')
class CommodityGroup(Base, RawBaseModel):
class CommodityGroup(RawBaseModel):
__tablename__ = 'vg'
__column_map__ = {'VaruGruppKod': 'CommodityGroupNumber',
'VaruGruppBeskr': 'CommodityGroupName'}
@ -61,14 +49,14 @@ class CommodityGroup(Base, RawBaseModel):
ArticleClass = relationship(ArticleClass)
class ArticleAlternativeUnit(Base, RawBaseModel):
class ArticleAlternativeUnit(RawBaseModel):
__tablename__ = 'xae'
__column_map__ = {'AltEnhetKod': 'UnitCode', 'AltEnhetBeskr': 'UnitName',
'AltEnhetOmrFaktor': 'DefaultUnitConv'}
__to_dict_only__ = ('AltEnhetBeskr', 'AltEnhetOmrFaktor')
class ArticleUnit(Base, RawBaseModel):
class ArticleUnit(RawBaseModel):
__tablename__ = 'xare'
__column_map__ = {'ArtNr': 'ArticleNumber',
'AltEnhetKod': 'UnitCode', 'AltEnhetOmrFaktor': 'UnitConv',
@ -82,7 +70,7 @@ class ArticleUnit(Base, RawBaseModel):
ArticleAlternativeUnit = relationship(ArticleAlternativeUnit)
class ArticleBalance(Base, RawBaseModel):
class ArticleBalance(RawBaseModel):
__tablename__ = 'ars'
__column_map__ = {'LagSaldo': 'Balance',
'LagResAnt': 'ReservedBalance',
@ -100,7 +88,7 @@ class ArticleBalance(Base, RawBaseModel):
ArtNr = Column(Integer, ForeignKey('ar.ArtNr'), primary_key=True)
class Article(Base, RawBaseModel):
class Article(RawBaseModel):
__tablename__ = 'ar'
__column_map__ = {'ArtNr': 'ArticleNumber',
@ -178,7 +166,7 @@ class Article(Base, RawBaseModel):
)
class Company(Base, RawBaseModel):
class Company(RawBaseModel):
__tablename__ = 'fr'
__column_map__ = {'FtgNr': 'CompanyNumber', 'FtgNamn': 'CompanyName'}
__to_dict_only__ = ('FtgNr', 'FtgNamn', 'Customer')
@ -188,13 +176,17 @@ class Company(Base, RawBaseModel):
Customer = relationship('Customer', uselist=False, back_populates='Company', lazy='joined')
class CustomerCategory(Base, RawBaseModel):
class DelivLoc(RawBaseModel):
__tablename__ = 'lp'
class CustomerCategory(RawBaseModel):
__tablename__ = 'x1k'
KundKategoriKod = Column(Integer, primary_key=True)
class Customer(Base, RawBaseModel):
class Customer(RawBaseModel):
__tablename__ = 'kus'
__column_map__ = {'FtgNr': 'CompanyNumber', 'kundkategorikod': 'CustomerCategoryCode',
'PrisListaKundSpec': 'PriceListPrimary', 'PrisLista': 'PriceListSecondary'}
@ -218,10 +210,10 @@ class Customer(Base, RawBaseModel):
@hybrid_property
def CustomerCategory(self):
return self.KundKategori.KundKatBeskr
return self.KundKategori.KundKatBeskr if self.KundKategori else ""
class PriceList(Base, RawBaseModel):
class PriceList(RawBaseModel):
__tablename__ = 'prh'
__column_map__ = {'PrisListaBeskr': 'Description', 'PrisLista': 'PriceListNumber',
'MarkUpBelopp': 'PriceFactor'}
@ -234,7 +226,7 @@ class PriceList(Base, RawBaseModel):
PriceListItems = relationship('PriceListItem', back_populates="PriceList", lazy='joined')
class PriceListItem(Base, RawBaseModel):
class PriceListItem(RawBaseModel):
__tablename__ = 'prl'
__column_map__ = {'ArtNr': 'ArticleNumber', 'vb_pris': 'UnitPrice',
'MarkUpBelopp': 'UnitPriceFactor', 'NollFaktor': 'NullPriceAllowed'}
@ -272,8 +264,123 @@ class PriceListItem(Base, RawBaseModel):
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
Order = Base.classes.oh # Orders by customers
DelivLoc = Base.classes.lp # Connections between a delivery company and customer company
__dict_args__ = {
'adapters': {
**{
'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

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

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from pyjeeves.models.raw import Article as ArticleModel, ProductClass, ArticleClass, CommodityGroup
from pyjeeves.models import db
from sqlalchemy.sql.expression import and_
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.
class Article():
"""Handles articles in Jeeves"""
"""Handles articles in Jeeves, currently filters out all articles with class = 2"""
@staticmethod
def get(art_no):
""" Query an article by number """
try:
return ArticleModel.query.filter_by(
ArtNr=art_no
return db.raw.query(ArticleModel).filter_by(
ArtNr=str(art_no)
).one()
except NoResultFound:
raise KeyError
@ -25,7 +26,29 @@ class Article():
@staticmethod
def get_all(filter_=and_(ArticleModel.ItemStatusCode == 0, ArticleModel.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():
@ -34,9 +57,9 @@ class ArticleCategory():
@staticmethod
def get_all():
# .filter_by(ItemStatusCode=0, ArtKod=2)
prod_classes = ProductClass.query.all()
art_classes = ArticleClass.query.all()
com_groups = CommodityGroup.query.all()
prod_classes = db.raw.query(ProductClass).all()
art_classes = db.raw.query(ArticleClass).all()
com_groups = db.raw.query(CommodityGroup).all()
return {'ProductClasses': prod_classes,
'ArticleClasses': art_classes, 'CommodityGroups': com_groups}

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from pyjeeves.models.raw import Company as CompanyModel, Customer as CustomerModel
from pyjeeves.models import db
from sqlalchemy.sql.expression import and_
from pyjeeves import logging
@ -14,18 +15,18 @@ class Company():
@staticmethod
def get(ftg_nr):
""" Query an article by number """
return CompanyModel.query.filter_by(
return db.raw.query(CompanyModel).filter_by(
FtgNr=ftg_nr
).one()
@staticmethod
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]
@staticmethod
def get_list(ftg_nr=[]):
return CompanyModel.query.filter(
return db.raw.query(CompanyModel).filter(
CompanyModel.FtgNr.in_(ftg_nr)
).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 -*-
from pyjeeves.models.raw import PriceList as PriceListModel
from pyjeeves.models import db
from sqlalchemy.orm.exc import NoResultFound
from pyjeeves import logging
@ -15,7 +16,7 @@ class PriceList():
def get(price_list_no):
""" Query a price list by number """
try:
return PriceListModel.query.filter_by(
return db.raw.query(PriceListModel).filter_by(
PrisLista=price_list_no
).one()
except NoResultFound: