diff --git a/.gitignore b/.gitignore index 1f5d317..dec62d9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +*.csv + *.py[cod] # C extensions diff --git a/README.md b/README.md new file mode 100644 index 0000000..161b950 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# PyJeeves Module + +This project is a Jeeves data extraction and integration project. + +## Initial creation of database schema. + +```bash +docker run --link db --network marcus_default -v /srv/pyjeeves/config.yml:/app/config.yml gitlab.lndvll.se:5500/lindvallskaffe/pyjeeves python ./pyjeeves/db_raw.py +``` + +## Connecting to DB with client + +```bash +docker run -it --network marcus_default --link db:mysql --rm mysql sh -c 'exec mysql -h"db" -P"3306" -uroot -p"ROOT_PW"' +``` + +## Forcing updates + +You may force updates of objects by setting RowUpdatedDt to null. +For example: +´update jvs_customers set RowUpdatedDt = null;´ + diff --git a/README.rst b/README.rst deleted file mode 100644 index 872edcf..0000000 --- a/README.rst +++ /dev/null @@ -1,22 +0,0 @@ -PyJeeves Module -=============== - -This project is a Jeeves data extraction and integration project. - - -## Initial creation of database schema. - -´docker run --link db --network marcus_default -v /srv/pyjeeves/config.yml:/app/config.yml gitlab.lndvll.se:5500/lindvallskaffe/pyjeeves python ./pyjeeves/db_raw.py´ - - -## Connecting to DB with client - -´docker run -it --network marcus_default --link db:mysql --rm mysql sh -c 'exec mysql -h"db" -P"3306" -uroot -p"ROOT_PW"'´ - - -## Forcing updates - -You may force updates of objects by setting RowUpdatedDt to null. -For example: -´update jvs_customers set RowUpdatedDt = null;´ - diff --git a/jeeves_sp_updates/Jeeves_Esales_PlaceOrder.spr b/jeeves_sp_updates/Jeeves_Esales_PlaceOrder.spr index 5c43760..ba4c3f7 100644 --- a/jeeves_sp_updates/Jeeves_Esales_PlaceOrder.spr +++ b/jeeves_sp_updates/Jeeves_Esales_PlaceOrder.spr @@ -101,6 +101,7 @@ declare @dbc int, @jvss_OrderReservation char(1), @kpwid integer, @OrdBerLevDat DateTime, + @OrdBerednDat DateTime, @SALES007 smallint, @kus_AddArtEjAktiv char(1), @OrdLevAdr4 Jeeves_StrVarChar64 @@ -196,7 +197,8 @@ execute @wi = Jeeves_oh_ordervarde @c_Foretagkod = @c_ForetagKod --The estimated delivery date of the order will be set to the earliest delivery date of a row. -select @OrdBerLevDat = min(OrdBerLevDat) from orp where foretagkod = @c_ForetagKod AND OrderNr = @c_OrderNumber +-- Also fetch OrdBerednDat to set base later +select @OrdBerLevDat = min(OrdBerLevDat), @OrdBerednDat = min(OrdBerednDat) from orp where foretagkod = @c_ForetagKod AND OrderNr = @c_OrderNumber begin tran @@ -304,6 +306,8 @@ if @c_PaymentType <> '1' begin oh.levforetolv = @c_levforetolv, oh.levsattkod = @Levsattkod, oh.ordberlevdat = @OrdBerLevDat, + oh.ohordberlevdatbase = @OrdBerLevDat, -- Fix base LevDat and BerednDat. Used when adding rows in GUI. + oh.ohordberedndatbase = @OrdBerednDat, oh.ordlevadr1 = @c_CoName, oh.ordlevadr2 = @c_Addr1, oh.ordlevadr3 = @c_Addr2, @@ -356,6 +360,8 @@ end else begin oh.levforetolv = @c_levforetolv, oh.levsattkod = @Levsattkod, oh.ordberlevdat = @OrdBerLevDat, + oh.ohordberlevdatbase = @OrdBerLevDat, -- Fix base LevDat and BerednDat. Used when adding rows in GUI. + oh.ohordberedndatbase = @OrdBerednDat, oh.ordlevadr1 = @c_CoName, oh.ordlevadr2 = @c_Addr1, oh.ordlevadr3 = @c_Addr2, diff --git a/pyjeeves/connector.py b/pyjeeves/connector.py index bb62f48..64c32c4 100644 --- a/pyjeeves/connector.py +++ b/pyjeeves/connector.py @@ -57,7 +57,9 @@ class DBConnector(object): def __init__(self, enabled_clients=['raw'], metadata=None): logger.info("Creating engines and sessionmakers") - self.raw, self.raw_engine = (self.raw_session() if 'raw' in enabled_clients else {}) + self.enabled_clients = enabled_clients + self.raw_db, self.raw_session, self.raw_engine = ( + self.raw_client() if 'raw' in enabled_clients else {}) self.meta = (self.meta_session() if 'meta' in enabled_clients else {}) def callproc(self, procedure="", params=[]): @@ -90,14 +92,22 @@ class DBConnector(object): conn.close() return results - def raw_session(self): + def raw_client(self): + if 'raw' not in self.enabled_clients: + logger.error('Raw client is not enabled') + + logger.info("Using DB %s" % config.config['databases']['raw']['db']) 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 + return db, db.session, db.engine + + def set_model_class(self, model_class): + self.raw_db.model_class = model_class + self.raw_db.update_models_registry() def meta_session(self): diff --git a/pyjeeves/models/abc.py b/pyjeeves/models/abc.py index da8983c..bb41186 100644 --- a/pyjeeves/models/abc.py +++ b/pyjeeves/models/abc.py @@ -26,10 +26,11 @@ logger.info("Reading Jeeves DB structure") meta = MetaData() try: - meta.reflect(bind=db.raw.connection(), - only=['ar', 'ars', 'xae', 'xare', 'fr', 'kus', 'x1k', + meta.reflect(bind=db.raw_session.connection(), + only=['ar', 'ars', 'arsh', 'arean', 'xae', 'xare', 'fr', 'kus', 'x1k', 'oh', 'orp', 'lp', 'vg', 'xp', 'xm', 'prh', 'prl', - 'kp', 'kpw', 'cr', 'X4', 'xw', 'X1']) + 'kp', 'kpw', 'cr', 'X4', 'xw', 'X1', + 'JAPP_EWMS_Item_Replenishment_Levels']) except OperationalError as e: logger.error("Failed to read Jeeves DB structure") raise e @@ -194,3 +195,6 @@ def receive_attribute_instrument(cls, key, inst): "listen for the 'attribute_instrument' event" install_validator_listner(cls, key, inst) + + +db.set_model_class(RawBaseModel) diff --git a/pyjeeves/models/ext.py b/pyjeeves/models/ext.py index 9df4934..38ace14 100644 --- a/pyjeeves/models/ext.py +++ b/pyjeeves/models/ext.py @@ -52,3 +52,5 @@ class LengthValidator(): state.__class__.__name__, state.__class__._map_columns(self.col_name), len(value), self.max_length)) return value + +# Add more validators, such as type for ints. diff --git a/pyjeeves/models/raw.py b/pyjeeves/models/raw.py index a42c78f..fe95d66 100644 --- a/pyjeeves/models/raw.py +++ b/pyjeeves/models/raw.py @@ -67,8 +67,8 @@ class ArticleUnit(RawBaseModel): ArtNr = Column(String, ForeignKey('ar.ArtNr'), primary_key=True) - AltEnhetKod = Column(Integer, ForeignKey('xae.AltEnhetKod'), primary_key=True) - ArticleAlternativeUnit = relationship(ArticleAlternativeUnit) + AltEnhetKod = Column(String, ForeignKey('xae.AltEnhetKod'), primary_key=True) + ArticleAlternativeUnit = relationship(ArticleAlternativeUnit, lazy='joined') class ArticleBalance(RawBaseModel): @@ -89,6 +89,30 @@ class ArticleBalance(RawBaseModel): ArtNr = Column(Integer, ForeignKey('ar.ArtNr'), primary_key=True) +class ArticleEAN(RawBaseModel): + __tablename__ = 'arean' + __column_map__ = {'ArtNrEAN': 'EAN', 'ArtNr': 'ArticleNumber'} + __to_dict_only__ = ('ArtNr', 'ArtNrEAN', 'ArticleUnit') + + ArtNr = Column(String, ForeignKey('ar.ArtNr'), primary_key=True) + ArtNrEAN = Column(String, primary_key=True) + AltEnhetKod = Column(String, ForeignKey('xare.AltEnhetKod')) + + ArticleUnit = relationship(ArticleUnit, lazy='joined') + + +class ArticleShelf(RawBaseModel): + __tablename__ = 'arsh' + __column_map__ = {'LagPlats': 'Shelf', + 'LagStalle': 'WarehouseID', + 'JAPP_EWMS_zoneid': 'WMSZoneID', + 'ArtNr': 'ArticleNumber'} + __to_dict_only__ = ('LagPlats', 'LagStalle', 'JAPP_EWMS_zoneid', 'ArtNr') + + LagPlats = Column(String, ForeignKey('ar.ArtNr'), primary_key=True) + LagStalle = Column(String, ForeignKey('ar.ArtNr'), primary_key=True) + + class VATRate(RawBaseModel): __tablename__ = 'X1' __column_map__ = {'MomsKod': 'VATID', 'MomsSats': 'VATRate'} @@ -200,12 +224,12 @@ class Article(RawBaseModel): except TypeError: logger.debug("NoneType error, %s" % self.ArtNr) - @classmethod - def _base_filters(self, obj): - return RawBaseModel._base_filters( - obj, - and_(obj.ItemStatusCode == 0) - ) + # @classmethod + # def _base_filters(self, obj): + # return RawBaseModel._base_filters( + # obj, + # and_(obj.ItemStatusCode == 0) + # ) class ContactInformationType(RawBaseModel): @@ -535,3 +559,16 @@ class OrderItem(RawBaseModel): pers_sign=self['PersSign']).callproc() self['OrdRadNr'] = row_no return self + + +class ItemReplenishmentLevels(RawBaseModel): + # __table_args__ = {'mssql_autoincrement': False, 'extend_existing': True} + # __table_args__ = {'implicit_returning': False, 'extend_existing': True} + __tablename__ = 'JAPP_EWMS_Item_Replenishment_Levels' + __column_map__ = {'ArtNr': 'ArticleNumber', 'LagPlats': 'Shelf', 'LagStalle': 'WarehouseID'} + __to_dict_only__ = ('LagPlats', 'ArtNr') + + # Workaround for: + # "Table 'JAPP_EWMS_Item_Replenishment_Levels' does not have the identity property. + # Cannot perform SET operation." + ForetagKod = Column(Integer, primary_key=True, autoincrement=False) diff --git a/pyjeeves/models/sp_classes.py b/pyjeeves/models/sp_classes.py index ffc5c2f..3628382 100644 --- a/pyjeeves/models/sp_classes.py +++ b/pyjeeves/models/sp_classes.py @@ -24,9 +24,11 @@ class StoredProcedure(OrderedDict): 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 + 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""" diff --git a/pyjeeves/repositories/__init__.py b/pyjeeves/repositories/__init__.py index b58262c..f349a0f 100644 --- a/pyjeeves/repositories/__init__.py +++ b/pyjeeves/repositories/__init__.py @@ -2,4 +2,5 @@ from .location import Location from .article import Article, ArticleCategory from .company import Company from .pricelist import PriceList -from .order import Order \ No newline at end of file +from .order import Order +from .warehouse import Warehouse \ No newline at end of file diff --git a/pyjeeves/repositories/article.py b/pyjeeves/repositories/article.py index d0ad923..a8b0cd3 100644 --- a/pyjeeves/repositories/article.py +++ b/pyjeeves/repositories/article.py @@ -1,9 +1,12 @@ # -*- 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, ArticleEAN, ArticleUnit) from pyjeeves.models import db from sqlalchemy.sql.expression import and_ from sqlalchemy.orm.exc import NoResultFound +from gtin import GTIN from pyjeeves import logging logger = logging.getLogger("PyJeeves." + __name__) @@ -17,22 +20,31 @@ class Article(): def get(art_no): """ Query an article by number """ try: - return db.raw.query(ArticleModel).filter_by( + return db.raw_session.query(ArticleModel).filter_by( ArtNr=str(art_no) ).one() except NoResultFound: raise KeyError @staticmethod - def get_all(filter_=and_(ArticleModel.ItemStatusCode == 0, ArticleModel.ArtKod != 2)): + def get_all(filter_=and_( + ArticleModel.ItemStatusCode == 0, + ArticleModel.ArtKod != 2, + ArticleModel.VaruGruppKod != 90, + ArticleModel.ArtProdKlass != 0) + ): # .filter_by(ItemStatusCode=0, ArtKod=2) - return db.raw.query(ArticleModel).filter(filter_).all() + return db.raw_session.query(ArticleModel).filter(filter_).all() + + @staticmethod + def get_article_units(filter_=and_()): + return db.raw_session.query(ArticleUnit).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( + articles = db.raw_session.query(ArticleModel).filter( and_(ArticleModel.ArtNr.in_(art_no_list))).all() blocked_articles = [article.ArtNr for article in articles @@ -50,6 +62,43 @@ class Article(): return True, {} + @staticmethod + def get_article_gtins(): + return db.raw_session.query(ArticleEAN).all() + + @staticmethod + def add_article_gtins(gtins=[], dry_run=False): + # Expects a list of dicts like this: + # [{ + # 'article_no': article.ArtNr, + # 'article_gtin': gtin, + # 'unit': unit.AltEnhetKod, + # }] + for gtin in gtins: + n1 = ArticleEAN( + ArtNr=gtin['article_no'], AltEnhetKod=gtin.get('unit', None), + ArtNrEAN=str(gtin['article_gtin']), ForetagKod=1) + if dry_run: + logger.info('Creating GTIN for %s, %s, %s' % (n1.ArtNr, n1.AltEnhetKod, n1.ArtNrEAN)) + continue + + db.raw_db.add(n1) + logger.debug('Created/updated Article EAN for %s - %s with GTIN %s' % ( + gtin['article_no'], gtin.get('unit', 'no unit'), gtin['article_gtin'])) + + db.raw_db.commit() + logger.info('Succesfully commited %s GTINs to database' % (len(gtins))) + + @staticmethod + def clear_article_gtins(): + gtins = db.raw_session.query(ArticleEAN).all() + + for gtin in gtins: + db.raw_db.delete(gtin) + + db.raw_db.commit() + logger.info('Deleted %s GTINs' % (len(gtins))) + class ArticleCategory(): """Handles article categories, such as classes and groups in Jeeves""" @@ -57,27 +106,362 @@ class ArticleCategory(): @staticmethod def get_all(): # .filter_by(ItemStatusCode=0, ArtKod=2) - prod_classes = db.raw.query(ProductClass).all() - art_classes = db.raw.query(ArticleClass).all() - com_groups = db.raw.query(CommodityGroup).all() + prod_classes = db.raw_session.query(ProductClass).all() + art_classes = db.raw_session.query(ArticleClass).all() + com_groups = db.raw_session.query(CommodityGroup).all() return {'ProductClasses': prod_classes, 'ArticleClasses': art_classes, 'CommodityGroups': com_groups} +# TODO: Should be moved to separate project with Lindvalls specific code +def get_gtin_for_article(article_ean, article_unit=None, use_prefix=True): + # If we don't want to prefix with 0, then exclude them here. + UNIT_MAPPING = { + 'Påse': '', + 'st': '', + 'paket': 0, + '200g': 0, + 'kg': 9, + 'Kart': 1, + 'Bricka': 1, + '½-pall': 2, + 'tray_no_wrap': 8 + } + prefixes = [] + if article_unit: + # Find matching values in unit mapping + prefixes = [ + val for key, val in UNIT_MAPPING.items() + if article_unit[0:len(key)].lower() in key.lower()] + if len(prefixes) > 1: + logger.warning('More than one unit match found in unit mapping') + + # Use the first match + raw_gtin = (str(prefixes[0]) + article_ean) if prefixes and use_prefix else article_ean + + # Handle GS1-128 GTIN code + if len(raw_gtin) >= 15 and raw_gtin[0:2] == '01': + raw_gtin = raw_gtin[2::] + + article_gtin = GTIN(raw=raw_gtin) + + return article_gtin + + +# TODO: Should be moved to separate project with Lindvalls specific code +def create_gtins_for_trading_goods(filename='gtin_trading_goods.csv'): + articles = Article.get_all(and_( + ArticleModel.ArtProdKlass == 4)) + + gtins = [] + gtin_data = {} + + import csv + with open(filename, newline='') as csvfile: + gtinreader = csv.reader(csvfile, delimiter=',') + headers = gtinreader.__next__() + logger.info('Found these columns: %s' % (', '.join(headers))) + + for row in gtinreader: + gtin_data[row[0]] = row + + logger.info("Found %s articles and updating with %s rows of data" % ( + len(articles), len(gtin_data))) + + for article in articles: + data = gtin_data.get(article.ArtNr) + if data: + default_set = False + if len(article.ArticleUnit) == 0 and data[3]: + logger.warning('Article %s has no ArticleUnits, but requires it' % (article.ArtNr)) + + for unit in article.ArticleUnit: + if unit.AltEnhetKod[0:3] == 'kart' and not data[3]: + logger.warning('Article %s missing kart unit' % (article.ArtNr)) + + if data[3] and unit.AltEnhetKod != 'st': + gtin = get_gtin_for_article(data[3], unit.AltEnhetKod, False) + # Only add GTINs for order units (not PO units) + if unit.AltEnhetOrder == '1': + gtins.append({ + 'article_no': article.ArtNr, + 'article_gtin': gtin, + 'unit': unit.AltEnhetKod + }) + if unit.AltEnhetKod == 'st': + gtin = get_gtin_for_article(data[2], unit.AltEnhetKod, False) + # Only add GTINs for order units (not PO units) + if unit.AltEnhetOrder == '1': + gtins.append({ + 'article_no': article.ArtNr, + 'article_gtin': gtin, + 'unit': unit.AltEnhetKod + }) + default_set = True + + # Add default gtin if 'st' not used + if not default_set: + gtin = get_gtin_for_article(data[2], None, False) + gtins.append({ + 'article_no': article.ArtNr, + 'article_gtin': gtin + }) + else: + # Warn about active and stock items that didn't get updated. + if article.LagTyp == 0 and article.ItemStatusCode == 0: + logger.warning('Article %s has no GTIN data in CSV' % (article.ArtNr)) + + Article.add_article_gtins(gtins) + + +# TODO: Should be moved to separate project with Lindvalls specific code +def create_gtins(dry_run=True): + # GS1 Company Prefixes that we manage locally, prefixing etc. + LOCAL_GCPS = [ + '731083', # Lindvalls Kaffe + '7392736', # Sackeus AB + '735007318', # Sarria Import AB + '732157', # Martin & Servera AB + '350096', # Scænsei Thee Kompani AB (Used by REKYL In Omnia Paratus AB) + '735003307', # Coffee Please + '735003711', # Emmas Skafferi AB (Used by Coffee Please) + '735003712', # Prefix no longer subscribed (Used by Coffee Please) + '735003302', # Josephine Selander - YogaGo (Used by Coffee Please) + ] + + articles = Article.get_all(and_( + ArticleModel.ItemStatusCode == 0, + ArticleModel.VaruGruppKod != 90, + ArticleModel.ArtProdKlass != 0)) + + articles_with_existing_gtins = [ + gtin.ArtNr for i, gtin in enumerate(Article.get_article_gtins())] + + gtins = [] + + for article in articles: + if article.ArtNr in articles_with_existing_gtins: + continue + + if not article.ArtStreckKod: + logger.warning('No base GTIN for article %s' % (article.ArtNr)) + continue + + GCP = GTIN(raw=article.ArtStreckKod).gcp + + if 12 < len(article.ArtStreckKod) < 12 and GCP in LOCAL_GCPS: + logger.error('Base GTIN is wrong length for article %s' % (article.ArtNr)) + continue + + # If GTIN is provided by vendor, skip prefixes and gohead if only one or no units exist. + if GCP not in LOCAL_GCPS and len(article.ArticleUnit) <= 1: + use_prefix = False + logger.info('Externally provided GTIN for %s, skipping prefixes' % (article.ArtNr)) + elif GCP not in LOCAL_GCPS and len(article.ArticleUnit) > 1: + logger.warning('Externally provided GTIN for %s, too many units' % (article.ArtNr)) + continue + else: + use_prefix = True + + # Create gtin without ArticleUnit, for the base unit. + # gtins.append({ + # 'article_no': article.ArtNr, + # 'article_gtin': get_gtin_for_article(article.ArtStreckKod, None, False) + # }) + + for unit in article.ArticleUnit: + + # Skip paket for 21%, should only match HV with plastic wrapping. + if article.ArtNr[0:2] == '21' and unit.AltEnhetKod[0:6].lower() == 'paket': + logger.info('Skip paket unit for %s' % (article.ArtNr)) + continue + + # Special for 20%/30%, should only match HV without plastic wrapping. + if article.ArtNr[0:2] in ('20', '30') and unit.AltEnhetKod[0:6].lower() == 'bricka': + unit_code = 'tray_no_wrap' + else: + unit_code = unit.AltEnhetKod + + gtin = get_gtin_for_article(article.ArtStreckKod, unit_code, use_prefix) + + # Only add GTINs for order units (not PO units) + if unit.AltEnhetOrder == '1': + gtins.append({ + 'article_no': article.ArtNr, + 'article_gtin': gtin, + 'unit': unit.AltEnhetKod + }) + + # Workaround for scanning HV base units without plastic wrapping + if str(gtin)[0] == '0': + # Create gtin without ArticleUnit, for the base unit. + gtins.append({ + 'article_no': article.ArtNr, + 'article_gtin': get_gtin_for_article(article.ArtStreckKod, None, False) + }) + + # Add GTIN to articles that don't use article units + # Should this still be added to arean/ArticleEAN??? + # if len(article.ArticleUnit) == 0: + # gtin = get_gtin_for_article(article.ArtStreckKod, None, use_prefix) + # gtins.append({ + # 'article_no': article.ArtNr, + # 'article_gtin': gtin, + # 'unit': None, + # }) + # add_gtin_for_article( + # article.ArtNr, article.ArtStreckKod, None, use_prefix) + + Article.add_article_gtins(gtins, dry_run) + + +# TODO: Should be moved to separate project with Lindvalls specific code +def find_articles_without_base_gtin(): + articles = Article.get_all(and_( + ArticleModel.ItemStatusCode == 0, + ArticleModel.VaruGruppKod != 90, + ArticleModel.ArtProdKlass != 0)) + + _list = [] + + for article in articles: + if not article.ArtStreckKod: + _list.append( + {'artnr': article.ArtNr, + 'artbeskr': article.ArtBeskr, + 'error': 'no_base'}) + continue + else: + if 12 < len(article.ArtStreckKod) < 12: + _list.append( + {'artnr': article.ArtNr, + 'artbeskr': article.ArtBeskr, + 'error': 'wrong_length'}) + continue + + for item in _list: + print('{artnr}, "{artbeskr}", {error}'.format(**item)) + + +# TODO: Should be moved to separate project with Lindvalls specific code +def set_storage_type(): + articles = Article.get_all(and_( + ArticleModel.LagTyp == 0, + ArticleModel.ItemStatusCode == 0, + ArticleModel.AnskaffningsSatt == 10)) + + for article in articles: + article.LagTyp = 4 + + db.raw_db.commit() + + logger.info("Updated storage type for %s articles" % (len(articles))) + + +# TODO: Should be moved to separate project with Lindvalls specific code +def set_zone_placement(): + # Logic for article groups and zones + # ArticleClass descides which zone to put it. + # set Article.ArticleBalance[0].japp_ewms_rec_zoneid to correct zoneid + + article_class_map = { + 'Kaffe': 'U', + 'OoH-Kaffe': 'K', + 'Private Label': 'S', + 'Tillbehör och maskiner': 'U', + 'Komplement': 'U' + } + + articles = Article.get_all(and_( + ArticleModel.ItemStatusCode == 0, + ArticleModel.AnskaffningsSatt == 10)) + + zone_placements_update = 0 + for article in articles: + zone_id = article_class_map.get(article.ArticleClass.ArtTypBeskr) + if zone_id and article.ArticleBalance: + article.ArticleBalance[0].JAPP_EWMS_REC_ZoneID = zone_id + zone_placements_update += 1 + else: + logger.info("Excluded %s, wrong article class or no balance " % (article.ArtNr)) + + db.raw_db.commit() + + logger.info("Updated placement zone for %s articles" % (zone_placements_update)) + + # a = Article.get('2109') + # print([ab.to_dict() for ab in a['ArticleBalance']]) + + +def update_decimals_on_alt_units(): + units = Article.get_article_units(ArticleUnit.AltEnhetKod == 'påse') + + updated_units = 0 + + for unit in units: + if unit.AltEnhetOmrFaktor is not None: + dec_count = 0 + for digit in unit.AltEnhetOmrFaktor.as_tuple().digits: + if digit != 0: + dec_count += 1 + unit.AltEnhetAntDec = dec_count + + updated_units += 1 + + db.raw_db.commit() + logger.info("Updated decimal count for %s article units" % (updated_units)) + + if __name__ == '__main__': # print([column.key for column in Company.__table__.columns]) - logger.info("Starting TEST") + # from pprint import pprint + # logger.info("Starting TEST") + # session = RawSession() - logger.info("Testing gettings an article") - # c1 = session.query(Company).filter_by(FtgNr="179580").first() - # print(ArticleModel) - c1 = ArticleModel.query.filter_by(ArtNr="2103").first() - print(c1) - logger.info(c1.json) + # logger.info("Testing gettings an article") + # # c1 = session.query(Company).filter_by(FtgNr="179580").first() + # # print(ArticleModel) + # c1 = db.raw_session.query(ArticleModel).filter_by(ArtNr="2003").first() + # c1 = Article.get("2003") + # pprint([unit.to_dict() for unit in c1.ArticleUnit]) + # pprint(c1.to_dict()) + # pprint([(au.to_dict(), au.AltEnhetOrder) for au in c1.ArticleUnit]) + # logger.info(c1.to_dict()) - print( - len(ArticleModel.get_all()) - ) + # print( + # len(Article.get_all()) + # ) + + # c1 = db.raw_session.query(ArticleEAN).all() + # pprint([c.to_dict() for c in c1]) + + # c1 = db.raw_session.query(ArticleEAN).filter_by(ArtNr="1054").first() + # pprint(c1.to_dict()) + # c1.ArtNrEAN = '7310830010548' + # pprint(c1.to_dict()) + # c1.save() + # logger.info(c1.to_dict()) + # create_gtins_for_trading_goods('gtin_trading_goods_test.csv') + + # LIVE FUNCTIONS BELOW + + # find_articles_without_base_gtin() + + # logger.info("Truncating GTINs") + # Article.clear_article_gtins() + logger.info("Creating new GTINs from base GTIN") + create_gtins(dry_run=False) + # logger.info("Creating new GTINs from trading goods CSV") + # create_gtins_for_trading_goods() + + # logger.info("Update articles for batch management") + # set_storage_type() + + # logger.info("Set zone information on article balance") + # set_zone_placement() + + # logger.info("Updating alt units") + # update_decimals_on_alt_units() diff --git a/pyjeeves/repositories/company.py b/pyjeeves/repositories/company.py index c0fc206..a707296 100644 --- a/pyjeeves/repositories/company.py +++ b/pyjeeves/repositories/company.py @@ -18,7 +18,7 @@ class Company(): def get(ftg_nr): """ Query an article by number """ try: - return db.raw.query(CompanyModel).filter_by( + return db.raw_session.query(CompanyModel).filter_by( FtgNr=ftg_nr ).one() except NoResultFound: @@ -26,17 +26,21 @@ class Company(): @staticmethod def get_all_active_customers(): - cust = db.raw.query(CustomerModel).filter(and_(CustomerModel.Makulerad == 0)).all() + cust = db.raw_session.query(CustomerModel).filter(and_(CustomerModel.Makulerad == 0)).all() return [c.CompanyModel for c in cust] @staticmethod - def get_customer_numbers(category_list=[10], class_list=[], filter_inactive=True): + def get_customers(category_list=[10], class_list=[], filter_inactive=True, filters=and_()): category_in = CustomerModel.kundkategorikod.in_(category_list) if category_list else and_() class_in = CustomerModel.kundklass.in_(class_list) if class_list else and_() inactive = and_(CustomerModel.Makulerad == 0) if filter_inactive else and_() - cust = db.raw.query(CustomerModel).options( - Load(CustomerModel).noload('*')).filter( - and_(category_in, class_in, inactive)).all() + return db.raw_session.query(CustomerModel).options( + Load(CompanyModel).noload('*')).filter( + and_(category_in, class_in, inactive, filters)).all() + + @staticmethod + def get_customer_numbers(category_list=[10], class_list=[], filter_inactive=True): + cust = Company.get_customers(category_list, class_list, filter_inactive) return [c.FtgNr for c in cust] @staticmethod @@ -45,24 +49,71 @@ class Company(): if ftg_nr: ftg_filter = CompanyModel.FtgNr.in_(ftg_nr) - return db.raw.query(CompanyModel).join(CustomerModel).filter( + return db.raw_session.query(CompanyModel).join(CustomerModel).filter( and_(ftg_filter, filter_)).order_by( CompanyModel.FtgNr.desc()).offset(offset).limit(limit).all() +# TODO: Should be moved to separate project with Lindvalls specific code +def update_customer_delivery_from_csv(filename='zip_codes_svhl.csv'): + SVHL_ZONES = {} + + logger.info("Get customers") + customers = Company.get_customers(filters=( + and_(CustomerModel.LevSattKod == 3, CustomerModel.kundklass == None))) # noqa + logger.info("Amount of customers is %d" % len(customers)) + + import csv + with open(filename, newline='') as csvfile: + shelfreader = csv.reader(csvfile, delimiter=',') + headers = shelfreader.__next__() + logger.info('Found these columns: %s' % (', '.join(headers))) + for row in shelfreader: + SVHL_ZONES[row[0]] = '%s - %s' % (row[2], row[1]) + + logger.info('Length of zones dict is %d' % (len(SVHL_ZONES))) + + customers_to_update = 0 + + for customer in customers: + + if customer.Company.FtgLevPostNr: + FtgLevPostNr = customer.Company.FtgLevPostNr.strip().replace(" ", "") + if FtgLevPostNr and FtgLevPostNr in SVHL_ZONES: + logger.info('FtgLevPostNr: %s - %s is within SVHL zone %s' % ( + customer.FtgNr, customer.Company.FtgNamn, SVHL_ZONES[FtgLevPostNr])) + customers_to_update += 1 + customer.LevSattKod = 11 + continue + # Return? Break? + if customer.Company.FtgPostnr: + FtgPostnr = customer.Company.FtgPostnr.strip().replace(" ", "") + if FtgPostnr and FtgPostnr in SVHL_ZONES: + logger.info('FtgPostnr: %s - %s is within SVHL zone %s' % ( + customer.FtgNr, customer.Company.FtgNamn, SVHL_ZONES[FtgPostnr])) + customer.LevSattKod = 11 + customers_to_update += 1 + + logger.info('Amount updated %d' % customers_to_update) + + # db.raw_db.merge(n1) + db.raw_db.commit() + logger.info('Succesfully commited updated customers to database') + + if __name__ == '__main__': # print([column.key for column in CompanyModel.__table__.columns]) - logger.info("Starting TEST") + # logger.info("Starting TEST") # session = RawSession() - logger.info("Testing gettings a company") + # logger.info("Testing gettings a company") # c1 = session.query(CompanyModel).filter_by(FtgNr="179580").first() # print(CompanyModel) # print(CompanyModel.get_list(['406569', '179580', '2440070', '179584'])) - from pprint import pprint - pprint(CompanyModel.get('179584').to_dict()) + # from pprint import pprint + # pprint(CompanyModel.get('179584').to_dict()) # c1 = CompanyModel.query.filter_by(FtgNr="406569").first() # print(c1) @@ -73,3 +124,6 @@ if __name__ == '__main__': # ) # print(CompanyModel.get_all_active_customers()[0].CompanyModel) + + logger.info("Starting") + update_customer_delivery_from_csv() diff --git a/requirements.txt b/requirements.txt index 26f5eed..7993e5f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,9 @@ nose==1.3.7 -Sphinx==1.8.5 -pymssql==2.1.4 -SQLAlchemy==1.3.1 -sqlservice==1.1.3 -PyMySQL==0.9.3 -alembic==1.0.8 -PyYAML==5.1 \ No newline at end of file +Sphinx==3.2.1 +pymssql-py38==2.1.4 +SQLAlchemy==1.3.22 +sqlservice==1.2.1 +PyMySQL==0.10.0 +alembic==1.4.2 +PyYAML==5.3.1 +gtin==0.1.13 \ No newline at end of file diff --git a/setup.py b/setup.py index bf97f9d..6c9064a 100755 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ with open('LICENSE') as f: setup( name='pyjeeves', version='0.0.1', - description='PyJeeves syncronization module', + description='PyJeeves communication module', long_description=readme, author='Marcus Lindvall', author_email='marcus.lindvall@lindvallskaffe.se', @@ -22,11 +22,12 @@ setup( install_requires=[ 'nose', 'sphinx', - 'pymssql', + 'pymssql-py38', 'sqlalchemy', 'sqlservice', 'PyMySQL', 'alembic', 'pyyaml', + 'gtin' ] )