From 2d61903cf7fb557473c7d6912264084414c2af2d Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 27 Feb 2020 21:52:05 -0600 Subject: [PATCH 001/136] Use twine to upload released package to PyPI --- tasks.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tasks.py b/tasks.py index 38d2267..a9b9fc3 100644 --- a/tasks.py +++ b/tasks.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018 Lance Edgar +# Copyright © 2018-2020 Lance Edgar # # This file is part of pyCOREPOS. # @@ -24,17 +24,21 @@ Tasks for 'pyCOREPOS' package """ -from __future__ import unicode_literals, absolute_import - +import os import shutil from invoke import task +here = os.path.abspath(os.path.dirname(__file__)) +exec(open(os.path.join(here, 'corepos', '_version.py')).read()) + + @task def release(ctx): """ Release a new version of 'pyCOREPOS'. """ shutil.rmtree('pyCOREPOS.egg-info') - ctx.run('python setup.py sdist --formats=gztar upload') + ctx.run('python setup.py sdist --formats=gztar') + ctx.run('twine upload dist/pyCOREPOS-{}.tar.gz'.format(__version__)) From a0efa1a967369d93fbf798466654f07bc0286c21 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 1 Mar 2020 20:01:05 -0600 Subject: [PATCH 002/136] Add model for datasync changes queue table just in case it's there, for use w/ datasync watcher --- corepos/db/office_op/model.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 824ec07..e1e54f7 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -21,12 +21,9 @@ # ################################################################################ """ -CORE POS Data Model +Data model for CORE POS "office_op" DB """ -from __future__ import unicode_literals, absolute_import - -import six import sqlalchemy as sa from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base @@ -36,7 +33,23 @@ from sqlalchemy.ext.associationproxy import association_proxy Base = declarative_base() -@six.python_2_unicode_compatible +class Change(Base): + """ + Represents a changed (or deleted) record, which is pending synchronization + to another system(s). + + .. note:: + This table may or may not be installed to a given CORE Office Op DB. Its + presence is required if Rattail datasync needs to "watch" the DB. + """ + __tablename__ = 'datasync_changes' + + id = sa.Column(sa.Integer(), nullable=False, primary_key=True) + object_type = sa.Column(sa.String(length=255), nullable=False) + object_key = sa.Column(sa.String(length=255), nullable=False) + deleted = sa.Column(sa.Boolean(), nullable=False, default=False) + + class Parameter(Base): """ Represents a "parameter" value. @@ -57,7 +70,6 @@ class Parameter(Base): return "{}-{} {}".format(self.store_id, self.lane_id, self.param_key) -@six.python_2_unicode_compatible class Department(Base): """ Represents a department within the organization. @@ -95,7 +107,6 @@ class Department(Base): return self.name or '' -@six.python_2_unicode_compatible class Subdepartment(Base): """ Represents a subdepartment within the organization. @@ -120,7 +131,6 @@ class Subdepartment(Base): return self.name or '' -@six.python_2_unicode_compatible class Vendor(Base): """ Represents a vendor from which product may be purchased. @@ -184,7 +194,6 @@ class VendorContact(Base): notes = sa.Column(sa.Text(), nullable=True) -@six.python_2_unicode_compatible class Product(Base): """ Represents a product, purchased and/or sold by the organization. @@ -303,7 +312,6 @@ class Product(Base): return self.description or '' -@six.python_2_unicode_compatible class ProductFlag(Base): """ Represents a product flag attribute. @@ -320,7 +328,6 @@ class ProductFlag(Base): return self.description or '' -@six.python_2_unicode_compatible class Employee(Base): """ Represents an employee within the organization. @@ -351,7 +358,6 @@ class Employee(Base): return ' '.join([self.first_name or '', self.last_name or '']).strip() -@six.python_2_unicode_compatible class MemberType(Base): """ Represents a type of membership within the organization. @@ -378,7 +384,6 @@ class MemberType(Base): return self.description or "" -@six.python_2_unicode_compatible class Customer(Base): """ Represents a customer of the organization. @@ -452,7 +457,6 @@ class Customer(Base): return "{} {}".format(self.first_name or '', self.last_name or '').strip() -@six.python_2_unicode_compatible class MemberInfo(Base): """ Contact info regarding a member of the organization. @@ -517,7 +521,6 @@ class MemberInfo(Base): return self.full_name -@six.python_2_unicode_compatible class MemberDate(Base): """ Join/exit dates for members @@ -536,7 +539,6 @@ class MemberDate(Base): self.end_date.date() if self.end_date else "??") -@six.python_2_unicode_compatible class MemberContact(Base): """ Contact preferences for members @@ -566,7 +568,6 @@ class MemberContact(Base): return str(self.preference) -@six.python_2_unicode_compatible class HouseCoupon(Base): """ Represents a "house" (store) coupon. From 7fc5ae9b4ed216a9971779582dbdac922ec810dc Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 3 Mar 2020 21:35:39 -0600 Subject: [PATCH 003/136] Add basic CORE webservices API client, for Vendor data lots more to come yet, once the basic patterns are proven --- corepos/api.py | 174 ++++++++++++++++++++++++++++++++++ corepos/db/office_op/model.py | 5 +- 2 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 corepos/api.py diff --git a/corepos/api.py b/corepos/api.py new file mode 100644 index 0000000..c444eae --- /dev/null +++ b/corepos/api.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# pyCOREPOS -- Python Interface to CORE POS +# Copyright © 2018-2020 Lance Edgar +# +# This file is part of pyCOREPOS. +# +# pyCOREPOS is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# pyCOREPOS is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# pyCOREPOS. If not, see . +# +################################################################################ +""" +CORE-POS webservices API +""" + +import json +import logging + +import requests + + +log = logging.getLogger(__name__) + + +class CoreAPIError(Exception): + """ + Base class for errors coming from the CORE API proper. + """ + + def __init__(self, message): + self.message = message + + def __str__(self): + return "CORE API returned an error: {}".format(self.message) + + +class CoreWebAPI(object): + """ + Client implementation for the CORE webservices API. + """ + + def __init__(self, url, verify=True): + """ + Constructor for the API client. + + :param str url: URL to the CORE webservices API, + e.g. ``'http://localhost/fannie/ws/'`` + + :param bool verify: How to handle certificate validation for HTTPS + URLs. This value is passed as-is to the ``requests`` library, so + see those docs for more info. The default value for this is + ``True`` because the assumption is that security should be on by + default. Set it to ``False`` in order to disable validation + entirely, e.g. for self-signed certs. (This may also be needed for + basic HTTP URLs?) Other values may be possible also; again see the + ``requests`` docs for more info. + """ + self.url = url + self.verify = verify + + def post(self, params, method=None): + """ + Issue a POST request to the API, with the given ``params``. If not + specified, ``method`` will be CORE's ``FannieEntity`` webservice. + """ + if not method: + method = r'\COREPOS\Fannie\API\webservices\FannieEntity' + + payload = { + 'jsonrpc': '2.0', + 'method': method, + 'params': params, + # we're not dealing with async here, so KISS for this 'id' + # https://stackoverflow.com/questions/4390369/json-rpc-how-can-one-make-a-unique-id#comment4786119_4391070 + 'id': 1, + } + + response = requests.post(self.url, data=json.dumps(payload), + verify=self.verify) + response.raise_for_status() + return response + + def parse_response(self, response): + """ + Generic method to "parse" a response from the API. Really this just + converts the JSON to a dict (etc.), and then checks for error. If an + error is found in the response, it will be raised here. + """ + try: + js = response.json() + except: + raise CoreAPIError("Received invalid response: {}".format(response.content)) + + if 'error' in js: + raise CoreAPIError(js['error']) + + assert set(js.keys()) == set(['jsonrpc', 'id', 'result']) + assert set(js['result'].keys()) == set(['result']) + return js['result']['result'] + + def get_vendors(self, **columns): + """ + Fetch some or all of Vendor records from CORE. + + :returns: A (potentially empty) list of vendor dict records. + + To fetch all vendors:: + + api.get_vendors() + + To fetch only vendors named "UNFI":: + + api.get_vendors(vendorName='UNFI') + """ + params = { + 'entity': 'Vendors', + 'submethod': 'get', + 'columns': columns, + } + response = self.post(params) + result = self.parse_response(response) + return [json.loads(rec) for rec in result] + + def get_vendor(self, vendorID, **columns): + """ + Fetch an existing Vendor record from CORE. + + :returns: Either a vendor dict record, or ``None``. + """ + columns['vendorID'] = vendorID + params = { + 'entity': 'Vendors', + 'submethod': 'get', + 'columns': columns, + } + response = self.post(params) + result = self.parse_response(response) + if result: + if len(result) > 1: + log.warning("CORE API returned %s vendor results", len(result)) + return json.loads(result[0]) + + def set_vendor(self, vendorID, **columns): + """ + Update an existing Vendor record in CORE. + + :returns: Boolean indicating success of the operation. + """ + columns['vendorID'] = vendorID + params = { + 'entity': 'Vendors', + 'submethod': 'set', + 'columns': columns, + } + response = self.post(params) + result = self.parse_response(response) + + if result == 'OK': + return True + + # TODO: need to see what happens here, when it happens + raise NotImplementedError("Unexpected result for set_vendor: {}".format(result)) + # return False diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index e1e54f7..1227039 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -137,7 +137,10 @@ class Vendor(Base): """ __tablename__ = 'vendors' - id = sa.Column('vendorID', sa.Integer(), primary_key=True, autoincrement=False, nullable=False) + # TODO: this maybe should be the pattern we use going forward, for all + # models? for now it was deemed necessary to "match" the API output + vendorID = sa.Column(sa.Integer(), primary_key=True, autoincrement=False, nullable=False) + id = orm.synonym('vendorID') name = sa.Column('vendorName', sa.String(length=50), nullable=True) From 73b0acecdd1514d6a7cd9b0a4a3dcc45649a6cbf Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 3 Mar 2020 21:49:16 -0600 Subject: [PATCH 004/136] Declare dependency for 'requests' lib --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index d9afeec..098d0fc 100644 --- a/setup.py +++ b/setup.py @@ -61,6 +61,7 @@ requires = [ # package # low high 'mysql-connector-python', # 8.0.6 + 'requests', # 2.23.0 'six', # 1.12.0 'SQLAlchemy', # 0.9.8 ] From a12c9de4152c71a3680fa5d699835a293aa917c1 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 4 Mar 2020 18:54:34 -0600 Subject: [PATCH 005/136] Tweak how we handle return value from API 'set' method for Vendors also add note about using `set_vendor()` for creating new ones. and misc. other cleanup --- corepos/api.py | 14 +++++++------- corepos/db/util.py | 6 ++---- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/corepos/api.py b/corepos/api.py index c444eae..e67a0ff 100644 --- a/corepos/api.py +++ b/corepos/api.py @@ -156,6 +156,12 @@ class CoreWebAPI(object): Update an existing Vendor record in CORE. :returns: Boolean indicating success of the operation. + + .. note:: + Currently this is being used to create a *new* vendor also. CORE's + ``vendors`` table does not use auto-increment for its PK, which + means we must provide one even when creating; therefore this method + may be used for that. """ columns['vendorID'] = vendorID params = { @@ -165,10 +171,4 @@ class CoreWebAPI(object): } response = self.post(params) result = self.parse_response(response) - - if result == 'OK': - return True - - # TODO: need to see what happens here, when it happens - raise NotImplementedError("Unexpected result for set_vendor: {}".format(result)) - # return False + return json.loads(result) diff --git a/corepos/db/util.py b/corepos/db/util.py index 582aa17..324a057 100644 --- a/corepos/db/util.py +++ b/corepos/db/util.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2019 Lance Edgar +# Copyright © 2018-2020 Lance Edgar # # This file is part of pyCOREPOS. # @@ -24,11 +24,9 @@ CORE POS Database Utilities """ -from __future__ import unicode_literals, absolute_import - import sqlalchemy as sa -from corepos.db import model as corepos +from corepos.db.office_op import model as corepos def get_last_card_number(session): From deea31597cc14756c33fcbd64b7532d0c3f9d481 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 6 Mar 2020 13:47:32 -0600 Subject: [PATCH 006/136] Try out the "synonym" approach for all Vendor model fields this seems promising, but let's see if anything breaks first --- corepos/db/office_op/model.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 1227039..f38f75a 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -142,11 +142,14 @@ class Vendor(Base): vendorID = sa.Column(sa.Integer(), primary_key=True, autoincrement=False, nullable=False) id = orm.synonym('vendorID') - name = sa.Column('vendorName', sa.String(length=50), nullable=True) + vendorName = sa.Column(sa.String(length=50), nullable=True) + name = orm.synonym('vendorName') - abbreviation = sa.Column('vendorAbbreviation', sa.String(length=10), nullable=True) + vendorAbbreviation = sa.Column(sa.String(length=10), nullable=True) + abbreviation = orm.synonym('vendorAbbreviation') - discount_rate = sa.Column('discountRate', sa.Float(), nullable=True) + discountRate = sa.Column(sa.Float(), nullable=True) + discount_rate = orm.synonym('discountRate') contact = orm.relationship( 'VendorContact', From b76e82975a7487eea8a57de4ed1684ef3538001e Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 15 Mar 2020 14:27:22 -0500 Subject: [PATCH 007/136] Add GET methods to API, for departments, subdepartments, products --- corepos/api.py | 126 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/corepos/api.py b/corepos/api.py index e67a0ff..67e7f14 100644 --- a/corepos/api.py +++ b/corepos/api.py @@ -109,6 +109,90 @@ class CoreWebAPI(object): assert set(js['result'].keys()) == set(['result']) return js['result']['result'] + def get_departments(self, **columns): + """ + Fetch some or all of Department records from CORE. + + :returns: A (potentially empty) list of department dict records. + + To fetch all departments:: + + api.get_departments() + + To fetch only departments named "Grocery":: + + api.get_departments(dept_name='Grocery') + """ + params = { + 'entity': 'Departments', + 'submethod': 'get', + 'columns': columns, + } + response = self.post(params) + result = self.parse_response(response) + return [json.loads(rec) for rec in result] + + def get_department(self, dept_no, **columns): + """ + Fetch an existing Department record from CORE. + + :returns: Either a department dict record, or ``None``. + """ + columns['dept_no'] = dept_no + params = { + 'entity': 'Departments', + 'submethod': 'get', + 'columns': columns, + } + response = self.post(params) + result = self.parse_response(response) + if result: + if len(result) > 1: + log.warning("CORE API returned %s department results", len(result)) + return json.loads(result[0]) + + def get_subdepartments(self, **columns): + """ + Fetch some or all of Subdepartment records from CORE. + + :returns: A (potentially empty) list of subdepartment dict records. + + To fetch all subdepartments:: + + api.get_subdepartments() + + To fetch only subdepartments named "Grocery":: + + api.get_subdepartments(subdept_name='Grocery') + """ + params = { + 'entity': 'SubDepts', + 'submethod': 'get', + 'columns': columns, + } + response = self.post(params) + result = self.parse_response(response) + return [json.loads(rec) for rec in result] + + def get_subdepartment(self, subdept_no, **columns): + """ + Fetch an existing Subdepartment record from CORE. + + :returns: Either a subdepartment dict record, or ``None``. + """ + columns['subdept_no'] = subdept_no + params = { + 'entity': 'SubDepts', + 'submethod': 'get', + 'columns': columns, + } + response = self.post(params) + result = self.parse_response(response) + if result: + if len(result) > 1: + log.warning("CORE API returned %s subdepartment results", len(result)) + return json.loads(result[0]) + def get_vendors(self, **columns): """ Fetch some or all of Vendor records from CORE. @@ -172,3 +256,45 @@ class CoreWebAPI(object): response = self.post(params) result = self.parse_response(response) return json.loads(result) + + def get_products(self, **columns): + """ + Fetch some or all of Product records from CORE. + + :returns: A (potentially empty) list of product dict records. + + To fetch all products:: + + api.get_products() + + To fetch only products with brand name "Braggs":: + + api.get_products(brand='Braggs') + """ + params = { + 'entity': 'Products', + 'submethod': 'get', + 'columns': columns, + } + response = self.post(params) + result = self.parse_response(response) + return [json.loads(rec) for rec in result] + + def get_product(self, upc, **columns): + """ + Fetch an existing Product record from CORE. + + :returns: Either a product dict record, or ``None``. + """ + columns['upc'] = upc + params = { + 'entity': 'Products', + 'submethod': 'get', + 'columns': columns, + } + response = self.post(params) + result = self.parse_response(response) + if result: + if len(result) > 1: + log.warning("CORE API returned %s product results", len(result)) + return json.loads(result[0]) From 2d0cfa30ca4051d423cc452a5d81e4b8a6cc0631 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 15 Mar 2020 15:52:28 -0500 Subject: [PATCH 008/136] Add "set" API methods for Department, Subdepartment, Product --- corepos/api.py | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/corepos/api.py b/corepos/api.py index 67e7f14..b324f96 100644 --- a/corepos/api.py +++ b/corepos/api.py @@ -151,6 +151,28 @@ class CoreWebAPI(object): log.warning("CORE API returned %s department results", len(result)) return json.loads(result[0]) + def set_department(self, dept_no, **columns): + """ + Update an existing Department record in CORE. + + :returns: Boolean indicating success of the operation. + + .. note:: + Currently this is being used to create a *new* department also. CORE's + ``departments`` table does not use auto-increment for its PK, which + means we must provide one even when creating; therefore this method + may be used for that. + """ + columns['dept_no'] = dept_no + params = { + 'entity': 'Departments', + 'submethod': 'set', + 'columns': columns, + } + response = self.post(params) + result = self.parse_response(response) + return json.loads(result) + def get_subdepartments(self, **columns): """ Fetch some or all of Subdepartment records from CORE. @@ -193,6 +215,28 @@ class CoreWebAPI(object): log.warning("CORE API returned %s subdepartment results", len(result)) return json.loads(result[0]) + def set_subdepartment(self, subdept_no, **columns): + """ + Update an existing Subdepartment record in CORE. + + :returns: Boolean indicating success of the operation. + + .. note:: + Currently this is being used to create a *new* subdepartment also. CORE's + ``subdepartments`` table does not use auto-increment for its PK, which + means we must provide one even when creating; therefore this method + may be used for that. + """ + columns['subdept_no'] = subdept_no + params = { + 'entity': 'SubDepts', + 'submethod': 'set', + 'columns': columns, + } + response = self.post(params) + result = self.parse_response(response) + return json.loads(result) + def get_vendors(self, **columns): """ Fetch some or all of Vendor records from CORE. @@ -298,3 +342,25 @@ class CoreWebAPI(object): if len(result) > 1: log.warning("CORE API returned %s product results", len(result)) return json.loads(result[0]) + + def set_product(self, upc, **columns): + """ + Update an existing Product record in CORE. + + :returns: Boolean indicating success of the operation. + + .. note:: + Currently this is being used to create a *new* product also. CORE's + ``products`` table does not use auto-increment for its PK, which + means we must provide one even when creating; therefore this method + may be used for that. + """ + columns['upc'] = upc + params = { + 'entity': 'Products', + 'submethod': 'set', + 'columns': columns, + } + response = self.post(params) + result = self.parse_response(response) + return json.loads(result) From 5509089741dd8cda1408285093b97f343ff57384 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 15 Mar 2020 19:29:01 -0500 Subject: [PATCH 009/136] Add `get_customer()` API method --- corepos/api.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/corepos/api.py b/corepos/api.py index b324f96..9a6b5de 100644 --- a/corepos/api.py +++ b/corepos/api.py @@ -75,7 +75,9 @@ class CoreWebAPI(object): specified, ``method`` will be CORE's ``FannieEntity`` webservice. """ if not method: - method = r'\COREPOS\Fannie\API\webservices\FannieEntity' + method = 'FannieEntity' + if '\\' not in method: + method = r'\COREPOS\Fannie\API\webservices\{}'.format(method) payload = { 'jsonrpc': '2.0', @@ -91,7 +93,7 @@ class CoreWebAPI(object): response.raise_for_status() return response - def parse_response(self, response): + def parse_response(self, response, method=None): """ Generic method to "parse" a response from the API. Really this just converts the JSON to a dict (etc.), and then checks for error. If an @@ -105,10 +107,30 @@ class CoreWebAPI(object): if 'error' in js: raise CoreAPIError(js['error']) + # note, the result data format may depend on the API method involved + if method == 'FannieMember': + return js['result'] + + # assuming typical FannieEntity result here assert set(js.keys()) == set(['jsonrpc', 'id', 'result']) assert set(js['result'].keys()) == set(['result']) return js['result']['result'] + def get_customer(self, cardNo, **columns): + """ + Fetch an existing Customer record from CORE. + + :returns: Either a customer dict record, or ``None``. + """ + params = { + 'cardNo': cardNo, + 'method': 'get', + } + response = self.post(params, method='FannieMember') + result = self.parse_response(response, method='FannieMember') + if result: + return result + def get_departments(self, **columns): """ Fetch some or all of Department records from CORE. From 92f522cf6561f7d1c6bd77ee8d5f4111541df1c8 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 16 Mar 2020 16:10:49 -0500 Subject: [PATCH 010/136] Rename "old" Customer model, to CustData need to make room for "new" Customer model --- corepos/db/office_op/model.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index f38f75a..ba24d7d 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -390,7 +390,7 @@ class MemberType(Base): return self.description or "" -class Customer(Base): +class CustData(Base): """ Represents a customer of the organization. """ @@ -451,7 +451,7 @@ class Customer(Base): member_info = orm.relationship( 'MemberInfo', - primaryjoin='MemberInfo.card_number == Customer.card_number', + primaryjoin='MemberInfo.card_number == CustData.card_number', foreign_keys=[card_number], uselist=False, back_populates='customers', @@ -463,6 +463,10 @@ class Customer(Base): return "{} {}".format(self.first_name or '', self.last_name or '').strip() +# TODO: deprecate / remove this (so we can repurpose, for 'Customers' table) +Customer = CustData + + class MemberInfo(Base): """ Contact info regarding a member of the organization. @@ -496,13 +500,13 @@ class MemberInfo(Base): ads_ok = sa.Column('ads_OK', sa.Boolean(), nullable=True, default=True) customers = orm.relationship( - Customer, - primaryjoin=Customer.card_number == card_number, - foreign_keys=[Customer.card_number], + CustData, + primaryjoin=CustData.card_number == card_number, + foreign_keys=[CustData.card_number], back_populates='member_info', - remote_side=Customer.card_number, + remote_side=CustData.card_number, doc=""" - List of :class:`Customer` instances which are associated with this member info. + List of :class:`CustData` instances which are associated with this member info. """) dates = orm.relationship( From 9e850496e733c71f50b58f76abf6c9f8b85dca69 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 16 Mar 2020 16:54:21 -0500 Subject: [PATCH 011/136] Add new Customer and CustomerAccount models --- corepos/db/office_op/model.py | 119 ++++++++++++++++++++++++++++++++-- 1 file changed, 115 insertions(+), 4 deletions(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index ba24d7d..8f67aa8 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -390,9 +390,124 @@ class MemberType(Base): return self.description or "" +class CustomerAccount(Base): + """ + This represents the customer account itself, and not a "person" per se. + + https://github.com/CORE-POS/IS4C/blob/master/fannie/classlib2.0/data/models/op/CustomerAccountsModel.php + """ + __tablename__ = 'CustomerAccounts' + + id = sa.Column('customerAccountID', sa.Integer(), primary_key=True, autoincrement=True, nullable=False) + + card_number = sa.Column('cardNo', sa.Integer(), nullable=True, + unique=True, index=True) + + member_status = sa.Column('memberStatus', sa.String(length=10), nullable=True, + default='PC') + + active_status = sa.Column('activeStatus', sa.String(length=10), nullable=True, + default='') + + customer_type_id = sa.Column('customerTypeID', sa.Integer(), nullable=True, + default=1) + + charge_balance = sa.Column('chargeBalance', sa.Numeric(precision=10, scale=2), nullable=True, + default=0) + + charge_limit = sa.Column('chargeLimit', sa.Numeric(precision=10, scale=2), nullable=True, + default=0) + + id_card_upc = sa.Column('idCardUPC', sa.String(length=13), nullable=True) + + start_date = sa.Column('startDate', sa.DateTime(), nullable=True) + + end_date = sa.Column('endDate', sa.DateTime(), nullable=True) + + address_first_line = sa.Column('addressFirstLine', sa.String(length=100), nullable=True) + + address_second_line = sa.Column('addressSecondLine', sa.String(length=100), nullable=True) + + city = sa.Column(sa.String(length=50), nullable=True) + + state = sa.Column(sa.String(length=10), nullable=True) + + zip = sa.Column(sa.String(length=10), nullable=True) + + contact_allowed = sa.Column('contactAllowed', sa.Boolean(), nullable=True, + default=True) + + contact_method = sa.Column('contactMethod', sa.String(length=10), nullable=True, + default='mail') + + modified = sa.Column(sa.DateTime(), nullable=True) + + def __str__(self): + return "Account ID-{}".format(self.id) + + +class Customer(Base): + """ + This really represents a "person" attached to a proper "customer account". + + https://github.com/CORE-POS/IS4C/blob/master/fannie/classlib2.0/data/models/op/CustomersModel.php + """ + __tablename__ = 'Customers' + + id = sa.Column('customerID', sa.Integer(), primary_key=True, autoincrement=True, nullable=False) + + account_id = sa.Column('customerAccountID', sa.Integer(), + sa.ForeignKey('CustomerAccounts.customerAccountID'), + nullable=True) + account = orm.relationship(CustomerAccount) + + card_number = sa.Column('cardNo', sa.Integer(), nullable=True) + + first_name = sa.Column('firstName', sa.String(length=50), nullable=True) + + last_name = sa.Column('lastName', sa.String(length=50), nullable=True) + + charge_allowed = sa.Column('chargeAllowed', sa.Boolean(), nullable=True, + default=True) + + checks_allowed = sa.Column('checksAllowed', sa.Boolean(), nullable=True, + default=True) + + discount = sa.Column(sa.Boolean(), nullable=True, + default=False) + + account_holder = sa.Column('accountHolder', sa.Boolean(), nullable=True, + default=False) + + staff = sa.Column(sa.Boolean(), nullable=True, + default=False) + + phone = sa.Column(sa.String(length=20), nullable=True) + + alternate_phone = sa.Column('altPhone', sa.String(length=20), nullable=True) + + email = sa.Column(sa.String(length=100), nullable=True) + + member_pricing_allowed = sa.Column('memberPricingAllowed', sa.Boolean(), nullable=True, + default=False) + + member_coupons_allowed = sa.Column('memberCouponsAllowed', sa.Boolean(), nullable=True, + default=False) + + low_income_benefits = sa.Column('lowIncomeBenefits', sa.Boolean(), nullable=True, + default=False) + + modified = sa.Column(sa.DateTime(), nullable=True) + + def __str__(self): + return "{} {}".format(self.first_name or '', self.last_name or '').strip() + + class CustData(Base): """ Represents a customer of the organization. + + https://github.com/CORE-POS/IS4C/blob/master/fannie/classlib2.0/data/models/op/CustdataModel.php """ __tablename__ = 'custdata' @@ -463,10 +578,6 @@ class CustData(Base): return "{} {}".format(self.first_name or '', self.last_name or '').strip() -# TODO: deprecate / remove this (so we can repurpose, for 'Customers' table) -Customer = CustData - - class MemberInfo(Base): """ Contact info regarding a member of the organization. From d8e93c9d2ee6ca54bf8f838e94f4310342b59398 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 17 Mar 2020 16:04:22 -0500 Subject: [PATCH 012/136] Add `set_member()` API method also rename `get_customer()` to `get_member` --- corepos/api.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/corepos/api.py b/corepos/api.py index 9a6b5de..5c2a66f 100644 --- a/corepos/api.py +++ b/corepos/api.py @@ -116,11 +116,11 @@ class CoreWebAPI(object): assert set(js['result'].keys()) == set(['result']) return js['result']['result'] - def get_customer(self, cardNo, **columns): + def get_member(self, cardNo): """ - Fetch an existing Customer record from CORE. + Fetch an existing Member record from CORE. - :returns: Either a customer dict record, or ``None``. + :returns: Either a member dict record, or ``None``. """ params = { 'cardNo': cardNo, @@ -131,6 +131,27 @@ class CoreWebAPI(object): if result: return result + def set_member(self, cardNo, **kwargs): + """ + Update an existing Member record in CORE. + + :returns: Boolean indicating success of the operation. + + .. warning:: + Only simple updates have been attempted thus far; have yet to try + creation or deletion. Neither of those should be expected to work. + """ + kwargs['cardNo'] = cardNo + params = { + 'cardNo': cardNo, + 'method': 'set', + 'member': kwargs, + } + response = self.post(params, method='FannieMember') + result = self.parse_response(response, method='FannieMember') + if result: + return result + def get_departments(self, **columns): """ Fetch some or all of Department records from CORE. From 573595497ec07712eb35a6652c9a6f7292463f9b Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 18 Mar 2020 11:28:19 -0500 Subject: [PATCH 013/136] Add `get_members()` API method --- corepos/api.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/corepos/api.py b/corepos/api.py index 5c2a66f..b66613d 100644 --- a/corepos/api.py +++ b/corepos/api.py @@ -116,6 +116,20 @@ class CoreWebAPI(object): assert set(js['result'].keys()) == set(['result']) return js['result']['result'] + def get_members(self): + """ + Fetch all Member records from CORE. + + :returns: A (potentially empty) list of member dict records. + """ + params = { + 'method': 'get', + 'cardNo': None, + } + response = self.post(params, method='FannieMember') + result = self.parse_response(response, method='FannieMember') + return result + def get_member(self, cardNo): """ Fetch an existing Member record from CORE. From 9466b16b64575d2c0dbfab36044067471b19d11e Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 27 Mar 2020 20:50:00 -0500 Subject: [PATCH 014/136] Add DB models for TaxRate, TaxRateComponent --- corepos/db/office_op/model.py | 39 ++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 8f67aa8..1b2384a 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -200,6 +200,41 @@ class VendorContact(Base): notes = sa.Column(sa.Text(), nullable=True) +class TaxRate(Base): + """ + Represents a tax rate. Note that this may be a "combo" of various local + tax rates / levels. + """ + __tablename__ = 'taxrates' + + id = sa.Column(sa.Integer(), primary_key=True, autoincrement=False, nullable=False) + + rate = sa.Column(sa.Float(), nullable=True) + + description = sa.Column(sa.String(length=50), nullable=True) + + sales_code = sa.Column('salesCode', sa.Integer(), nullable=True) + + +class TaxRateComponent(Base): + """ + Represents a "component" of a tax rate. + """ + __tablename__ = 'TaxRateComponents' + __table_args__ = ( + sa.ForeignKeyConstraint(['taxRateID'], ['taxrates.id']), + ) + + id = sa.Column(sa.Integer(), primary_key=True, autoincrement=True, nullable=False) + + tax_rate_id = sa.Column('taxRateID', sa.Integer()) + tax_rate = orm.relationship(TaxRate, backref='components') + + rate = sa.Column(sa.Float(), nullable=True) + + description = sa.Column(sa.String(length=50), nullable=True) + + class Product(Base): """ Represents a product, purchased and/or sold by the organization. @@ -207,6 +242,7 @@ class Product(Base): __tablename__ = 'products' __table_args__ = ( sa.ForeignKeyConstraint(['department'], ['departments.dept_no']), + sa.ForeignKeyConstraint(['tax'], ['taxrates.id']), ) id = sa.Column(sa.Integer(), primary_key=True, autoincrement=True, nullable=False) @@ -243,7 +279,8 @@ class Product(Base): size = sa.Column(sa.String(length=9), nullable=True) - tax = sa.Column(sa.SmallInteger(), nullable=True) + tax_rate_id = sa.Column('tax', sa.SmallInteger(), nullable=True) + tax_rate = orm.relationship(TaxRate) foodstamp = sa.Column(sa.Boolean(), nullable=True) From e0058c003ddba043b9f99c7042813cae959dc546 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 29 Mar 2020 23:36:29 -0500 Subject: [PATCH 015/136] Add `Product.subdepartment` relationship --- corepos/db/office_op/model.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 1b2384a..99dee9f 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -242,6 +242,7 @@ class Product(Base): __tablename__ = 'products' __table_args__ = ( sa.ForeignKeyConstraint(['department'], ['departments.dept_no']), + sa.ForeignKeyConstraint(['subdept'], ['subdepts.subdept_no']), sa.ForeignKeyConstraint(['tax'], ['taxrates.id']), ) @@ -276,6 +277,13 @@ class Product(Base): end_date = sa.Column(sa.DateTime(), nullable=True) department_number = sa.Column('department', sa.SmallInteger(), nullable=True) + department = orm.relationship( + Department, + primaryjoin=Department.number == department_number, + foreign_keys=[department_number], + doc=""" + Reference to the :class:`Department` to which the product belongs. + """) size = sa.Column(sa.String(length=9), nullable=True) @@ -317,6 +325,13 @@ class Product(Base): flags = sa.Column('numflag', sa.Integer(), nullable=True, default=0) subdepartment_number = sa.Column('subdept', sa.SmallInteger(), nullable=True) + subdepartment = orm.relationship( + Subdepartment, + primaryjoin=Subdepartment.number == subdepartment_number, + foreign_keys=[subdepartment_number], + doc=""" + Reference to the :class:`Subdepartment` to which the product belongs. + """) deposit = sa.Column(sa.Float(), nullable=True) @@ -325,17 +340,6 @@ class Product(Base): store_id = sa.Column(sa.SmallInteger(), nullable=True, default=0) default_vendor_id = sa.Column(sa.Integer(), nullable=True, default=0) - - current_origin_id = sa.Column(sa.Integer(), nullable=True, default=0) - - department = orm.relationship( - Department, - primaryjoin=Department.number == department_number, - foreign_keys=[department_number], - doc=""" - Reference to the :class:`Department` to which the product belongs. - """) - vendor = orm.relationship( Vendor, primaryjoin=Vendor.id == default_vendor_id, @@ -344,6 +348,8 @@ class Product(Base): Reference to the default :class:`Vendor` from which the product is obtained. """) + current_origin_id = sa.Column(sa.Integer(), nullable=True, default=0) + @property def full_description(self): fields = ['brand', 'description', 'size'] From 07c24b63dcb13e86cd7c056a3743ea00c2cc3abd Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 29 Mar 2020 23:36:37 -0500 Subject: [PATCH 016/136] Fix how we interpret `Product.id_enforced` values will be an age, not a flag --- corepos/db/office_op/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 99dee9f..574e42b 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -316,7 +316,7 @@ class Product(Base): quantity_enforced = sa.Column('qttyEnforced', sa.Boolean(), nullable=True) - id_enforced = sa.Column('idEnforced', sa.Boolean(), nullable=True) + id_enforced = sa.Column('idEnforced', sa.SmallInteger(), nullable=True) cost = sa.Column(sa.Float(), nullable=True, default=0) From 3a440d20bc36ed5fdd21018fab78b9b09a5b7065 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 30 Mar 2020 00:05:26 -0500 Subject: [PATCH 017/136] Add enum for product price method --- corepos/enum.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/corepos/enum.py b/corepos/enum.py index 62c16ac..f0860ea 100644 --- a/corepos/enum.py +++ b/corepos/enum.py @@ -92,3 +92,14 @@ MEMBER_CONTACT_PREFERENCE = OrderedDict([ (MEMBER_CONTACT_PREFERENCE_EMAIL_ONLY, "email only"), (MEMBER_CONTACT_PREFERENCE_BOTH, "both (postal mail and email)"), ]) + + +PRODUCT_PRICE_METHOD_DISABLED = 0 +PRODUCT_PRICE_METHOD_ALWAYS = 1 +PRODUCT_PRICE_METHOD_FULL_SETS = 2 + +PRODUCT_PRICE_METHOD = OrderedDict([ + (PRODUCT_PRICE_METHOD_DISABLED, "Disabled"), + (PRODUCT_PRICE_METHOD_ALWAYS, "Always use this price"), + (PRODUCT_PRICE_METHOD_FULL_SETS, "Use this price for full sets"), +]) From 9b429eb2932df579d0137a40c03a6590b1dc7eea Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 30 Mar 2020 11:29:21 -0500 Subject: [PATCH 018/136] Revert "Try out the "synonym" approach for all Vendor model fields" This reverts commit deea31597cc14756c33fcbd64b7532d0c3f9d481. we'll keep the synonym in place for `vendorID` but the rest of these just caused problems, e.g. with tailbone grids --- corepos/db/office_op/model.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 574e42b..40c3b69 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -142,14 +142,11 @@ class Vendor(Base): vendorID = sa.Column(sa.Integer(), primary_key=True, autoincrement=False, nullable=False) id = orm.synonym('vendorID') - vendorName = sa.Column(sa.String(length=50), nullable=True) - name = orm.synonym('vendorName') + name = sa.Column('vendorName', sa.String(length=50), nullable=True) - vendorAbbreviation = sa.Column(sa.String(length=10), nullable=True) - abbreviation = orm.synonym('vendorAbbreviation') + abbreviation = sa.Column('vendorAbbreviation', sa.String(length=10), nullable=True) - discountRate = sa.Column(sa.Float(), nullable=True) - discount_rate = orm.synonym('discountRate') + discount_rate = sa.Column('discountRate', sa.Float(), nullable=True) contact = orm.relationship( 'VendorContact', From 5f3ad79a956e49c8b0a89828cce4b315f1767efc Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 30 Mar 2020 11:39:57 -0500 Subject: [PATCH 019/136] Add `VendorDepartment` and `VendorItem` models --- corepos/db/office_op/model.py | 95 ++++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 40c3b69..8cc34ea 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -197,6 +197,40 @@ class VendorContact(Base): notes = sa.Column(sa.Text(), nullable=True) +class VendorDepartment(Base): + """ + Represents specific details / settings for a given vendor in the context of + a given department. + """ + __tablename__ = 'vendorDepartments' + __table_args__ = ( + sa.ForeignKeyConstraint(['vendorID'], ['vendors.vendorID']), + sa.ForeignKeyConstraint(['deptID'], ['departments.dept_no']), + ) + + vendor_id = sa.Column('vendorID', sa.Integer(), primary_key=True, nullable=False) + vendor = orm.relationship( + Vendor, + doc=""" + Reference to the :class:`Vendor` to which this record applies. + """) + + department_id = sa.Column('deptID', sa.Integer(), primary_key=True, nullable=False) + department = orm.relationship( + Department, + doc=""" + Reference to the :class:`Department` to which this record applies. + """) + + name = sa.Column(sa.String(length=125), nullable=True) + + margin = sa.Column(sa.Float(), nullable=True) + + testing = sa.Column(sa.Float(), nullable=True) + + pos_department_id = sa.Column('posDeptID', sa.Integer(), nullable=True) + + class TaxRate(Base): """ Represents a tax rate. Note that this may be a "combo" of various local @@ -337,13 +371,15 @@ class Product(Base): store_id = sa.Column(sa.SmallInteger(), nullable=True, default=0) default_vendor_id = sa.Column(sa.Integer(), nullable=True, default=0) - vendor = orm.relationship( + default_vendor = orm.relationship( Vendor, primaryjoin=Vendor.id == default_vendor_id, foreign_keys=[default_vendor_id], doc=""" Reference to the default :class:`Vendor` from which the product is obtained. """) + # TODO: deprecate / remove this? + vendor = orm.synonym('default_vendor') current_origin_id = sa.Column(sa.Integer(), nullable=True, default=0) @@ -374,6 +410,63 @@ class ProductFlag(Base): return self.description or '' +class VendorItem(Base): + """ + Represents a "source" for a given item, from a given vendor. + """ + __tablename__ = 'vendorItems' + __table_args__ = ( + sa.ForeignKeyConstraint(['vendorID'], ['vendors.vendorID']), + ) + + sku = sa.Column(sa.String(length=13), primary_key=True, nullable=False) + + vendor_id = sa.Column('vendorID', sa.Integer(), primary_key=True, nullable=False) + vendor = orm.relationship( + Vendor, + doc=""" + Reference to the :class:`Vendor` from which the product is obtained. + """) + + # TODO: this should be autoincrement, but not primary key?? + vendor_item_id = sa.Column('vendorItemID', sa.Integer(), nullable=False) + + upc = sa.Column(sa.String(length=13), nullable=False) + product = orm.relationship( + Product, + primaryjoin=Product.upc == upc, + foreign_keys=[upc], + doc=""" + Reference to the :class:`Product` to which this record applies. + """, + backref=orm.backref( + 'vendor_items', + order_by=vendor_item_id, + doc=""" + List of :class:`VendorItem` records for this product. + """)) + + brand = sa.Column(sa.String(length=50), nullable=True) + + description = sa.Column(sa.String(length=50), nullable=True) + + size = sa.Column(sa.String(length=25), nullable=True) + + units = sa.Column(sa.Float(), nullable=True, default=1) + + cost = sa.Column(sa.Numeric(precision=10, scale=3), nullable=True) + + sale_cost = sa.Column('saleCost', sa.Numeric(precision=10, scale=3), + nullable=True, default=0) + + vendor_department_id = sa.Column('vendorDept', sa.Integer(), nullable=True, + default=0) + + srp = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True) + + modified = sa.Column(sa.DateTime(), nullable=True) + + class Employee(Base): """ Represents an employee within the organization. From 6ecfbf4e1a669afb2fd91c49e6a14df0ccca5cc3 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 30 Mar 2020 12:00:52 -0500 Subject: [PATCH 020/136] Add `ScaleItem` model --- corepos/db/office_op/model.py | 59 +++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 8cc34ea..7e19da0 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -467,6 +467,65 @@ class VendorItem(Base): modified = sa.Column(sa.DateTime(), nullable=True) +class ScaleItem(Base): + """ + Represents deli scale info for a given item. + """ + __tablename__ = 'scaleItems' + + plu = sa.Column(sa.String(length=13), primary_key=True, nullable=False) + product = orm.relationship( + Product, + primaryjoin=Product.upc == plu, + foreign_keys=[plu], + doc=""" + Reference to the :class:`Product` to which this record applies. + """, + backref=orm.backref( + 'scale_item', + uselist=False, + doc=""" + Reference to the :class:`ScaleItem` record for this product. + """)) + + price = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True) + + item_description = sa.Column('itemdesc', sa.String(length=100), nullable=True) + + exception_price = sa.Column('exceptionprice', sa.Numeric(precision=10, scale=2), nullable=True) + + exception_price = sa.Column('exceptionprice', sa.Numeric(precision=10, scale=2), nullable=True) + + weight = sa.Column(sa.SmallInteger(), nullable=True, default=0) + + by_count = sa.Column('bycount', sa.Boolean(), nullable=True, default=False) + + tare = sa.Column(sa.Float(), nullable=True, default=0) + + shelf_life = sa.Column('shelflife', sa.SmallInteger(), nullable=True, default=0) + + net_weight = sa.Column('netWeight', sa.SmallInteger(), nullable=True, default=0) + + text = sa.Column(sa.Text(), nullable=True) + + reporting_class = sa.Column('reportingClass', sa.String(length=6), nullable=True) + + label = sa.Column(sa.Integer(), nullable=True) + + graphics = sa.Column(sa.Integer(), nullable=True) + + modified = sa.Column(sa.DateTime(), nullable=True) + + # TODO: this was not in some older DBs + # linked_plu = sa.Column('linkedPLU', sa.String(length=13), nullable=True) + + # TODO: this was not in some older DBs + # mosa_statement = sa.Column('mosaStatement', sa.Boolean(), nullable=True, default=False) + + # TODO: this was not in some older DBs + # origin_text = sa.Column('originText', sa.String(length=100), nullable=True) + + class Employee(Base): """ Represents an employee within the organization. From acc85ba8de327ded4cca399fdc373ce2fff185ba Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 30 Mar 2020 12:24:05 -0500 Subject: [PATCH 021/136] Add `ProductUser` model --- corepos/db/office_op/model.py | 45 +++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 7e19da0..d5e4080 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -410,6 +410,51 @@ class ProductFlag(Base): return self.description or '' +class ProductUser(Base): + """ + Represents extended "user" info for a product (e.g. sale signage). + """ + __tablename__ = 'productUser' + + upc = sa.Column(sa.String(length=13), primary_key=True, nullable=False) + product = orm.relationship( + Product, + primaryjoin=Product.upc == upc, + foreign_keys=[upc], + doc=""" + Reference to the :class:`Product` to which this record applies. + """, + backref=orm.backref( + 'user_info', + uselist=False, + doc=""" + Reference to the :class:`ProductUser` record for this product, if any. + """)) + + description = sa.Column(sa.String(length=255), nullable=True) + + brand = sa.Column(sa.String(length=255), nullable=True) + + sizing = sa.Column(sa.String(length=255), nullable=True) + + photo = sa.Column(sa.String(length=255), nullable=True) + + # TODO: this was not in some older DBs + # nutrition_facts = sa.Column('nutritionFacts', sa.String(length=255), nullable=True) + + long_text = sa.Column(sa.Text(), nullable=True) + + enable_online = sa.Column('enableOnline', sa.Boolean(), nullable=True) + + sold_out = sa.Column('soldOut', sa.Boolean(), nullable=True, default=False) + + # TODO: this was not in some older DBs + # sign_count = sa.Column('signCount', sa.SmallInteger(), nullable=True, default=1) + + # TODO: this was not in some older DBs + # narrow = sa.Column(sa.Boolean(), nullable=True, default=False) + + class VendorItem(Base): """ Represents a "source" for a given item, from a given vendor. From 64e4b18cf0c3ef90c0f777da24058eb742be18f0 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 30 Mar 2020 13:06:53 -0500 Subject: [PATCH 022/136] Add `FloorSection` and `ProductPhysicalLocation` models --- corepos/db/office_op/model.py | 69 +++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index d5e4080..67e9be8 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -571,6 +571,75 @@ class ScaleItem(Base): # origin_text = sa.Column('originText', sa.String(length=100), nullable=True) +class FloorSection(Base): + """ + Represents a physical "floor section" within a store. + """ + __tablename__ = 'FloorSections' + + floorSectionID = sa.Column(sa.Integer(), primary_key=True, autoincrement=True, nullable=False) + id = orm.synonym('floorSectionID') + + store_id = sa.Column('storeID', sa.Integer(), nullable=True, default=1) + + name = sa.Column(sa.String(length=50), nullable=True) + + # TODO: this was not in some older DBs + # map_x = sa.Column('mapX', sa.Integer(), nullable=True, default=0) + + # TODO: this was not in some older DBs + # map_y = sa.Column('mapY', sa.Integer(), nullable=True, default=0) + + # TODO: this was not in some older DBs + # map_rotate = sa.Column('mapRotate', sa.Integer(), nullable=True, default=0) + + +class ProductPhysicalLocation(Base): + """ + Represents a physical location for a product + """ + __tablename__ = 'prodPhysicalLocation' + __table_args__ = ( + sa.ForeignKeyConstraint(['floorSectionID'], ['FloorSections.floorSectionID']), + ) + + upc = sa.Column(sa.String(length=13), primary_key=True, nullable=False) + product = orm.relationship( + Product, + primaryjoin=Product.upc == upc, + foreign_keys=[upc], + doc=""" + Reference to the :class:`Product` to which this record applies. + """, + backref=orm.backref( + 'physical_location', + uselist=False, + doc=""" + Reference to the :class:`ProductPhysicalLocation` record for this + product. + """)) + + store_id = sa.Column(sa.SmallInteger(), nullable=True, default=0) + + floor_section_id = sa.Column('floorSectionID', sa.Integer(), nullable=True) + floor_section = orm.relationship( + FloorSection, + doc=""" + Reference to the :class:`FloorSection` with which this location is + associated. + """) + + section = sa.Column(sa.SmallInteger(), nullable=True, default=0) + + subsection = sa.Column(sa.SmallInteger(), nullable=True, default=0) + + shelf_set = sa.Column(sa.SmallInteger(), nullable=True, default=0) + + shelf = sa.Column(sa.SmallInteger(), nullable=True, default=0) + + location = sa.Column(sa.SmallInteger(), nullable=True, default=0) + + class Employee(Base): """ Represents an employee within the organization. From 14a182c6759d46caf7a8ebb36032a0bac668f806 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 30 Mar 2020 13:44:22 -0500 Subject: [PATCH 023/136] Disable `TaxRate.sales_code` attribute, for now since some older DBs don't have it --- corepos/db/office_op/model.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 67e9be8..b61bdd3 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -244,7 +244,11 @@ class TaxRate(Base): description = sa.Column(sa.String(length=50), nullable=True) - sales_code = sa.Column('salesCode', sa.Integer(), nullable=True) + # TODO: this was not in some older DBs + # sales_code = sa.Column('salesCode', sa.Integer(), nullable=True) + + def __str__(self): + return self.description or "" class TaxRateComponent(Base): From d818100da3f76c7d1b80be1af92287f44b37d6c5 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 31 Mar 2020 14:09:39 -0500 Subject: [PATCH 024/136] Fix `id` field for TaxRateComponent --- corepos/db/office_op/model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index b61bdd3..5d60fc3 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -260,7 +260,8 @@ class TaxRateComponent(Base): sa.ForeignKeyConstraint(['taxRateID'], ['taxrates.id']), ) - id = sa.Column(sa.Integer(), primary_key=True, autoincrement=True, nullable=False) + taxRateComponentID = sa.Column(sa.Integer(), primary_key=True, autoincrement=True, nullable=False) + id = orm.synonym('taxRateComponentID') tax_rate_id = sa.Column('taxRateID', sa.Integer()) tax_rate = orm.relationship(TaxRate, backref='components') From 87fd5367a13cbae75f60967c5558093c5e910eb1 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 31 Mar 2020 14:09:51 -0500 Subject: [PATCH 025/136] Add `table_exists()` util function --- corepos/db/util.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/corepos/db/util.py b/corepos/db/util.py index 324a057..f16da72 100644 --- a/corepos/db/util.py +++ b/corepos/db/util.py @@ -36,3 +36,25 @@ def get_last_card_number(session): """ return session.query(sa.func.max(corepos.Customer.card_number))\ .scalar() or 0 + + +def table_exists(session, model_class): + """ + Determine if a table exists in the database. + + :param session: SQLAlchemy session object, opened against the database in + question. + + :param model_class: The model class associated with the table in question. + + :returns: Boolean indicating if the table exists. + """ + try: + session.query(model_class).count() + except sa.exc.ProgrammingError as error: + if "doesn't exist" in str(error): + return False + else: + raise + else: + return True From 4b971c289ebbd80cd1c4558f344260c2a8537669 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 10 Apr 2020 14:09:38 -0500 Subject: [PATCH 026/136] Add `LikeCode` and `ProductLikeCode` models --- corepos/db/office_op/model.py | 76 +++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 5d60fc3..c094de3 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -270,6 +270,41 @@ class TaxRateComponent(Base): description = sa.Column(sa.String(length=50), nullable=True) + def __str__(self): + return self.description or "" + + +class LikeCode(Base): + """ + Represents a "like code" for sake of product pricing. + """ + __tablename__ = 'likeCodes' + + likeCode = sa.Column(sa.Integer(), primary_key=True, autoincrement=False, nullable=False) + id = orm.synonym('likeCode') + + description = sa.Column('likeCodeDesc', sa.String(length=50), nullable=True) + + strict = sa.Column(sa.Boolean(), nullable=True, default=False) + + organic = sa.Column(sa.Boolean(), nullable=True, default=False) + + preferred_vendor_id = sa.Column('preferredVendorID', sa.Integer(), nullable=True, default=0) + + multi_vendor = sa.Column('multiVendor', sa.Boolean(), nullable=True, default=False) + + sort_retail = sa.Column('sortRetail', sa.String(length=255), nullable=True) + + sort_internal = sa.Column('sortInternal', sa.String(length=255), nullable=True) + + products = association_proxy( + '_products', 'product', + creator=lambda p: ProductLikeCode(product=p), + ) + + def __str__(self): + return self.description or "" + class Product(Base): """ @@ -388,6 +423,11 @@ class Product(Base): current_origin_id = sa.Column(sa.Integer(), nullable=True, default=0) + like_code = association_proxy( + '_like_code', 'like_code', + creator=lambda lc: ProductLikeCode(like_code=lc), + ) + @property def full_description(self): fields = ['brand', 'description', 'size'] @@ -399,6 +439,42 @@ class Product(Base): return self.description or '' +class ProductLikeCode(Base): + """ + Represents the association between a product and like code. + """ + __tablename__ = 'upcLike' + __table_args__ = ( + sa.ForeignKeyConstraint(['likeCode'], ['likeCodes.likeCode']), + ) + + upc = sa.Column(sa.String(length=13), primary_key=True, nullable=False) + product = orm.relationship( + Product, + primaryjoin=Product.upc == orm.foreign(upc), + doc=""" + Reference to the product to which this association applies. + """, + backref=orm.backref( + '_like_code', + uselist=False, + doc=""" + Reference to the like code association for the product. + """)) + + like_code_id = sa.Column('likeCode', sa.Integer(), nullable=True) + like_code = orm.relationship( + LikeCode, + doc=""" + Reference to the LikeCode to which this association applies. + """, + backref=orm.backref( + '_products', + doc=""" + List of product associations for this like code. + """)) + + class ProductFlag(Base): """ Represents a product flag attribute. From beb73dc6687169cf88fdc2f77550dde028371497 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 17 Apr 2020 00:24:53 -0500 Subject: [PATCH 027/136] Add `Product.last_sold` to schema hopefully that's a good idea.. --- corepos/db/office_op/model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index c094de3..d730bed 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -423,6 +423,9 @@ class Product(Base): current_origin_id = sa.Column(sa.Integer(), nullable=True, default=0) + # TODO: some older DB's might not have this? guess we'll see + last_sold = sa.Column(sa.DateTime(), nullable=True) + like_code = association_proxy( '_like_code', 'like_code', creator=lambda lc: ProductLikeCode(like_code=lc), From 07291b3fa7530baaa9725c48d9742f272f3de53a Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 13 Jul 2020 11:02:59 -0500 Subject: [PATCH 028/136] Add `MemberNote` and `MemberInfo.notes` to data model --- corepos/db/office_op/model.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index d730bed..f525144 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -1024,6 +1024,21 @@ class MemberInfo(Base): Reference to the member to whom the date record applies. """)) + notes = orm.relationship( + 'MemberNote', + primaryjoin='MemberNote.card_number == MemberInfo.card_number', + foreign_keys='MemberNote.card_number', + order_by='MemberNote.timestamp', + cascade='all, delete-orphan', + doc=""" + List of note records for the member. + """, + backref=orm.backref( + 'member_info', + doc=""" + Reference to the :class:`MemberInfo` record to which the note applies. + """)) + @property def full_name(self): return '{} {}'.format(self.first_name or '', self.last_name or '').strip() @@ -1079,6 +1094,26 @@ class MemberContact(Base): return str(self.preference) +class MemberNote(Base): + """ + Additional notes for a member. + """ + __tablename__ = 'memberNotes' + + id = sa.Column('memberNoteID', sa.Integer(), nullable=False, primary_key=True, autoincrement=True) + + card_number = sa.Column('cardno', sa.Integer(), nullable=True) + + note = sa.Column(sa.Text(), nullable=True) + + timestamp = sa.Column('stamp', sa.DateTime(), nullable=True) + + username = sa.Column(sa.String(length=50), nullable=True) + + def __str__(self): + return self.note or "" + + class HouseCoupon(Base): """ Represents a "house" (store) coupon. From 68ab8ff55dfe55467dcd8195865c348a909a3c53 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 13 Jul 2020 11:03:32 -0500 Subject: [PATCH 029/136] Add `CustomerAccount.customer_type` reference, to `MemberType` not 100% sure about the best naming here, hopefully this is good --- corepos/db/office_op/model.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index f525144..400c4a6 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -801,6 +801,13 @@ class CustomerAccount(Base): customer_type_id = sa.Column('customerTypeID', sa.Integer(), nullable=True, default=1) + customer_type = orm.relationship( + MemberType, + primaryjoin=MemberType.id == customer_type_id, + foreign_keys=[customer_type_id], + doc=""" + Reference to the :class:`MemberType` with which this account is associated. + """) charge_balance = sa.Column('chargeBalance', sa.Numeric(precision=10, scale=2), nullable=True, default=0) From 257ed82d6db2ebe19916edc393dbbcf5570b5e95 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 15 Jul 2020 21:42:28 -0500 Subject: [PATCH 030/136] Bring `__version__` into root namespace can't remember why that wouldn't already be there..? maybe just forgot --- corepos/__init__.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/corepos/__init__.py b/corepos/__init__.py index e69de29..22b270b 100644 --- a/corepos/__init__.py +++ b/corepos/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# pyCOREPOS -- Python Interface to CORE POS +# Copyright © 2018-2020 Lance Edgar +# +# This file is part of pyCOREPOS. +# +# pyCOREPOS is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# pyCOREPOS is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# pyCOREPOS. If not, see . +# +################################################################################ +""" +CORE POS Interface +""" + +from ._version import __version__ From 13b838052734b1bc19225519eb40c0043b8240c8 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 15 Jul 2020 22:15:11 -0500 Subject: [PATCH 031/136] Declare foreign key for CustData.member_type --- corepos/db/office_op/model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 400c4a6..3684d39 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -907,6 +907,9 @@ class CustData(Base): https://github.com/CORE-POS/IS4C/blob/master/fannie/classlib2.0/data/models/op/CustdataModel.php """ __tablename__ = 'custdata' + __table_args__ = ( + sa.ForeignKeyConstraint(['memType'], ['memtype.memtype']), + ) id = sa.Column(sa.Integer(), primary_key=True, autoincrement=True, nullable=False) From 365d679d7673f34cbb405e00091975202c40934b Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 21 Jul 2020 15:07:28 -0500 Subject: [PATCH 032/136] Add `split_street()` method for MemberInfo --- corepos/db/office_op/model.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 3684d39..8e4a1dc 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -24,12 +24,16 @@ Data model for CORE POS "office_op" DB """ +import logging + import sqlalchemy as sa from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.associationproxy import association_proxy +log = logging.getLogger(__name__) + Base = declarative_base() @@ -1056,6 +1060,23 @@ class MemberInfo(Base): def __str__(self): return self.full_name + def split_street(self): + """ + Tries to split the :attr:`street` attribute into 2 separate lines, e.g. + "street1" and "street2" style. Always returns a 2-tuple even if the + second line would be empty. + """ + address = (self.street or '').strip() + lines = address.split('\n') + street1 = lines[0].strip() or None + street2 = None + if len(lines) > 1: + street2 = lines[1].strip() or None + if len(lines) > 2: + log.warning("member #%s has %s address lines: %s", + self.card_number, len(lines), self) + return (street1, street2) + class MemberDate(Base): """ From 4c7b208e6e419465050e22e2570a89ccea0b2c8a Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 22 Jul 2020 20:25:20 -0500 Subject: [PATCH 033/136] Add Suspension and ReasonCodes to model --- corepos/db/office_op/model.py | 60 +++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 8e4a1dc..6374624 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -1053,6 +1053,21 @@ class MemberInfo(Base): Reference to the :class:`MemberInfo` record to which the note applies. """)) + suspension = orm.relationship( + 'Suspension', + primaryjoin='Suspension.card_number == MemberInfo.card_number', + foreign_keys='Suspension.card_number', + uselist=False, + doc=""" + Suspension record for the member, if applicable. + """, + backref=orm.backref( + 'member_info', + doc=""" + Reference to the :class:`MemberInfo` record to which the suspension + applies. + """)) + @property def full_name(self): return '{} {}'.format(self.first_name or '', self.last_name or '').strip() @@ -1145,6 +1160,51 @@ class MemberNote(Base): return self.note or "" +class ReasonCode(Base): + """ + Reason codes for legacy account suspensions. + """ + __tablename__ = 'reasoncodes' + + mask = sa.Column(sa.Integer(), nullable=False, primary_key=True, autoincrement=False) + + text_string = sa.Column('textStr', sa.String(length=100), nullable=True) + + def __str__(self): + return "#{}: {}".format(self.mask, self.text_string) + + +class Suspension(Base): + """ + Suspension status for legacy customer accounts. + """ + __tablename__ = 'suspensions' + __table_args__ = ( + sa.ForeignKeyConstraint(['reasoncode'], ['reasoncodes.mask']), + ) + + card_number = sa.Column('cardno', sa.Integer(), nullable=False, primary_key=True, autoincrement=False) + + type = sa.Column(sa.String(length=1), nullable=True) + + memtype1 = sa.Column(sa.Integer(), nullable=True) + + memtype2 = sa.Column(sa.String(length=6), nullable=True) + + suspension_date = sa.Column('suspDate', sa.DateTime(), nullable=True) + + reason = sa.Column(sa.Text(), nullable=True) + + mail_flag = sa.Column('mailflag', sa.Integer(), nullable=True) + + discount = sa.Column(sa.Integer(), nullable=True) + + charge_limit = sa.Column('chargelimit', sa.Numeric(precision=10, scale=2), nullable=True) + + reason_code = sa.Column('reasoncode', sa.Integer(), nullable=True) + reason_object = orm.relationship(ReasonCode) + + class HouseCoupon(Base): """ Represents a "house" (store) coupon. From ef1a25f0dc269cc39250c69d17b2b8a38e64c38b Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 30 Jul 2020 11:11:17 -0500 Subject: [PATCH 034/136] Make sure `MemberInfo.customers` is sorted by person number also improve the str() method a bit --- corepos/db/office_op/model.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 6374624..4026d8a 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -1017,6 +1017,7 @@ class MemberInfo(Base): customers = orm.relationship( CustData, primaryjoin=CustData.card_number == card_number, + order_by=CustData.person_number, foreign_keys=[CustData.card_number], back_populates='member_info', remote_side=CustData.card_number, @@ -1073,7 +1074,10 @@ class MemberInfo(Base): return '{} {}'.format(self.first_name or '', self.last_name or '').strip() def __str__(self): - return self.full_name + name = self.full_name + if name: + return name + return "Member Info #{}".format(self.card_number) def split_street(self): """ From 9299cd445f8f6922df6fa6bb7ae1ba81da88cc43 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 10 Aug 2020 15:57:32 -0500 Subject: [PATCH 035/136] Add "origin" models for office_op schema --- corepos/db/office_op/model.py | 78 +++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 4026d8a..14bedbe 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -310,6 +310,84 @@ class LikeCode(Base): return self.description or "" +class OriginCountry(Base): + """ + Represents a country which relates to the "origin" for some product(s). + """ + __tablename__ = 'originCountry' + + id = sa.Column('countryID', sa.Integer(), primary_key=True, autoincrement=True, nullable=False) + + name = sa.Column(sa.String(length=50), nullable=True) + + abbreviation = sa.Column('abbr', sa.String(length=5), nullable=True) + + def __str__(self): + return self.name or self.abbreviation or "" + + +class OriginStateProv(Base): + """ + Represents a state/province which relates to the "origin" for some product(s). + """ + __tablename__ = 'originStateProv' + + id = sa.Column('stateProvID', sa.Integer(), primary_key=True, autoincrement=True, nullable=False) + + name = sa.Column(sa.String(length=50), nullable=True) + + abbreviation = sa.Column('abbr', sa.String(length=5), nullable=True) + + def __str__(self): + return self.name or self.abbreviation or "" + + +class OriginCustomRegion(Base): + """ + Represents a custom region which relates to the "origin" for some product(s). + """ + __tablename__ = 'originCustomRegion' + + id = sa.Column('customID', sa.Integer(), primary_key=True, autoincrement=True, nullable=False) + + name = sa.Column(sa.String(length=50), nullable=True) + + def __str__(self): + return self.name or "" + + +class Origin(Base): + """ + Represents a location which is the "origin" for some product(s). + """ + __tablename__ = 'origins' + __table_args__ = ( + sa.ForeignKeyConstraint(['countryID'], ['originCountry.countryID']), + sa.ForeignKeyConstraint(['stateProvID'], ['originStateProv.stateProvID']), + sa.ForeignKeyConstraint(['customID'], ['originCustomRegion.customID']), + ) + + id = sa.Column('originID', sa.Integer(), primary_key=True, autoincrement=True, nullable=False) + + country_id = sa.Column('countryID', sa.Integer(), nullable=True) + country = orm.relationship(OriginCountry) + + state_prov_id = sa.Column('stateProvID', sa.Integer(), nullable=True) + state_prov = orm.relationship(OriginStateProv) + + custom_id = sa.Column('customID', sa.Integer(), nullable=True) + custom_region = orm.relationship(OriginCustomRegion) + + local = sa.Column(sa.Boolean(), nullable=True, default=0) + + name = sa.Column(sa.String(length=100), nullable=True) + + short_name = sa.Column('shortName', sa.String(length=50), nullable=True) + + def __str__(self): + return self.name or self.short_name or "" + + class Product(Base): """ Represents a product, purchased and/or sold by the organization. From 472f43896bb18922cff8cdbee561b60d66f35d53 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 16 Aug 2020 16:57:54 -0500 Subject: [PATCH 036/136] Add basic batch models --- corepos/db/office_op/model.py | 94 +++++++++++++++++++++++++++++++++++ corepos/enum.py | 11 ++++ 2 files changed, 105 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 14bedbe..5b494c7 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -1326,3 +1326,97 @@ class HouseCoupon(Base): def __str__(self): return self.description or '' + + +class BatchType(Base): + """ + Represents the definition of a batch type. + """ + __tablename__ = 'batchType' + + id = sa.Column('batchTypeID', sa.Integer(), primary_key=True, autoincrement=False, nullable=False) + + description = sa.Column('typeDesc', sa.String(length=50), nullable=True) + + discount_type = sa.Column('discType', sa.Integer(), nullable=True) + + dated_signs = sa.Column('datedSigns', sa.Boolean(), nullable=True, default=True) + + special_order_eligible = sa.Column('specialOrderEligible', sa.Boolean(), nullable=True, default=True) + + editor_ui = sa.Column('editorUI', sa.Boolean(), nullable=True, default=True) + + allow_single_store = sa.Column('allowSingleStore', sa.Boolean(), nullable=True, default=False) + + exit_inventory = sa.Column('exitInventory', sa.Boolean(), nullable=True, default=False) + + def __str__(self): + return self.description or "" + + +class Batch(Base): + """ + Represents a batch. + """ + __tablename__ = 'batches' + __table_args__ = ( + sa.ForeignKeyConstraint(['batchType'], ['batchType.batchTypeID']), + ) + + id = sa.Column('batchID', sa.Integer(), primary_key=True, autoincrement=True, nullable=False) + + start_date = sa.Column('startDate', sa.DateTime(), nullable=True) + + end_date = sa.Column('endDate', sa.DateTime(), nullable=True) + + name = sa.Column('batchName', sa.String(length=80), nullable=True) + + batch_type_id = sa.Column('batchType', sa.Integer(), nullable=True) + batch_type = orm.relationship(BatchType) + + discount_type = sa.Column('discountType', sa.Integer(), nullable=True) + + priority = sa.Column(sa.Integer(), nullable=True) + + owner = sa.Column(sa.String(length=50), nullable=True) + + trans_limit = sa.Column('transLimit', sa.Boolean(), nullable=True, default=False) + + notes = sa.Column(sa.Text(), nullable=True) + + def __str__(self): + return self.name or "" + + +class BatchItem(Base): + """ + Represents a batch "list" item. + """ + __tablename__ = 'batchList' + __table_args__ = ( + sa.ForeignKeyConstraint(['batchID'], ['batches.batchID']), + ) + + id = sa.Column('listID', sa.Integer(), primary_key=True, autoincrement=True, nullable=False) + + batch_id = sa.Column('batchID', sa.Integer(), nullable=True) + batch = orm.relationship(Batch, backref=orm.backref('items')) + + upc = sa.Column(sa.String(length=13), nullable=True) + + sale_price = sa.Column('salePrice', sa.Numeric(precision=9, scale=3), nullable=True) + + group_sale_price = sa.Column('groupSalePrice', sa.Numeric(precision=9, scale=3), nullable=True) + + active = sa.Column(sa.Boolean(), nullable=True) + + price_method = sa.Column('pricemethod', sa.Integer(), nullable=True, default=0) + + quantity = sa.Column(sa.Integer(), nullable=True, default=0) + + sign_multiplier = sa.Column('signMultiplier', sa.Boolean(), nullable=True, default=True) + + cost = sa.Column(sa.Numeric(precision=9, scale=3), nullable=True, default=0) + + def __str__(self): + return self.upc or "" diff --git a/corepos/enum.py b/corepos/enum.py index f0860ea..55619d8 100644 --- a/corepos/enum.py +++ b/corepos/enum.py @@ -32,6 +32,17 @@ except ImportError: from ordereddict import OrderedDict +BATCH_DISCOUNT_TYPE_PRICE_CHANGE = 0 +BATCH_DISCOUNT_TYPE_SALE_EVERYONE = 1 +BATCH_DISCOUNT_TYPE_SALE_RESTRICTED = 2 + +BATCH_DISCOUNT_TYPE = OrderedDict([ + (BATCH_DISCOUNT_TYPE_PRICE_CHANGE, "Price Change"), + (BATCH_DISCOUNT_TYPE_SALE_EVERYONE, "Sale for everyone"), + (BATCH_DISCOUNT_TYPE_SALE_RESTRICTED, "Member/Owner only sale"), +]) + + HOUSE_COUPON_MEMBER_ONLY_NO = 0 HOUSE_COUPON_MEMBER_ONLY_YES = 1 HOUSE_COUPON_MEMBER_ONLY_PLUS = 2 From e2dc00b469ccda22876fd86e8f73b11b51c03194 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 20 Aug 2020 14:26:46 -0500 Subject: [PATCH 037/136] Add model for `SuperDepartment` --- corepos/db/office_op/model.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 5b494c7..330e8d1 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -74,6 +74,38 @@ class Parameter(Base): return "{}-{} {}".format(self.store_id, self.lane_id, self.param_key) +class SuperDepartment(Base): + """ + Represents a "super" (parent/child) department mapping. + """ + __tablename__ = 'superdepts' + __table_args__ = ( + sa.ForeignKeyConstraint(['superID'], ['departments.dept_no']), + sa.ForeignKeyConstraint(['dept_ID'], ['departments.dept_no']), + ) + + parent_id = sa.Column('superID', sa.Integer(), primary_key=True, autoincrement=False, nullable=False) + parent = orm.relationship( + 'Department', + foreign_keys=[parent_id], + doc=""" + Reference to the parent department for this mapping. + """, + backref=orm.backref('_super_children')) + + child_id = sa.Column('dept_ID', sa.Integer(), primary_key=True, autoincrement=False, nullable=False) + child = orm.relationship( + 'Department', + foreign_keys=[child_id], + doc=""" + Reference to the child department for this mapping. + """, + backref=orm.backref('_super_parent', uselist=False)) + + def __str__(self): + return "{} / {}".format(self.parent, self.child) + + class Department(Base): """ Represents a department within the organization. From 74a28514dca2a4118a4563e8831ac85d351455c6 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 20 Aug 2020 14:41:08 -0500 Subject: [PATCH 038/136] Tweak relationship for `Department._super_parents` since there can be more than one --- corepos/db/office_op/model.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 330e8d1..14e1ddd 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -100,7 +100,9 @@ class SuperDepartment(Base): doc=""" Reference to the child department for this mapping. """, - backref=orm.backref('_super_parent', uselist=False)) + backref=orm.backref( + '_super_parents', + order_by=parent_id)) def __str__(self): return "{} / {}".format(self.parent, self.child) From 963f544b021179a684977e678a1115ccdc3f99ba Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 2 Sep 2020 11:25:10 -0500 Subject: [PATCH 039/136] Expose some "new" columns for ScaleItem model may need to revisit this again, when an "old" DB is encountered --- corepos/db/office_op/model.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 14e1ddd..24a185e 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -761,14 +761,17 @@ class ScaleItem(Base): modified = sa.Column(sa.DateTime(), nullable=True) - # TODO: this was not in some older DBs - # linked_plu = sa.Column('linkedPLU', sa.String(length=13), nullable=True) + # TODO: the following 3 columns are not in some older DBs; maybe need to + # figure out a "simple" way to conditionally include them? - # TODO: this was not in some older DBs - # mosa_statement = sa.Column('mosaStatement', sa.Boolean(), nullable=True, default=False) + linked_plu = sa.Column('linkedPLU', sa.String(length=13), nullable=True) - # TODO: this was not in some older DBs - # origin_text = sa.Column('originText', sa.String(length=100), nullable=True) + mosa_statement = sa.Column('mosaStatement', sa.Boolean(), nullable=True, default=False) + + origin_text = sa.Column('originText', sa.String(length=100), nullable=True) + + def __str__(self): + return str(self.product) class FloorSection(Base): From 286df47c220dd06c07640a189964d52b34be7eac Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 4 Sep 2020 19:07:41 -0500 Subject: [PATCH 040/136] Add `get_vendor_items()` and `get_vendor_item()` methods for web API --- corepos/api.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/corepos/api.py b/corepos/api.py index b66613d..abe2afc 100644 --- a/corepos/api.py +++ b/corepos/api.py @@ -421,3 +421,46 @@ class CoreWebAPI(object): response = self.post(params) result = self.parse_response(response) return json.loads(result) + + def get_vendor_items(self, **columns): + """ + Fetch some or all of VendorItem records from CORE. + + :returns: A (potentially empty) list of vendor item dict records. + + To fetch all vendor items:: + + api.get_vendor_items() + + To fetch only products with brand name "Braggs":: + + api.get_vendor_items(brand='Braggs') + """ + params = { + 'entity': 'VendorItems', + 'submethod': 'get', + 'columns': columns, + } + response = self.post(params) + result = self.parse_response(response) + return [json.loads(rec) for rec in result] + + def get_vendor_item(self, upc, vendorID, **columns): + """ + Fetch an existing VendorItem record from CORE. + + :returns: Either a vendor item dict record, or ``None``. + """ + columns['upc'] = upc + columns['vendorID'] = vendorID + params = { + 'entity': 'VendorItems', + 'submethod': 'get', + 'columns': columns, + } + response = self.post(params) + result = self.parse_response(response) + if result: + if len(result) > 1: + log.warning("CORE API returned %s VendorItem results", len(result)) + return json.loads(result[0]) From 9dd581352064b3673f886a7e015aaade869ce09f Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 16 Sep 2020 19:43:35 -0500 Subject: [PATCH 041/136] Update changelog --- CHANGELOG.md | 4 ++++ corepos/_version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b1c709..93af3bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.1.1] - 2020-09-16 +### Added +- A ton of updates, mostly a "save point" release. + ## [0.1.0] - 2020-02-27 ### Added - Initial version of the package; defines some basic table mappings. diff --git a/corepos/_version.py b/corepos/_version.py index e41b669..4984097 100644 --- a/corepos/_version.py +++ b/corepos/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.0' +__version__ = '0.1.1' From 29638c062c6c1df8e7d9fc4a0e61b6389036e548 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 7 Dec 2020 17:41:01 -0600 Subject: [PATCH 042/136] Remove "default" values from Product model definition i'm a bit torn about this. on the one hand i like there being some default values here, but realistically they didn't all make sense. also it could be said that default values amount to business logic and that should stay in CORE basically. but in the end, i needed these to go away in order to simplify generating some SQL INSERT statements. was using SQLAlchemy core and it kept insisting that all fields with a "default" defined should be part of the INSERT statement, but really i didn't want them to be. so they no longer have defaults. --- corepos/db/office_op/model.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 24a185e..6799fbe 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -481,7 +481,10 @@ class Product(Base): scale = sa.Column(sa.Boolean(), nullable=True) - scale_price = sa.Column('scaleprice', sa.Boolean(), nullable=True, default=False) + # TODO: yikes, did i just code this all wrong the first time? pretty sure + # this needs to change to a decimal column... + scale_price = sa.Column('scaleprice', sa.Boolean(), nullable=True) + # scale_price = sa.Column('scaleprice', sa.Numeric(precision=10, scale=2), nullable=True) mix_match_code = sa.Column('mixmatchcode', sa.String(length=13), nullable=True) @@ -505,11 +508,11 @@ class Product(Base): id_enforced = sa.Column('idEnforced', sa.SmallInteger(), nullable=True) - cost = sa.Column(sa.Float(), nullable=True, default=0) + cost = sa.Column(sa.Float(), nullable=True) in_use = sa.Column('inUse', sa.Boolean(), nullable=True) - flags = sa.Column('numflag', sa.Integer(), nullable=True, default=0) + flags = sa.Column('numflag', sa.Integer(), nullable=True) subdepartment_number = sa.Column('subdept', sa.SmallInteger(), nullable=True) subdepartment = orm.relationship( @@ -522,11 +525,11 @@ class Product(Base): deposit = sa.Column(sa.Float(), nullable=True) - local = sa.Column(sa.Integer(), nullable=True, default=0) + local = sa.Column(sa.Integer(), nullable=True) - store_id = sa.Column(sa.SmallInteger(), nullable=True, default=0) + store_id = sa.Column(sa.SmallInteger(), nullable=True) - default_vendor_id = sa.Column(sa.Integer(), nullable=True, default=0) + default_vendor_id = sa.Column(sa.Integer(), nullable=True) default_vendor = orm.relationship( Vendor, primaryjoin=Vendor.id == default_vendor_id, @@ -537,7 +540,7 @@ class Product(Base): # TODO: deprecate / remove this? vendor = orm.synonym('default_vendor') - current_origin_id = sa.Column(sa.Integer(), nullable=True, default=0) + current_origin_id = sa.Column(sa.Integer(), nullable=True) # TODO: some older DB's might not have this? guess we'll see last_sold = sa.Column(sa.DateTime(), nullable=True) From ff428c4635218c3acfd6f1e1df36186f04debd61 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 9 Dec 2020 13:09:58 -0600 Subject: [PATCH 043/136] Misc. tweaks to product-related schema, for sake of generating SQL e.g. from IFPS data --- corepos/db/office_op/model.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 6799fbe..ea53110 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -24,6 +24,7 @@ Data model for CORE POS "office_op" DB """ +import datetime import logging import sqlalchemy as sa @@ -37,6 +38,23 @@ log = logging.getLogger(__name__) Base = declarative_base() +class StringableDateTime(sa.TypeDecorator): + """ + Sort of a hack, to let us string-ify certain DateTime values when + generating "raw" SQL output. + + cf. https://docs.sqlalchemy.org/en/14/faq/sqlexpressions.html#rendering-bound-parameters-inline + """ + impl = sa.DateTime + + def process_literal_param(self, value, dialect): + if value is None: + return 'NULL' + if isinstance(value, datetime.datetime): + return "'{}'".format(value.strftime('%Y-%m-%d %H:%M:%S')) + raise NotImplementedError + + class Change(Base): """ Represents a changed (or deleted) record, which is pending synchronization @@ -135,11 +153,11 @@ class Department(Base): modified_by_id = sa.Column('modifiedby', sa.Integer(), nullable=True) - margin = sa.Column(sa.Float(), nullable=False, default=0) + margin = sa.Column(sa.Float(), nullable=False) - sales_code = sa.Column('salesCode', sa.Integer(), nullable=False, default=0) + sales_code = sa.Column('salesCode', sa.Integer(), nullable=False) - member_only = sa.Column('memberOnly', sa.SmallInteger(), nullable=False, default=0) + member_only = sa.Column('memberOnly', sa.SmallInteger(), nullable=False) def __str__(self): return self.name or '' @@ -488,6 +506,8 @@ class Product(Base): mix_match_code = sa.Column('mixmatchcode', sa.String(length=13), nullable=True) + created = sa.Column(StringableDateTime(), nullable=True) + modified = sa.Column(sa.DateTime(), nullable=True) # advertised = sa.Column(sa.Boolean(), nullable=True) @@ -649,7 +669,7 @@ class ProductUser(Base): enable_online = sa.Column('enableOnline', sa.Boolean(), nullable=True) - sold_out = sa.Column('soldOut', sa.Boolean(), nullable=True, default=False) + sold_out = sa.Column('soldOut', sa.Boolean(), nullable=True) # TODO: this was not in some older DBs # sign_count = sa.Column('signCount', sa.SmallInteger(), nullable=True, default=1) @@ -657,6 +677,9 @@ class ProductUser(Base): # TODO: this was not in some older DBs # narrow = sa.Column(sa.Boolean(), nullable=True, default=False) + def __str__(self): + return str(self.product or '') + class VendorItem(Base): """ From 52d959533146b957b9dd42cc1aed7abada10f9d3 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 31 Dec 2020 19:01:31 -0600 Subject: [PATCH 044/136] Fix `Department.see_id` field definition this is not a flag, but a minimum age requirement --- corepos/db/office_op/model.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index ea53110..f5c415d 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -146,8 +146,7 @@ class Department(Base): discount = sa.Column('dept_discount', sa.Boolean(), nullable=True) - # TODO: probably should rename this attribute, but to what? - dept_see_id = sa.Column(sa.Boolean(), nullable=True) + see_id = sa.Column('dept_see_id', sa.Integer(), nullable=True) modified = sa.Column(sa.DateTime(), nullable=True) From 19d62b535f51212508f17f2a5c1223271d0dd57a Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 31 Dec 2020 19:25:33 -0600 Subject: [PATCH 045/136] Tweak that thing again... --- corepos/db/office_op/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index f5c415d..6677e98 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -146,7 +146,7 @@ class Department(Base): discount = sa.Column('dept_discount', sa.Boolean(), nullable=True) - see_id = sa.Column('dept_see_id', sa.Integer(), nullable=True) + see_id = sa.Column('dept_see_id', sa.SmallInteger(), nullable=True) modified = sa.Column(sa.DateTime(), nullable=True) From d398e706c421dda069a1ffe64246c0ce0bcda656 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 13 Jan 2021 19:18:17 -0600 Subject: [PATCH 046/136] Add `MemberBarcode` to op model --- corepos/db/office_op/model.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 6677e98..a4da4fc 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -1179,6 +1179,21 @@ class MemberInfo(Base): Reference to the member to whom the date record applies. """)) + barcodes = orm.relationship( + 'MemberBarcode', + primaryjoin='MemberBarcode.card_number == MemberInfo.card_number', + foreign_keys='MemberBarcode.card_number', + order_by='MemberBarcode.upc', + # cascade='all, delete-orphan', + doc=""" + List of extra barcode records for the member. + """, + backref=orm.backref( + 'member_info', + doc=""" + Reference to the :class:`MemberInfo` record to which the barcode applies. + """)) + notes = orm.relationship( 'MemberNote', primaryjoin='MemberNote.card_number == MemberInfo.card_number', @@ -1284,6 +1299,21 @@ class MemberContact(Base): return str(self.preference) +class MemberBarcode(Base): + """ + Additional barcode for a member. + """ + __tablename__ = 'memberCards' + + card_number = sa.Column('card_no', sa.Integer(), nullable=False, + primary_key=True, autoincrement=False) + + upc = sa.Column(sa.String(length=13), nullable=False, primary_key=True) + + def __str__(self): + return self.upc or "" + + class MemberNote(Base): """ Additional notes for a member. From ee9451588ca8f5a9067eff5c49e13202633548cd Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 27 Jan 2021 22:19:38 -0600 Subject: [PATCH 047/136] Add basic support for Stores schema and API --- corepos/api.py | 23 +++++++++++++++++++++++ corepos/db/office_op/model.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/corepos/api.py b/corepos/api.py index abe2afc..3a8b624 100644 --- a/corepos/api.py +++ b/corepos/api.py @@ -166,6 +166,29 @@ class CoreWebAPI(object): if result: return result + def get_stores(self, **columns): + """ + Fetch some or all of Store records from CORE. + + :returns: A (potentially empty) list of store dict records. + + To fetch all stores:: + + api.get_stores() + + To fetch only stores named "Headquarters":: + + api.get_stores(description='Headquarters') + """ + params = { + 'entity': 'Stores', + 'submethod': 'get', + 'columns': columns, + } + response = self.post(params) + result = self.parse_response(response) + return [json.loads(rec) for rec in result] + def get_departments(self, **columns): """ Fetch some or all of Department records from CORE. diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index a4da4fc..0e8bf49 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -92,6 +92,41 @@ class Parameter(Base): return "{}-{} {}".format(self.store_id, self.lane_id, self.param_key) +class Store(Base): + """ + Represents a known store. + """ + __tablename__ = 'Stores' + + storeID = sa.Column(sa.Integer(), nullable=False, primary_key=True, autoincrement=True) + id = orm.synonym('storeID') + + description = sa.Column(sa.String(length=50), nullable=True) + + db_host = sa.Column('dbHost', sa.String(length=50), nullable=True) + + db_driver = sa.Column('dbDriver', sa.String(length=15), nullable=True) + + db_user = sa.Column('dbUser', sa.String(length=25), nullable=True) + + db_password = sa.Column('dbPassword', sa.String(length=25), nullable=True) + + trans_db = sa.Column('transDB', sa.String(length=20), nullable=True) + + op_db = sa.Column('opDB', sa.String(length=20), nullable=True) + + push = sa.Column(sa.Boolean(), nullable=True, default=True) + + pull = sa.Column(sa.Boolean(), nullable=True, default=True) + + has_own_items = sa.Column('hasOwnItems', sa.Boolean(), nullable=True, default=True) + + web_service_url = sa.Column('webServiceUrl', sa.String(length=255), nullable=True) + + def __str__(self): + return self.description or "" + + class SuperDepartment(Base): """ Represents a "super" (parent/child) department mapping. From 1757d0978175662c5d7b8c66ea8448075f8e06fc Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 1 Feb 2021 15:30:55 -0600 Subject: [PATCH 048/136] Add schema model for Purchase Orders --- corepos/db/office_op/model.py | 118 ++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 0e8bf49..8e24e05 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -1547,3 +1547,121 @@ class BatchItem(Base): def __str__(self): return self.upc or "" + + +class PurchaseOrder(Base): + """ + Represents a purchase order. + """ + __tablename__ = 'PurchaseOrder' + # TODO: would be simpler to declare these, but is it safe? + # __table_args__ = ( + # sa.ForeignKeyConstraint(['vendorID'], ['vendors.vendorID']), + # sa.ForeignKeyConstraint(['storeID'], ['Stores.storeID']), + # ) + + orderID = sa.Column(sa.Integer(), nullable=False, primary_key=True, autoincrement=True) + id = orm.synonym('orderID') + + vendor_id = sa.Column('vendorID', sa.Integer(), nullable=True) + vendor = orm.relationship( + Vendor, + primaryjoin=Vendor.id == vendor_id, + foreign_keys=[vendor_id]) + + store_id = sa.Column('storeID', sa.Integer(), nullable=True) + store = orm.relationship( + Store, + primaryjoin=Store.id == store_id, + foreign_keys=[store_id]) + + creation_date = sa.Column('creationDate', sa.DateTime(), nullable=True) + + placed = sa.Column(sa.Boolean(), nullable=True, default=False) + + placed_date = sa.Column('placedDate', sa.DateTime(), nullable=True) + + user_id = sa.Column('userID', sa.Integer(), nullable=True) + + vendor_order_id = sa.Column('vendorOrderID', sa.String(length=25), nullable=True) + + vendor_invoice_id = sa.Column('vendorInvoiceID', sa.String(length=25), nullable=True) + + standing_id = sa.Column('standingID', sa.Integer(), nullable=True) + + inventory_ignore = sa.Column('inventoryIgnore', sa.Boolean(), nullable=True, default=False) + + transfer_id = sa.Column('transferID', sa.Integer(), nullable=True) + + notes = association_proxy('notes', 'notes') + + def __str__(self): + return "#{} for {}".format(self.id, self.vendor or "??") + + +class PurchaseOrderItem(Base): + """ + Represents a line item in a purchase order. + """ + __tablename__ = 'PurchaseOrderItems' + __table_args__ = ( + sa.ForeignKeyConstraint(['orderID'], ['PurchaseOrder.orderID']), + ) + + order_id = sa.Column('orderID', sa.Integer(), nullable=False, + primary_key=True, autoincrement=False) + order = orm.relationship(PurchaseOrder, backref=orm.backref('items')) + + sku = sa.Column(sa.String(length=13), nullable=False, + primary_key=True) + + quantity = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True) + + unit_cost = sa.Column('unitCost', sa.Numeric(precision=10, scale=4), nullable=True) + + case_size = sa.Column('caseSize', sa.Float(), nullable=True) + + received_date = sa.Column('receivedDate', sa.DateTime(), nullable=True) + + received_quantity = sa.Column('receivedQty', sa.Float(), nullable=True) + + received_total_cost = sa.Column('receivedTotalCost', sa.Numeric(precision=10, scale=4), nullable=True) + + unit_size = sa.Column('unitSize', sa.String(length=25), nullable=True) + + brand = sa.Column(sa.String(length=50), nullable=True) + + description = sa.Column(sa.String(length=50), nullable=True) + + internal_upc = sa.Column('internalUPC', sa.String(length=13), nullable=True) + + sales_code = sa.Column('salesCode', sa.Integer(), nullable=True) + + is_special_order = sa.Column('isSpecialOrder', sa.Boolean(), nullable=True, + default=False) + + # TODO: this probably is FK to e.g. User.id ? + received_by = sa.Column('receivedBy', sa.Integer(), nullable=True, + default=0) + + def __str__(self): + return self.description or "" + + +class PurchaseOrderNote(Base): + """ + Represents a note attached to a purchase order. + """ + __tablename__ = 'PurchaseOrderNotes' + __table_args__ = ( + sa.ForeignKeyConstraint(['orderID'], ['PurchaseOrder.orderID']), + ) + + order_id = sa.Column('orderID', sa.Integer(), nullable=False, + primary_key=True, autoincrement=False) + order = orm.relationship(PurchaseOrder, backref=orm.backref('notes')) + + notes = sa.Column(sa.Text(), nullable=True) + + def __str__(self): + return self.notes or "" From 76f743f3b86060edf8ed77b4ab9d8ca5e5daf7a7 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 9 Feb 2021 14:25:18 -0600 Subject: [PATCH 049/136] Add `set_vendor_item()` method for API client --- corepos/api.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/corepos/api.py b/corepos/api.py index 3a8b624..b6ee1ed 100644 --- a/corepos/api.py +++ b/corepos/api.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2020 Lance Edgar +# Copyright © 2018-2021 Lance Edgar # # This file is part of pyCOREPOS. # @@ -487,3 +487,26 @@ class CoreWebAPI(object): if len(result) > 1: log.warning("CORE API returned %s VendorItem results", len(result)) return json.loads(result[0]) + + def set_vendor_item(self, sku, vendorID, **columns): + """ + Update an existing VendorItem record in CORE. + + :returns: Boolean indicating success of the operation. + + .. note:: + Currently this is being used to create a *new* product also. CORE's + ``vendorItems`` table does not use auto-increment for its PK, which + means we must provide one even when creating; therefore this method + may be used for that. + """ + columns['sku'] = sku + columns['vendorID'] = vendorID + params = { + 'entity': 'VendorItems', + 'submethod': 'set', + 'columns': columns, + } + response = self.post(params) + result = self.parse_response(response) + return json.loads(result) From 7cf4ec129545aac2dc2ab295af98706046443138 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 9 Feb 2021 16:11:50 -0600 Subject: [PATCH 050/136] Fetch single `vendorItems` record by `sku` instead of `upc` b/c that is what we must use as PK when updating the record etc. --- corepos/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/corepos/api.py b/corepos/api.py index b6ee1ed..e7310a0 100644 --- a/corepos/api.py +++ b/corepos/api.py @@ -468,13 +468,13 @@ class CoreWebAPI(object): result = self.parse_response(response) return [json.loads(rec) for rec in result] - def get_vendor_item(self, upc, vendorID, **columns): + def get_vendor_item(self, sku, vendorID, **columns): """ Fetch an existing VendorItem record from CORE. :returns: Either a vendor item dict record, or ``None``. """ - columns['upc'] = upc + columns['sku'] = sku columns['vendorID'] = vendorID params = { 'entity': 'VendorItems', From b40fbf7cab0f01e06afe60f3879e96ea283b1898 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 15 Feb 2021 12:59:21 -0600 Subject: [PATCH 051/136] Add the `Product.complete_size` convenience attribute also define `str(VendorItem)` --- corepos/db/office_op/model.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 8e24e05..c7fa505 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -611,6 +611,18 @@ class Product(Base): fields = filter(bool, fields) return ' '.join(fields) + @property + def complete_size(self): + """ + Returns the "complete" size string for the product. This is based on + the :attr:`size` and :attr:`unit_of_measure` attributes. + """ + parts = [self.size, self.unit_of_measure] + parts = [(part or '').strip() + for part in parts] + parts = [part for part in parts if part] + return " ".join(parts).strip() + def __str__(self): return self.description or '' @@ -771,6 +783,9 @@ class VendorItem(Base): modified = sa.Column(sa.DateTime(), nullable=True) + def __str__(self): + return "{} from {}".format(self.sku, self.vendor) + class ScaleItem(Base): """ From d94189c40432efc881122c58cccd740fb57fbeaa Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 4 May 2021 20:10:14 -0500 Subject: [PATCH 052/136] Add FK constraint for ProductLikeCode.upc not sure why that wasn't already present? --- corepos/db/office_op/model.py | 1 + 1 file changed, 1 insertion(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index c7fa505..2e25c34 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -633,6 +633,7 @@ class ProductLikeCode(Base): """ __tablename__ = 'upcLike' __table_args__ = ( + sa.ForeignKeyConstraint(['upc'], ['products.upc']), sa.ForeignKeyConstraint(['likeCode'], ['likeCodes.likeCode']), ) From 170f0a769aaf616c6a305ea01d1d5197a9c167a7 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 7 May 2021 11:12:44 -0500 Subject: [PATCH 053/136] Remove duplicated column name --- corepos/db/office_op/model.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 2e25c34..5e22e10 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2020 Lance Edgar +# Copyright © 2018-2021 Lance Edgar # # This file is part of pyCOREPOS. # @@ -815,8 +815,6 @@ class ScaleItem(Base): exception_price = sa.Column('exceptionprice', sa.Numeric(precision=10, scale=2), nullable=True) - exception_price = sa.Column('exceptionprice', sa.Numeric(precision=10, scale=2), nullable=True) - weight = sa.Column(sa.SmallInteger(), nullable=True, default=0) by_count = sa.Column('bycount', sa.Boolean(), nullable=True, default=False) From 0d3d007bd198684631de0540b86af4eb6a9e002d Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 11 Jun 2021 18:04:42 -0500 Subject: [PATCH 054/136] Update changelog --- CHANGELOG.md | 4 ++++ corepos/_version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93af3bf..ff62b28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.1.2] - 2021-06-11 +### Changed +- Several more updates, mostly a "save point" release. + ## [0.1.1] - 2020-09-16 ### Added - A ton of updates, mostly a "save point" release. diff --git a/corepos/_version.py b/corepos/_version.py index 4984097..de58c20 100644 --- a/corepos/_version.py +++ b/corepos/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.1' +__version__ = '0.1.2' From 178acdac31443cccbe43f6743d9c26f6902cb83a Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 21 Jul 2021 20:02:17 -0500 Subject: [PATCH 055/136] Add basic 'lane_op' DB schema just the 'products' table so far --- corepos/db/lane_op/__init__.py | 30 ++++++ corepos/db/lane_op/model.py | 169 +++++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 corepos/db/lane_op/__init__.py create mode 100644 corepos/db/lane_op/model.py diff --git a/corepos/db/lane_op/__init__.py b/corepos/db/lane_op/__init__.py new file mode 100644 index 0000000..884fe9f --- /dev/null +++ b/corepos/db/lane_op/__init__.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# pyCOREPOS -- Python Interface to CORE POS +# Copyright © 2018-2021 Lance Edgar +# +# This file is part of pyCOREPOS. +# +# pyCOREPOS is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# pyCOREPOS is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# pyCOREPOS. If not, see . +# +################################################################################ +""" +"Lane Operational" Database Interface +""" + +from sqlalchemy import orm + + +Session = orm.sessionmaker() diff --git a/corepos/db/lane_op/model.py b/corepos/db/lane_op/model.py new file mode 100644 index 0000000..2faa87b --- /dev/null +++ b/corepos/db/lane_op/model.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# pyCOREPOS -- Python Interface to CORE POS +# Copyright © 2018-2021 Lance Edgar +# +# This file is part of pyCOREPOS. +# +# pyCOREPOS is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# pyCOREPOS is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# pyCOREPOS. If not, see . +# +################################################################################ +""" +Data model for CORE POS "lane_op" DB +""" + +import sqlalchemy as sa +# from sqlalchemy import orm +from sqlalchemy.ext.declarative import declarative_base + + +Base = declarative_base() + + +class Product(Base): + """ + Represents a product, purchased and/or sold by the organization. + """ + __tablename__ = 'products' + # __table_args__ = ( + # sa.ForeignKeyConstraint(['department'], ['departments.dept_no']), + # sa.ForeignKeyConstraint(['subdept'], ['subdepts.subdept_no']), + # sa.ForeignKeyConstraint(['tax'], ['taxrates.id']), + # ) + + id = sa.Column(sa.Integer(), nullable=False, + primary_key=True, autoincrement=True) + + upc = sa.Column(sa.String(length=13), nullable=True) + + description = sa.Column(sa.String(length=30), nullable=True) + + brand = sa.Column(sa.String(length=30), nullable=True) + + formatted_name = sa.Column(sa.String(length=30), nullable=True) + + normal_price = sa.Column(sa.Float(), nullable=True) + + price_method = sa.Column('pricemethod', sa.SmallInteger(), nullable=True) + + group_price = sa.Column('groupprice', sa.Float(), nullable=True) + + quantity = sa.Column(sa.SmallInteger(), nullable=True) + + special_price = sa.Column(sa.Float(), nullable=True) + + special_price_method = sa.Column('specialpricemethod', sa.SmallInteger(), nullable=True) + + special_group_price = sa.Column('specialgroupprice', sa.Float(), nullable=True) + + special_quantity = sa.Column('specialquantity', sa.SmallInteger(), nullable=True) + + special_limit = sa.Column(sa.SmallInteger(), nullable=True) + + start_date = sa.Column(sa.DateTime(), nullable=True) + + end_date = sa.Column(sa.DateTime(), nullable=True) + + department_number = sa.Column('department', sa.SmallInteger(), nullable=True) + # department = orm.relationship( + # Department, + # primaryjoin=Department.number == department_number, + # foreign_keys=[department_number], + # doc=""" + # Reference to the :class:`Department` to which the product belongs. + # """) + + size = sa.Column(sa.String(length=9), nullable=True) + + tax_rate_id = sa.Column('tax', sa.SmallInteger(), nullable=True) + # tax_rate = orm.relationship(TaxRate) + + foodstamp = sa.Column(sa.Boolean(), nullable=True) + + scale = sa.Column(sa.Boolean(), nullable=True) + + scale_price = sa.Column('scaleprice', sa.Float(), nullable=True) + + mix_match_code = sa.Column('mixmatchcode', sa.String(length=13), nullable=True) + + created = sa.Column(sa.DateTime(), nullable=True) + + modified = sa.Column(sa.DateTime(), nullable=True) + + # TODO: what to do about this 'replaces' thing? + # 'batchID'=>array('type'=>'TINYINT', 'replaces'=>'advertised'), + # batch_id = sa.Column('batchID', sa.SmallInteger(), nullable=True) + # advertised = sa.Column(sa.Boolean(), nullable=True) + + tare_weight = sa.Column('tareweight', sa.Float(), nullable=True) + + discount = sa.Column(sa.SmallInteger(), nullable=True) + + discount_type = sa.Column('discounttype', sa.SmallInteger(), nullable=True) + + line_item_discountable = sa.Column(sa.Boolean(), nullable=True) + + unit_of_measure = sa.Column('unitofmeasure', sa.String(length=15), nullable=True) + + wicable = sa.Column(sa.SmallInteger(), nullable=True) + + quantity_enforced = sa.Column('qttyEnforced', sa.Boolean(), nullable=True) + + id_enforced = sa.Column('idEnforced', sa.SmallInteger(), nullable=True) + + cost = sa.Column(sa.Float(), nullable=True) + + special_cost = sa.Column(sa.Float(), nullable=True) + + received_cost = sa.Column(sa.Float(), nullable=True) + + in_use = sa.Column('inUse', sa.Boolean(), nullable=True) + + flags = sa.Column('numflag', sa.Integer(), nullable=True) + + subdepartment_number = sa.Column('subdept', sa.SmallInteger(), nullable=True) + # subdepartment = orm.relationship( + # Subdepartment, + # primaryjoin=Subdepartment.number == subdepartment_number, + # foreign_keys=[subdepartment_number], + # doc=""" + # Reference to the :class:`Subdepartment` to which the product belongs. + # """) + + deposit = sa.Column(sa.Float(), nullable=True) + + local = sa.Column(sa.Integer(), nullable=True, + default=0) # TODO: do we want a default here? + + store_id = sa.Column(sa.SmallInteger(), nullable=True) + + default_vendor_id = sa.Column(sa.Integer(), nullable=True) + # default_vendor = orm.relationship( + # Vendor, + # primaryjoin=Vendor.id == default_vendor_id, + # foreign_keys=[default_vendor_id], + # doc=""" + # Reference to the default :class:`Vendor` from which the product is obtained. + # """) + + current_origin_id = sa.Column(sa.Integer(), nullable=True) + + auto_par = sa.Column(sa.Float(), nullable=True, + default=0) # TODO: do we want a default here? + + price_rule_id = sa.Column(sa.Integer(), nullable=True) + + # TODO: some older DB's might not have this? guess we'll see + last_sold = sa.Column(sa.DateTime(), nullable=True) From b4a07f9875c4a5f973ffa3f6b0dc7e5f1dcf0467 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 21 Jul 2021 20:17:56 -0500 Subject: [PATCH 056/136] Update changelog --- CHANGELOG.md | 4 ++++ corepos/_version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff62b28..dcaf8dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.1.3] - 2021-07-21 +### Changed +- Add basic 'lane_op' DB schema. + ## [0.1.2] - 2021-06-11 ### Changed - Several more updates, mostly a "save point" release. diff --git a/corepos/_version.py b/corepos/_version.py index de58c20..d60824a 100644 --- a/corepos/_version.py +++ b/corepos/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.2' +__version__ = '0.1.3' From e4c46b3fa4c601f44e0455df5f0835571c8592ce Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 2 Aug 2021 08:56:05 -0500 Subject: [PATCH 057/136] Add schema for `TableSyncRules` --- corepos/db/office_op/model.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 5e22e10..182f3fe 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -92,6 +92,22 @@ class Parameter(Base): return "{}-{} {}".format(self.store_id, self.lane_id, self.param_key) +class TableSyncRule(Base): + """ + Represents a "table sync rule" value. + """ + __tablename__ = 'TableSyncRules' + + id = sa.Column('tableSyncRuleID', sa.Integer(), nullable=False, primary_key=True, autoincrement=True) + + table_name = sa.Column('tableName', sa.String(length=255), nullable=False, primary_key=True) + + rule = sa.Column(sa.String(length=255), nullable=True) + + def __str__(self): + return "{}: {}".format(self.table_name, self.rule) + + class Store(Base): """ Represents a known store. From 07e3c62b6c575dbdcb4b3c1f381908ddb4c53e5e Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 2 Aug 2021 09:07:50 -0500 Subject: [PATCH 058/136] Update changelog --- CHANGELOG.md | 4 ++++ corepos/_version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcaf8dc..cfab1d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.1.4] - 2021-08-02 +### Changed +- Add schema for `TableSyncRules`. + ## [0.1.3] - 2021-07-21 ### Changed - Add basic 'lane_op' DB schema. diff --git a/corepos/_version.py b/corepos/_version.py index d60824a..483f94f 100644 --- a/corepos/_version.py +++ b/corepos/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.3' +__version__ = '0.1.4' From 092884eab366cefb51c9b10a6569c4a84e4e1ef6 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 31 Aug 2021 22:40:58 -0500 Subject: [PATCH 059/136] Add lane_op model for Department --- corepos/db/lane_op/model.py | 41 +++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/corepos/db/lane_op/model.py b/corepos/db/lane_op/model.py index 2faa87b..b3e5901 100644 --- a/corepos/db/lane_op/model.py +++ b/corepos/db/lane_op/model.py @@ -32,6 +32,47 @@ from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() +class Department(Base): + """ + Represents a department within the organization. + """ + __tablename__ = 'departments' + + number = sa.Column('dept_no', sa.SmallInteger(), nullable=False, + primary_key=True, autoincrement=False) + + name = sa.Column('dept_name', sa.String(length=30), nullable=True) + + tax = sa.Column('dept_tax', sa.Boolean(), nullable=True) + + food_stampable = sa.Column('dept_fs', sa.Boolean(), nullable=True) + + limit = sa.Column('dept_limit', sa.Float(), nullable=True) + + minimum = sa.Column('dept_minimum', sa.Float(), nullable=True) + + discount = sa.Column('dept_discount', sa.Boolean(), nullable=True) + + see_id = sa.Column('dept_see_id', sa.SmallInteger(), nullable=True) + + modified = sa.Column(sa.DateTime(), nullable=True) + + modified_by_id = sa.Column('modifiedby', sa.Integer(), nullable=True) + + margin = sa.Column(sa.Float(), nullable=False) + + sales_code = sa.Column('salesCode', sa.Integer(), nullable=False) + + member_only = sa.Column('memberOnly', sa.SmallInteger(), nullable=False) + + line_item_discount = sa.Column(sa.Boolean(), nullable=True) + + wicable = sa.Column('dept_wicable', sa.Boolean(), nullable=True) + + def __str__(self): + return self.name or "" + + class Product(Base): """ Represents a product, purchased and/or sold by the organization. From 69c7be6356aadc0c81f684b27eab063f1fde0b6b Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 31 Aug 2021 22:42:22 -0500 Subject: [PATCH 060/136] Update changelog --- CHANGELOG.md | 4 ++++ corepos/_version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfab1d1..78a864d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.1.5] - 2021-08-31 +### Changed +- Add lane_op model for Department. + ## [0.1.4] - 2021-08-02 ### Changed - Add schema for `TableSyncRules`. diff --git a/corepos/_version.py b/corepos/_version.py index 483f94f..67ed0c1 100644 --- a/corepos/_version.py +++ b/corepos/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.4' +__version__ = '0.1.5' From 0d81a41b54ccad1362f2e25f83e231cf6015c40f Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 7 Sep 2021 15:45:51 -0500 Subject: [PATCH 061/136] Add `User` model for office_op --- corepos/db/office_op/model.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 182f3fe..1cb0bd9 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -108,6 +108,32 @@ class TableSyncRule(Base): return "{}: {}".format(self.table_name, self.rule) +class User(Base): + """ + Represents a user within CORE Office. + """ + __tablename__ = 'Users' + + name = sa.Column(sa.String(length=50), nullable=False, primary_key=True) + + password = sa.Column(sa.String(length=255), nullable=True) + + salt = sa.Column(sa.String(length=10), nullable=True) + + uid = sa.Column(sa.String(length=4), nullable=True) + + session_id = sa.Column(sa.String(length=50), nullable=True) + + real_name = sa.Column(sa.String(length=75), nullable=True) + + email = sa.Column(sa.String(length=75), nullable=True) + + totp_url = sa.Column('totpURL', sa.String(length=255), nullable=True) + + def __str__(self): + return self.name or "" + + class Store(Base): """ Represents a known store. From 754d8697e8a53c917534605f9044e37e1e3bdc53 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 30 Sep 2021 18:44:53 -0400 Subject: [PATCH 062/136] Add proper support for `str(Suspension)` --- corepos/db/office_op/model.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 1cb0bd9..97c61d0 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -1469,6 +1469,10 @@ class Suspension(Base): reason_code = sa.Column('reasoncode', sa.Integer(), nullable=True) reason_object = orm.relationship(ReasonCode) + def __str__(self): + return "#{} on {}".format(self.card_number, + self.suspension_date) + class HouseCoupon(Base): """ From 41c142b8379e7f958339174bdd0a77898a677577 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 4 Nov 2021 17:41:39 -0500 Subject: [PATCH 063/136] Add the `custdata` model for lane_op DB also use Numeric instead of Float for "MONEY" columns, for custdata in both lane_op and office_op --- corepos/db/lane_op/model.py | 68 +++++++++++++++++++++++++++++++++++ corepos/db/office_op/model.py | 10 +++--- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/corepos/db/lane_op/model.py b/corepos/db/lane_op/model.py index b3e5901..80051ab 100644 --- a/corepos/db/lane_op/model.py +++ b/corepos/db/lane_op/model.py @@ -208,3 +208,71 @@ class Product(Base): # TODO: some older DB's might not have this? guess we'll see last_sold = sa.Column(sa.DateTime(), nullable=True) + + +class CustData(Base): + """ + Represents a customer of the organization. + + https://github.com/CORE-POS/IS4C/blob/master/pos/is4c-nf/lib/models/op/CustdataModel.php + """ + __tablename__ = 'custdata' + # __table_args__ = ( + # sa.ForeignKeyConstraint(['memType'], ['memtype.memtype']), + # ) + + id = sa.Column(sa.Integer(), nullable=False, primary_key=True, autoincrement=True) + + card_number = sa.Column('CardNo', sa.Integer(), nullable=True) + + person_number = sa.Column('personNum', sa.SmallInteger(), nullable=True) + + first_name = sa.Column('FirstName', sa.String(length=30), nullable=True) + + last_name = sa.Column('LastName', sa.String(length=30), nullable=True) + + cash_back = sa.Column('CashBack', sa.Numeric(precision=10, scale=2), nullable=True) + + balance = sa.Column('Balance', sa.Numeric(precision=10, scale=2), nullable=True) + + discount = sa.Column('Discount', sa.SmallInteger(), nullable=True) + + member_discount_limit = sa.Column('MemDiscountLimit', sa.Numeric(precision=10, scale=2), nullable=True) + + charge_limit = sa.Column('ChargeLimit', sa.Numeric(precision=10, scale=2), nullable=True) + + charge_ok = sa.Column('ChargeOk', sa.Boolean(), nullable=True, default=True) + + write_checks = sa.Column('WriteChecks', sa.Boolean(), nullable=True, default=True) + + store_coupons = sa.Column('StoreCoupons', sa.Boolean(), nullable=True, default=True) + + type = sa.Column('Type', sa.String(length=10), nullable=True, default='PC') + + member_type_id = sa.Column('memType', sa.SmallInteger(), nullable=True) + # member_type = orm.relationship( + # MemberType, + # primaryjoin=MemberType.id == member_type_id, + # foreign_keys=[member_type_id], + # doc=""" + # Reference to the :class:`MemberType` to which this member belongs. + # """) + + staff = sa.Column(sa.Boolean(), nullable=True, default=False) + + ssi = sa.Column('SSI', sa.Boolean(), nullable=True, default=False) + + purchases = sa.Column('Purchases', sa.Numeric(precision=10, scale=2), nullable=True, default=0) + + number_of_checks = sa.Column('NumberOfChecks', sa.SmallInteger(), nullable=True, default=0) + + member_coupons = sa.Column('memCoupons', sa.Integer(), nullable=True, default=1) + + blue_line = sa.Column('blueLine', sa.String(length=50), nullable=True) + + shown = sa.Column('Shown', sa.Boolean(), nullable=True, default=True) + + last_change = sa.Column('LastChange', sa.DateTime(), nullable=True) + + def __str__(self): + return "{} {}".format(self.first_name or '', self.last_name or '').strip() diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 97c61d0..ce43a59 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -1156,15 +1156,15 @@ class CustData(Base): last_name = sa.Column('LastName', sa.String(length=30), nullable=True) - cash_back = sa.Column('CashBack', sa.Float(), nullable=False, default=60) + cash_back = sa.Column('CashBack', sa.Numeric(precision=10, scale=2), nullable=False, default=60) - balance = sa.Column('Balance', sa.Float(), nullable=False, default=0) + balance = sa.Column('Balance', sa.Numeric(precision=10, scale=2), nullable=False, default=0) discount = sa.Column('Discount', sa.SmallInteger(), nullable=True) - member_discount_limit = sa.Column('MemDiscountLimit', sa.Float(), nullable=False, default=0) + member_discount_limit = sa.Column('MemDiscountLimit', sa.Numeric(precision=10, scale=2), nullable=False, default=0) - charge_limit = sa.Column('ChargeLimit', sa.Float(), nullable=False, default=0) + charge_limit = sa.Column('ChargeLimit', sa.Numeric(precision=10, scale=2), nullable=False, default=0) charge_ok = sa.Column('ChargeOk', sa.Boolean(), nullable=False, default=False) @@ -1187,7 +1187,7 @@ class CustData(Base): ssi = sa.Column('SSI', sa.Boolean(), nullable=False, default=False) - purchases = sa.Column('Purchases', sa.Float(), nullable=False, default=0) + purchases = sa.Column('Purchases', sa.Numeric(precision=10, scale=2), nullable=False, default=0) number_of_checks = sa.Column('NumberOfChecks', sa.SmallInteger(), nullable=False, default=0) From ae45615717066509b4cf95898d57ff55c026f6f7 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 4 Nov 2021 21:26:22 -0500 Subject: [PATCH 064/136] Update changelog --- CHANGELOG.md | 6 ++++++ corepos/_version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78a864d..884a776 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.1.6] - 2021-11-04 +### Changed +- Add `User` model for office_op. +- Add proper support for `str(Suspension)`. +- Add the `custdata` model for lane_op DB. + ## [0.1.5] - 2021-08-31 ### Changed - Add lane_op model for Department. diff --git a/corepos/_version.py b/corepos/_version.py index 67ed0c1..955f1d8 100644 --- a/corepos/_version.py +++ b/corepos/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.5' +__version__ = '0.1.6' From a6306a48821315267ff1557069c8e6bde6761f1d Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 30 Dec 2021 21:47:55 -0600 Subject: [PATCH 065/136] Remove deprecation warning for `corepos.db` everything should be using the right imports now --- corepos/db/__init__.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/corepos/db/__init__.py b/corepos/db/__init__.py index 174fee4..db5c837 100644 --- a/corepos/db/__init__.py +++ b/corepos/db/__init__.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2020 Lance Edgar +# Copyright © 2018-2021 Lance Edgar # # This file is part of pyCOREPOS. # @@ -23,12 +23,3 @@ """ Database Interface """ - -from __future__ import unicode_literals, absolute_import - -import warnings -warnings.warn("The `corepos.db` module is deprecated! " - "Please use `corepos.db.office_op` instead.", - DeprecationWarning) - -from corepos.db.office_op import * From b46282264c808792a2492807c8305e2943b9b566 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 17 Jan 2022 18:59:19 -0600 Subject: [PATCH 066/136] Add model for `UserGroup` --- corepos/db/office_op/model.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index ce43a59..c913ae0 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -108,6 +108,24 @@ class TableSyncRule(Base): return "{}: {}".format(self.table_name, self.rule) +class UserGroup(Base): + """ + Represents a user/group assignment. + """ + __tablename__ = 'userGroups' + + group_id = sa.Column('gid', sa.Integer(), nullable=False, + primary_key=True, autoincrement=False) + + name = sa.Column(sa.String(length=50), nullable=True) + + username = sa.Column(sa.String(length=50), nullable=False, + primary_key=True) + + def __str__(self): + return self.name or "" + + class User(Base): """ Represents a user within CORE Office. From af6189b23724c4af9b645090f5a9ee0832925af5 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 2 Mar 2022 21:35:57 -0600 Subject: [PATCH 067/136] Update changelog --- CHANGELOG.md | 5 +++++ corepos/_version.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 884a776..f83bdb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.1.7] - 2022-03-02 +### Changed +- Remove deprecation warning for `corepos.db`. +- Add model for `UserGroup`. + ## [0.1.6] - 2021-11-04 ### Changed - Add `User` model for office_op. diff --git a/corepos/_version.py b/corepos/_version.py index 955f1d8..4cdc594 100644 --- a/corepos/_version.py +++ b/corepos/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.6' +__version__ = '0.1.7' From 2b80fd6a6ba5add0ba82fb0ad30ed5768c113b4a Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 26 Mar 2022 23:04:05 -0500 Subject: [PATCH 068/136] Add basic `TransactionDetail` for trans archive model --- corepos/db/office_trans/model.py | 10 ++++-- corepos/db/office_trans_archive/__init__.py | 30 ++++++++++++++++ corepos/db/office_trans_archive/model.py | 39 +++++++++++++++++++++ 3 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 corepos/db/office_trans_archive/__init__.py create mode 100644 corepos/db/office_trans_archive/model.py diff --git a/corepos/db/office_trans/model.py b/corepos/db/office_trans/model.py index 55aff86..7b6240c 100644 --- a/corepos/db/office_trans/model.py +++ b/corepos/db/office_trans/model.py @@ -35,11 +35,10 @@ Base = declarative_base() @six.python_2_unicode_compatible -class TransactionDetail(Base): +class TransactionDetailBase(object): """ Represents a POS transaction detail record. """ - __tablename__ = 'dtransactions' # store store_row_id = sa.Column(sa.Integer(), primary_key=True, nullable=False) @@ -123,3 +122,10 @@ class TransactionDetail(Base): def __str__(self): return self.description or '' + + +class TransactionDetail(TransactionDetailBase, Base): + """ + Represents a POS transaction detail record. + """ + __tablename__ = 'dtransactions' diff --git a/corepos/db/office_trans_archive/__init__.py b/corepos/db/office_trans_archive/__init__.py new file mode 100644 index 0000000..e25b200 --- /dev/null +++ b/corepos/db/office_trans_archive/__init__.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# pyCOREPOS -- Python Interface to CORE POS +# Copyright © 2018-2022 Lance Edgar +# +# This file is part of pyCOREPOS. +# +# pyCOREPOS is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# pyCOREPOS is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# pyCOREPOS. If not, see . +# +################################################################################ +""" +"Archive" Transaction Database Interface +""" + +from sqlalchemy import orm + + +Session = orm.sessionmaker() diff --git a/corepos/db/office_trans_archive/model.py b/corepos/db/office_trans_archive/model.py new file mode 100644 index 0000000..ce70603 --- /dev/null +++ b/corepos/db/office_trans_archive/model.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# pyCOREPOS -- Python Interface to CORE POS +# Copyright © 2018-2022 Lance Edgar +# +# This file is part of pyCOREPOS. +# +# pyCOREPOS is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# pyCOREPOS is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# pyCOREPOS. If not, see . +# +################################################################################ +""" +CORE POS Transaction Data Model +""" + +from sqlalchemy.ext.declarative import declarative_base + +from corepos.db.office_trans.model import TransactionDetailBase + + +Base = declarative_base() + + +class TransactionDetail(TransactionDetailBase, Base): + """ + Represents a POS transaction detail record. + """ + __tablename__ = 'bigArchive' From c2723de467a920f9d6b7aea3fed3692eba5502cc Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 21 Aug 2022 00:11:24 -0500 Subject: [PATCH 069/136] Delete `productUser` record when `products` record is deleted --- corepos/db/office_op/model.py | 1 + 1 file changed, 1 insertion(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index c913ae0..107da66 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -757,6 +757,7 @@ class ProductUser(Base): backref=orm.backref( 'user_info', uselist=False, + cascade='all, delete-orphan', doc=""" Reference to the :class:`ProductUser` record for this product, if any. """)) From 2444628c13e652ebaebbdbffc497448ba56e2e45 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 2 Jan 2023 16:56:30 -0600 Subject: [PATCH 070/136] Update changelog --- CHANGELOG.md | 5 +++++ corepos/_version.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f83bdb7..881ced9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.1.8] - 2023-01-02 +### Changed +- Add basic `TransactionDetail` for trans archive model. +- Delete `productUser` record when `products` record is deleted. + ## [0.1.7] - 2022-03-02 ### Changed - Remove deprecation warning for `corepos.db`. diff --git a/corepos/_version.py b/corepos/_version.py index 4cdc594..4e18c2b 100644 --- a/corepos/_version.py +++ b/corepos/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.7' +__version__ = '0.1.8' From ad5837405f975b31b292ce9d3d7e8b9519103086 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 15 Feb 2023 12:51:50 -0600 Subject: [PATCH 071/136] Require SQLAlchemy 1.4.x --- corepos/db/lane_op/model.py | 7 +++---- corepos/db/office_op/model.py | 5 ++--- corepos/db/office_trans/model.py | 10 +++------- corepos/db/office_trans_archive/model.py | 6 +++--- setup.py | 5 ++--- 5 files changed, 13 insertions(+), 20 deletions(-) diff --git a/corepos/db/lane_op/model.py b/corepos/db/lane_op/model.py index 80051ab..1c687ab 100644 --- a/corepos/db/lane_op/model.py +++ b/corepos/db/lane_op/model.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2021 Lance Edgar +# Copyright © 2018-2023 Lance Edgar # # This file is part of pyCOREPOS. # @@ -25,11 +25,10 @@ Data model for CORE POS "lane_op" DB """ import sqlalchemy as sa -# from sqlalchemy import orm -from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import orm -Base = declarative_base() +Base = orm.declarative_base() class Department(Base): diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 107da66..132fd33 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2021 Lance Edgar +# Copyright © 2018-2023 Lance Edgar # # This file is part of pyCOREPOS. # @@ -29,13 +29,12 @@ import logging import sqlalchemy as sa from sqlalchemy import orm -from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.associationproxy import association_proxy log = logging.getLogger(__name__) -Base = declarative_base() +Base = orm.declarative_base() class StringableDateTime(sa.TypeDecorator): diff --git a/corepos/db/office_trans/model.py b/corepos/db/office_trans/model.py index 7b6240c..20c244c 100644 --- a/corepos/db/office_trans/model.py +++ b/corepos/db/office_trans/model.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2020 Lance Edgar +# Copyright © 2018-2023 Lance Edgar # # This file is part of pyCOREPOS. # @@ -24,17 +24,13 @@ CORE POS Transaction Data Model """ -from __future__ import unicode_literals, absolute_import - -import six import sqlalchemy as sa -from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import orm -Base = declarative_base() +Base = orm.declarative_base() -@six.python_2_unicode_compatible class TransactionDetailBase(object): """ Represents a POS transaction detail record. diff --git a/corepos/db/office_trans_archive/model.py b/corepos/db/office_trans_archive/model.py index ce70603..8f4a079 100644 --- a/corepos/db/office_trans_archive/model.py +++ b/corepos/db/office_trans_archive/model.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2022 Lance Edgar +# Copyright © 2018-2023 Lance Edgar # # This file is part of pyCOREPOS. # @@ -24,12 +24,12 @@ CORE POS Transaction Data Model """ -from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import orm from corepos.db.office_trans.model import TransactionDetailBase -Base = declarative_base() +Base = orm.declarative_base() class TransactionDetail(TransactionDetailBase, Base): diff --git a/setup.py b/setup.py index 098d0fc..2c5f0f2 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2020 Lance Edgar +# Copyright © 2018-2023 Lance Edgar # # This file is part of pyCOREPOS. # @@ -63,7 +63,7 @@ requires = [ 'mysql-connector-python', # 8.0.6 'requests', # 2.23.0 'six', # 1.12.0 - 'SQLAlchemy', # 0.9.8 + 'SQLAlchemy>=1.4', # 1.4.0 ] @@ -87,7 +87,6 @@ setup( 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', 'Topic :: Office/Business', 'Topic :: Software Development :: Libraries :: Python Modules', ], From 24fb8c8feab6cd276de999f76793485f6460aa89 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 1 May 2023 22:17:38 -0500 Subject: [PATCH 072/136] Update changelog --- CHANGELOG.md | 4 ++++ corepos/_version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 881ced9..a7925f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.1.9] - 2023-05-01 +### Changed +- Require SQLAlchemy 1.4.x. + ## [0.1.8] - 2023-01-02 ### Changed - Add basic `TransactionDetail` for trans archive model. diff --git a/corepos/_version.py b/corepos/_version.py index 4e18c2b..def08d5 100644 --- a/corepos/_version.py +++ b/corepos/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.8' +__version__ = '0.1.9' From 6e43449ecbd863ca80975fb0b63b0e74c3019dfc Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 16 May 2023 13:16:41 -0500 Subject: [PATCH 073/136] Replace `setup.py` contents with `setup.cfg` --- setup.cfg | 33 +++++++++++++++++++++++++ setup.py | 74 ++----------------------------------------------------- 2 files changed, 35 insertions(+), 72 deletions(-) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..728f179 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,33 @@ +# -*- coding: utf-8; -*- + +[metadata] +name = pyCOREPOS +version = attr: corepos.__version__ +author = Lance Edgar +author_email = lance@edbob.org +url = https://rattailproject.org/ +license = GNU GPL v3 +description = Python Interface to CORE POS +long_description = file: README.rst +classifiers = + Development Status :: 3 - Alpha + Environment :: Console + Environment :: Web Environment + Intended Audience :: Developers + License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) + Natural Language :: English + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 3 + Topic :: Office/Business + Topic :: Software Development :: Libraries :: Python Modules + + +[options] +install_requires = + mysql-connector-python + requests + six + SQLAlchemy>=1.4 + +packages = find: diff --git a/setup.py b/setup.py index 2c5f0f2..a02ef90 100644 --- a/setup.py +++ b/setup.py @@ -21,76 +21,6 @@ # ################################################################################ -import os -import sys -from setuptools import setup, find_packages +from setuptools import setup - -here = os.path.abspath(os.path.dirname(__file__)) -exec(open(os.path.join(here, 'corepos', '_version.py')).read()) -README = open(os.path.join(here, 'README.rst')).read() - - -requires = [ - # - # Version numbers within comments below have specific meanings. - # Basically the 'low' value is a "soft low," and 'high' a "soft high." - # In other words: - # - # If either a 'low' or 'high' value exists, the primary point to be - # made about the value is that it represents the most current (stable) - # version available for the package (assuming typical public access - # methods) whenever this project was started and/or documented. - # Therefore: - # - # If a 'low' version is present, you should know that attempts to use - # versions of the package significantly older than the 'low' version - # may not yield happy results. (A "hard" high limit may or may not be - # indicated by a true version requirement.) - # - # Similarly, if a 'high' version is present, and especially if this - # project has laid dormant for a while, you may need to refactor a bit - # when attempting to support a more recent version of the package. (A - # "hard" low limit should be indicated by a true version requirement - # when a 'high' version is present.) - # - # In any case, developers and other users are encouraged to play - # outside the lines with regard to these soft limits. If bugs are - # encountered then they should be filed as such. - # - # package # low high - - 'mysql-connector-python', # 8.0.6 - 'requests', # 2.23.0 - 'six', # 1.12.0 - 'SQLAlchemy>=1.4', # 1.4.0 -] - - -setup( - name = "pyCOREPOS", - version = __version__, - author = "Lance Edgar", - author_email = "lance@edbob.org", - url = "https://rattailproject.org/", - license = "GNU GPL v3", - description = "Python Interface to CORE POS", - long_description = README, - - classifiers = [ - 'Development Status :: 3 - Alpha', - 'Environment :: Console', - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', - 'Natural Language :: English', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Topic :: Office/Business', - 'Topic :: Software Development :: Libraries :: Python Modules', - ], - - install_requires = requires, - packages = find_packages(), -) +setup() From 757fb50a967a624323fe16df0597b8cf237176ae Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 17 May 2023 06:57:14 -0500 Subject: [PATCH 074/136] Update changelog --- CHANGELOG.md | 4 ++++ corepos/_version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7925f0..7686346 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.1.10] - 2023-05-17 +### Changed +- Replace `setup.py` contents with `setup.cfg`. + ## [0.1.9] - 2023-05-01 ### Changed - Require SQLAlchemy 1.4.x. diff --git a/corepos/_version.py b/corepos/_version.py index def08d5..18c177e 100644 --- a/corepos/_version.py +++ b/corepos/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.9' +__version__ = '0.1.10' From d58426c07369724e8d33fe8dbb41e36a6086eba6 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 22 May 2023 21:34:46 -0500 Subject: [PATCH 075/136] Add support for htdigest auth when using CORE webservices API --- corepos/api.py | 54 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/corepos/api.py b/corepos/api.py index e7310a0..c93ff9d 100644 --- a/corepos/api.py +++ b/corepos/api.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2021 Lance Edgar +# Copyright © 2018-2023 Lance Edgar # # This file is part of pyCOREPOS. # @@ -28,6 +28,7 @@ import json import logging import requests +from requests.auth import HTTPDigestAuth log = logging.getLogger(__name__) @@ -48,27 +49,42 @@ class CoreAPIError(Exception): class CoreWebAPI(object): """ Client implementation for the CORE webservices API. + + :param str url: URL to the CORE webservices API, + e.g. ``'http://localhost/fannie/ws/'`` + + :param bool verify: How to handle certificate validation for HTTPS + URLs. This value is passed as-is to the ``requests`` library, + so see those docs for more info. The default value for this is + ``True`` because the assumption is that security should be on + by default. Set it to ``False`` in order to disable validation + entirely, e.g. for self-signed certs. (This may also be needed + for basic HTTP URLs?) Other values may be possible also; again + see the ``requests`` docs for more info. + + :param htdigest_username: Username for htdigest authentication, if + applicable. + + :param htdigest_password: Password for htdigest authentication, if + applicable. """ - def __init__(self, url, verify=True): - """ - Constructor for the API client. - - :param str url: URL to the CORE webservices API, - e.g. ``'http://localhost/fannie/ws/'`` - - :param bool verify: How to handle certificate validation for HTTPS - URLs. This value is passed as-is to the ``requests`` library, so - see those docs for more info. The default value for this is - ``True`` because the assumption is that security should be on by - default. Set it to ``False`` in order to disable validation - entirely, e.g. for self-signed certs. (This may also be needed for - basic HTTP URLs?) Other values may be possible also; again see the - ``requests`` docs for more info. - """ + def __init__( + self, + url, + verify=True, + htdigest_username=None, + htdigest_password=None, + ): self.url = url self.verify = verify + self.session = requests.Session() + + if htdigest_username and htdigest_password: + self.session.auth = HTTPDigestAuth(htdigest_username, + htdigest_password) + def post(self, params, method=None): """ Issue a POST request to the API, with the given ``params``. If not @@ -88,8 +104,8 @@ class CoreWebAPI(object): 'id': 1, } - response = requests.post(self.url, data=json.dumps(payload), - verify=self.verify) + response = self.session.post(self.url, data=json.dumps(payload), + verify=self.verify) response.raise_for_status() return response From bb8278dcc8a7c91656958532f300994e34e55233 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 2 Jun 2023 14:26:51 -0500 Subject: [PATCH 076/136] Update changelog --- CHANGELOG.md | 4 ++++ corepos/_version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7686346..20d31e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.1.11] - 2023-06-02 +### Changed +- Add support for htdigest auth when using CORE webservices API. + ## [0.1.10] - 2023-05-17 ### Changed - Replace `setup.py` contents with `setup.cfg`. diff --git a/corepos/_version.py b/corepos/_version.py index 18c177e..862eaaa 100644 --- a/corepos/_version.py +++ b/corepos/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.10' +__version__ = '0.1.11' From 852a989bd53516b1371cb1d901d3b33a32cf042f Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 6 Jun 2023 13:13:51 -0500 Subject: [PATCH 077/136] Add `get_member_types()` method for CORE API --- corepos/api.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/corepos/api.py b/corepos/api.py index c93ff9d..97ad8c7 100644 --- a/corepos/api.py +++ b/corepos/api.py @@ -132,6 +132,22 @@ class CoreWebAPI(object): assert set(js['result'].keys()) == set(['result']) return js['result']['result'] + def get_member_types(self): + """ + Fetch all Member Type records from CORE. + + :returns: A (potentially empty) list of member type dict records. + """ + params = { + 'entity': 'Memtype', + 'submethod': 'get', + 'columns': {}, + } + + response = self.post(params) + result = self.parse_response(response) + return [json.loads(rec) for rec in result] + def get_members(self): """ Fetch all Member records from CORE. From 4952d2fa3d3e80056f016723e9529c9659d980aa Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 12 Jun 2023 17:28:55 -0500 Subject: [PATCH 078/136] Rename `custdata` model to `CustomerClassic` --- corepos/db/lane_op/model.py | 6 +++++- corepos/db/office_op/model.py | 20 ++++++++++++-------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/corepos/db/lane_op/model.py b/corepos/db/lane_op/model.py index 1c687ab..c7ed5b2 100644 --- a/corepos/db/lane_op/model.py +++ b/corepos/db/lane_op/model.py @@ -209,7 +209,7 @@ class Product(Base): last_sold = sa.Column(sa.DateTime(), nullable=True) -class CustData(Base): +class CustomerClassic(Base): """ Represents a customer of the organization. @@ -275,3 +275,7 @@ class CustData(Base): def __str__(self): return "{} {}".format(self.first_name or '', self.last_name or '').strip() + + +# TODO: deprecate / remove this +CustData = CustomerClassic diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 132fd33..9ec20d2 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -1153,7 +1153,7 @@ class Customer(Base): return "{} {}".format(self.first_name or '', self.last_name or '').strip() -class CustData(Base): +class CustomerClassic(Base): """ Represents a customer of the organization. @@ -1219,7 +1219,7 @@ class CustData(Base): member_info = orm.relationship( 'MemberInfo', - primaryjoin='MemberInfo.card_number == CustData.card_number', + primaryjoin='MemberInfo.card_number == CustomerClassic.card_number', foreign_keys=[card_number], uselist=False, back_populates='customers', @@ -1231,6 +1231,10 @@ class CustData(Base): return "{} {}".format(self.first_name or '', self.last_name or '').strip() +# TODO: deprecate / remove this +CustData = CustomerClassic + + class MemberInfo(Base): """ Contact info regarding a member of the organization. @@ -1264,14 +1268,14 @@ class MemberInfo(Base): ads_ok = sa.Column('ads_OK', sa.Boolean(), nullable=True, default=True) customers = orm.relationship( - CustData, - primaryjoin=CustData.card_number == card_number, - order_by=CustData.person_number, - foreign_keys=[CustData.card_number], + CustomerClassic, + primaryjoin=CustomerClassic.card_number == card_number, + order_by=CustomerClassic.person_number, + foreign_keys=[CustomerClassic.card_number], back_populates='member_info', - remote_side=CustData.card_number, + remote_side=CustomerClassic.card_number, doc=""" - List of :class:`CustData` instances which are associated with this member info. + List of :class:`CustomerClassic` instances which are associated with this member info. """) dates = orm.relationship( From c6144ab31025f65e963b8a80a85d5c3d7deb0c0f Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 12 Jun 2023 20:38:12 -0500 Subject: [PATCH 079/136] Add note about `meminfo.email_2` field, aka. "alt. phone" --- corepos/db/office_op/model.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 9ec20d2..7328525 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -1263,7 +1263,10 @@ class MemberInfo(Base): email = sa.Column('email_1', sa.String(length=50), nullable=True) - email2 = sa.Column('email_2', sa.String(length=50), nullable=True) + email2 = sa.Column('email_2', sa.String(length=50), nullable=True, doc=""" + NB. this is labeled "Alt. Phone" in CORE Office member view, and + is named `altPhone` when dealing with CORE Office webservices API. + """) ads_ok = sa.Column('ads_OK', sa.Boolean(), nullable=True, default=True) From b2622c473d669908c989286bde30c0ae873ce098 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 12 Jun 2023 20:39:12 -0500 Subject: [PATCH 080/136] Update changelog --- CHANGELOG.md | 6 ++++++ corepos/_version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20d31e0..58781cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.1.12] - 2023-06-12 +### Changed +- Add `get_member_types()` method for CORE API. +- Rename model for `custdata` to `CustomerClassic`. +- Add note about `meminfo.email_2` field, aka. "alt. phone". + ## [0.1.11] - 2023-06-02 ### Changed - Add support for htdigest auth when using CORE webservices API. diff --git a/corepos/_version.py b/corepos/_version.py index 862eaaa..002a140 100644 --- a/corepos/_version.py +++ b/corepos/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.11' +__version__ = '0.1.12' From 5a77d14a261dc770a4f13fc3e632065ca8ec48b1 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 18 Jun 2023 11:38:14 -0500 Subject: [PATCH 081/136] Add models for StockPurchase and EquityLiveBalance --- corepos/db/office_trans/model.py | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/corepos/db/office_trans/model.py b/corepos/db/office_trans/model.py index 20c244c..d484b4b 100644 --- a/corepos/db/office_trans/model.py +++ b/corepos/db/office_trans/model.py @@ -31,6 +31,41 @@ from sqlalchemy import orm Base = orm.declarative_base() +# TODO: not sure what primary key should be for this? am trying a +# composite one so far, we'll see... +class StockPurchase(Base): + """ + Represents a member equity payment. + """ + __tablename__ = 'stockpurchases' + + card_number = sa.Column('card_no', sa.Integer(), nullable=False, primary_key=True, autoincrement=False) + + amount = sa.Column('stockPurchase', sa.Numeric(precision=10, scale=2), nullable=True) + + datetime = sa.Column('tdate', sa.DateTime(), nullable=True, primary_key=True) + + transaction_number = sa.Column('trans_num', sa.String(length=50), nullable=True, primary_key=True) + + transaction_id = sa.Column('trans_id', sa.Integer(), nullable=True) + + department_number = sa.Column('dept', sa.Integer(), nullable=True) + + def __str__(self): + return f"#{self.card_number} for ${self.amount}" + + +class EquityLiveBalance(Base): + + __tablename__ = 'equity_live_balance' + + member_number = sa.Column('memnum', sa.Integer(), nullable=False, primary_key=True, autoincrement=False) + + payments = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True) + + start_date = sa.Column('startdate', sa.DateTime(), nullable=True) + + class TransactionDetailBase(object): """ Represents a POS transaction detail record. From 5921a18f12b9dd3c43e3957aa3561d6a095e5c6b Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 2 Sep 2023 13:56:59 -0500 Subject: [PATCH 082/136] Update changelog --- CHANGELOG.md | 4 ++++ corepos/_version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58781cd..7e949e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.1.13] - 2023-09-02 +### Changed +- Add models for StockPurchase and EquityLiveBalance. + ## [0.1.12] - 2023-06-12 ### Changed - Add `get_member_types()` method for CORE API. diff --git a/corepos/_version.py b/corepos/_version.py index 002a140..ca4de46 100644 --- a/corepos/_version.py +++ b/corepos/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.12' +__version__ = '0.1.13' From 0e3030394796b39117a2c5d9671188b9bc3db6ad Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 7 Sep 2023 17:45:14 -0500 Subject: [PATCH 083/136] Tweak primary key for StockPurchase model per having a better understanding now, i think.. --- corepos/db/office_trans/model.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/corepos/db/office_trans/model.py b/corepos/db/office_trans/model.py index d484b4b..b203240 100644 --- a/corepos/db/office_trans/model.py +++ b/corepos/db/office_trans/model.py @@ -32,7 +32,8 @@ Base = orm.declarative_base() # TODO: not sure what primary key should be for this? am trying a -# composite one so far, we'll see... +# composite one so far, we'll see...cf. also andy's comments in +# https://github.com/CORE-POS/IS4C/pull/1189#issuecomment-1597481138 class StockPurchase(Base): """ Represents a member equity payment. @@ -43,13 +44,13 @@ class StockPurchase(Base): amount = sa.Column('stockPurchase', sa.Numeric(precision=10, scale=2), nullable=True) - datetime = sa.Column('tdate', sa.DateTime(), nullable=True, primary_key=True) + datetime = sa.Column('tdate', sa.DateTime(), nullable=True, primary_key=True, autoincrement=False) transaction_number = sa.Column('trans_num', sa.String(length=50), nullable=True, primary_key=True) transaction_id = sa.Column('trans_id', sa.Integer(), nullable=True) - department_number = sa.Column('dept', sa.Integer(), nullable=True) + department_number = sa.Column('dept', sa.Integer(), nullable=True, primary_key=True, autoincrement=False) def __str__(self): return f"#{self.card_number} for ${self.amount}" From 0dda359094d11627b27807eb88a923f0f2b24e3a Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 7 Sep 2023 18:37:48 -0500 Subject: [PATCH 084/136] Update changelog --- CHANGELOG.md | 4 ++++ corepos/_version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e949e4..9ef75b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.1.14] - 2023-09-07 +### Changed +- Tweak primary key for StockPurchase model. + ## [0.1.13] - 2023-09-02 ### Changed - Add models for StockPurchase and EquityLiveBalance. diff --git a/corepos/_version.py b/corepos/_version.py index ca4de46..19bf267 100644 --- a/corepos/_version.py +++ b/corepos/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.13' +__version__ = '0.1.14' From 5214db0a83208e37bd85c81e635807fbe6489b36 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 13 Sep 2023 16:23:33 -0500 Subject: [PATCH 085/136] Add model for `CustomerNotifications` table --- corepos/db/office_op/model.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 7328525..68d69d9 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -1450,6 +1450,26 @@ class MemberNote(Base): return self.note or "" +class CustomerNotification(Base): + """ + Represents a customer notification for display at the lane. + + https://github.com/CORE-POS/IS4C/blob/master/fannie/classlib2.0/data/models/op/CustomerNotificationsModel.php + """ + __tablename__ = 'CustomerNotifications' + + id = sa.Column('customerNotificationID', sa.Integer(), primary_key=True, autoincrement=True, nullable=False) + card_number = sa.Column('cardNo', sa.Integer(), nullable=True) + customer_id = sa.Column('customerID', sa.Integer(), nullable=True) + source = sa.Column(sa.String(length=50), nullable=True) + type = sa.Column(sa.String(length=50), nullable=True) + message = sa.Column(sa.String(length=255), nullable=True) + modifier_module = sa.Column('modifierModule', sa.String(length=50), nullable=True) + + def __str__(self): + return self.message or '' + + class ReasonCode(Base): """ Reason codes for legacy account suspensions. From 541adb69794a8d4eb92f4d8431ae8b314eb62ba8 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 13 Sep 2023 21:51:10 -0500 Subject: [PATCH 086/136] Update changelog --- CHANGELOG.md | 4 ++++ corepos/_version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ef75b6..4faaeb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.1.15] - 2023-09-13 +### Changed +- Add model for `CustomerNotifications` table. + ## [0.1.14] - 2023-09-07 ### Changed - Tweak primary key for StockPurchase model. diff --git a/corepos/_version.py b/corepos/_version.py index 19bf267..f877d42 100644 --- a/corepos/_version.py +++ b/corepos/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.14' +__version__ = '0.1.15' From 83eecad28d68871abbcc09ed224b78f3998f747a Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 15 Sep 2023 12:43:02 -0500 Subject: [PATCH 087/136] Add model for `office_op.Tender` --- corepos/db/office_op/model.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 68d69d9..f2a6884 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -1560,6 +1560,28 @@ class HouseCoupon(Base): return self.description or '' +class Tender(Base): + """ + Represents a tender for payment at POS + """ + __tablename__ = 'tenders' + + tender_id = sa.Column('TenderID', sa.Integer(), primary_key=True, nullable=False) + + tender_code = sa.Column('TenderCode', sa.String(length=2), nullable=True) + tender_name = sa.Column('TenderName', sa.String(length=25), nullable=True) + tender_type = sa.Column('TenderType', sa.String(length=2), nullable=True) + change_message = sa.Column('ChangeMessage', sa.String(length=25), nullable=True) + min_amount = sa.Column('MinAmount', sa.Numeric(precision=10, scale=2), nullable=True) + max_amount = sa.Column('MaxAmount', sa.Numeric(precision=10, scale=2), nullable=True) + max_refund = sa.Column('MaxRefund', sa.Numeric(precision=10, scale=2), nullable=True) + tender_module = sa.Column('TenderModule', sa.String(length=50), nullable=True) + sales_code = sa.Column('SalesCode', sa.Integer(), nullable=True) + + def __str__(self): + return self.tender_name or '' + + class BatchType(Base): """ Represents the definition of a batch type. From f06f236a604bb10a1e8d9f92f676647d19532b5b Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 15 Sep 2023 12:59:52 -0500 Subject: [PATCH 088/136] Update changelog --- CHANGELOG.md | 4 ++++ corepos/_version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4faaeb5..56b70c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.1.16] - 2023-09-15 +### Changed +- Add model for `office_op.Tender`. + ## [0.1.15] - 2023-09-13 ### Changed - Add model for `CustomerNotifications` table. diff --git a/corepos/_version.py b/corepos/_version.py index f877d42..05c3021 100644 --- a/corepos/_version.py +++ b/corepos/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.15' +__version__ = '0.1.16' From e5988102ad13c03f5d516dca1245b3959fc852ef Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 5 Oct 2023 11:52:39 -0500 Subject: [PATCH 089/136] Rename module to `corepos.db.office_arch` --- corepos/db/office_arch/__init__.py | 30 ++++++++++++++++ corepos/db/office_arch/model.py | 39 +++++++++++++++++++++ corepos/db/office_trans_archive/__init__.py | 10 +++--- corepos/db/office_trans_archive/model.py | 17 +++------ 4 files changed, 80 insertions(+), 16 deletions(-) create mode 100644 corepos/db/office_arch/__init__.py create mode 100644 corepos/db/office_arch/model.py diff --git a/corepos/db/office_arch/__init__.py b/corepos/db/office_arch/__init__.py new file mode 100644 index 0000000..70292e9 --- /dev/null +++ b/corepos/db/office_arch/__init__.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# pyCOREPOS -- Python Interface to CORE POS +# Copyright © 2018-2023 Lance Edgar +# +# This file is part of pyCOREPOS. +# +# pyCOREPOS is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# pyCOREPOS is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# pyCOREPOS. If not, see . +# +################################################################################ +""" +"Archive" Transaction Database Interface +""" + +from sqlalchemy import orm + + +Session = orm.sessionmaker() diff --git a/corepos/db/office_arch/model.py b/corepos/db/office_arch/model.py new file mode 100644 index 0000000..bf01a4d --- /dev/null +++ b/corepos/db/office_arch/model.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# pyCOREPOS -- Python Interface to CORE POS +# Copyright © 2018-2023 Lance Edgar +# +# This file is part of pyCOREPOS. +# +# pyCOREPOS is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# pyCOREPOS is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# pyCOREPOS. If not, see . +# +################################################################################ +""" +CORE Office "arch" data model +""" + +from sqlalchemy import orm + +from corepos.db.office_trans.model import TransactionDetailBase + + +Base = orm.declarative_base() + + +class TransactionDetail(TransactionDetailBase, Base): + """ + Represents a POS transaction detail record. + """ + __tablename__ = 'bigArchive' diff --git a/corepos/db/office_trans_archive/__init__.py b/corepos/db/office_trans_archive/__init__.py index e25b200..6ddb31d 100644 --- a/corepos/db/office_trans_archive/__init__.py +++ b/corepos/db/office_trans_archive/__init__.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2022 Lance Edgar +# Copyright © 2018-2023 Lance Edgar # # This file is part of pyCOREPOS. # @@ -24,7 +24,9 @@ "Archive" Transaction Database Interface """ -from sqlalchemy import orm +import warnings +warnings.warn("The `corepos.db.office_trans_archive` module is deprecated! " + "Please use `corepos.db.office_arch` instead.", + DeprecationWarning, stacklevel=2) - -Session = orm.sessionmaker() +from corepos.db.office_arch import * diff --git a/corepos/db/office_trans_archive/model.py b/corepos/db/office_trans_archive/model.py index 8f4a079..e57d071 100644 --- a/corepos/db/office_trans_archive/model.py +++ b/corepos/db/office_trans_archive/model.py @@ -24,16 +24,9 @@ CORE POS Transaction Data Model """ -from sqlalchemy import orm +import warnings +warnings.warn("The `corepos.db.office_trans_archive.model` module is deprecated! " + "Please use `corepos.db.office_arch.model` instead.", + DeprecationWarning, stacklevel=2) -from corepos.db.office_trans.model import TransactionDetailBase - - -Base = orm.declarative_base() - - -class TransactionDetail(TransactionDetailBase, Base): - """ - Represents a POS transaction detail record. - """ - __tablename__ = 'bigArchive' +from corepos.db.office_arch.model import * From 7171531dce6cd465c9a14f29f2437c829c21021d Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 7 Oct 2023 18:58:17 -0500 Subject: [PATCH 090/136] Update changelog --- CHANGELOG.md | 4 ++++ corepos/_version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56b70c3..247807d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.1.17] - 2023-10-07 +### Changed +- Rename module to `corepos.db.office_arch`. + ## [0.1.16] - 2023-09-15 ### Changed - Add model for `office_op.Tender`. diff --git a/corepos/_version.py b/corepos/_version.py index 05c3021..9a17824 100644 --- a/corepos/_version.py +++ b/corepos/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.16' +__version__ = '0.1.17' From 3a3fba19e461a9acbf75410c25bb333494674c2d Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 11 Oct 2023 18:36:12 -0500 Subject: [PATCH 091/136] Fix the `Department.tax_rate` relationship whoops i mistook `dept_tax` for a boolean previously --- corepos/db/office_op/model.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index f2a6884..c99f6a8 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -225,12 +225,18 @@ class Department(Base): Represents a department within the organization. """ __tablename__ = 'departments' + __table_args__ = ( + sa.ForeignKeyConstraint(['dept_tax'], ['taxrates.id']), + ) number = sa.Column('dept_no', sa.SmallInteger(), primary_key=True, autoincrement=False, nullable=False) name = sa.Column('dept_name', sa.String(length=30), nullable=True) - tax = sa.Column('dept_tax', sa.Boolean(), nullable=True) + tax_rate_id = sa.Column('dept_tax', sa.SmallInteger(), nullable=True) + tax_rate = orm.relationship('TaxRate') + # TODO: deprecate / remove this + tax = orm.synonym('tax_rate_id') food_stampable = sa.Column('dept_fs', sa.Boolean(), nullable=True) From 1597c163c6f40dfd97887b8e78560186f4e16cb0 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 12 Oct 2023 10:34:13 -0500 Subject: [PATCH 092/136] Let `MemberInfo.dates` be an object, not a list --- corepos/db/office_op/model.py | 1 + 1 file changed, 1 insertion(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index c99f6a8..f6787bf 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -1292,6 +1292,7 @@ class MemberInfo(Base): primaryjoin='MemberDate.card_number == MemberInfo.card_number', foreign_keys='MemberDate.card_number', cascade='all, delete-orphan', + uselist=False, doc=""" List of date records for the member. """, From 67dd9777ba827806cf95afd5bb47a7ce05923e70 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 12 Oct 2023 10:38:44 -0500 Subject: [PATCH 093/136] Update changelog --- CHANGELOG.md | 5 +++++ corepos/_version.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 247807d..8e0fbd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.1.18] - 2023-10-12 +### Changed +- Fix the `Department.tax_rate` relationship. +- Let `MemberInfo.dates` be an object, not a list. + ## [0.1.17] - 2023-10-07 ### Changed - Rename module to `corepos.db.office_arch`. diff --git a/corepos/_version.py b/corepos/_version.py index 9a17824..a6dbf47 100644 --- a/corepos/_version.py +++ b/corepos/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.17' +__version__ = '0.1.18' From f217f00a8f409cc1a29ab965da3d79f383a4da43 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 20 Oct 2023 14:31:13 -0500 Subject: [PATCH 094/136] Fix data types for tax, voided in `dtransactions` --- corepos/db/office_trans/model.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/corepos/db/office_trans/model.py b/corepos/db/office_trans/model.py index b203240..ce5304b 100644 --- a/corepos/db/office_trans/model.py +++ b/corepos/db/office_trans/model.py @@ -120,7 +120,8 @@ class TransactionDetailBase(object): reg_price = sa.Column('regPrice', sa.Numeric(precision=10, scale=2), nullable=True) - tax = sa.Column(sa.Boolean(), nullable=True) + tax = sa.Column(sa.SmallInteger(), nullable=True) + tax_rate_id = orm.synonym('tax') food_stamp = sa.Column('foodstamp', sa.Boolean(), nullable=True) @@ -132,7 +133,7 @@ class TransactionDetailBase(object): discount_type = sa.Column('discounttype', sa.Integer(), nullable=True) - voided = sa.Column(sa.Boolean(), nullable=True) + voided = sa.Column(sa.Integer(), nullable=True) percent_discount = sa.Column('percentDiscount', sa.Integer(), nullable=True) From 909d75796b40b7b1e527ef621c733799aeb9f9d4 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 20 Oct 2023 19:03:20 -0500 Subject: [PATCH 095/136] Fix synonym for `dtransactions.tax` --- corepos/db/office_trans/model.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/corepos/db/office_trans/model.py b/corepos/db/office_trans/model.py index ce5304b..2848af5 100644 --- a/corepos/db/office_trans/model.py +++ b/corepos/db/office_trans/model.py @@ -26,6 +26,7 @@ CORE POS Transaction Data Model import sqlalchemy as sa from sqlalchemy import orm +from sqlalchemy.ext.declarative import declared_attr Base = orm.declarative_base() @@ -121,7 +122,10 @@ class TransactionDetailBase(object): reg_price = sa.Column('regPrice', sa.Numeric(precision=10, scale=2), nullable=True) tax = sa.Column(sa.SmallInteger(), nullable=True) - tax_rate_id = orm.synonym('tax') + + @declared_attr + def tax_rate_id(self): + return orm.synonym('tax') food_stamp = sa.Column('foodstamp', sa.Boolean(), nullable=True) From 7cd89029ac1d5461d026f1d4e70ecb498a26aa6f Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 1 Nov 2023 08:15:37 -0500 Subject: [PATCH 096/136] Update changelog --- CHANGELOG.md | 5 +++++ corepos/_version.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e0fbd0..a52aee7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.1.19] - 2023-11-01 +### Changed +- Fix data types for tax, voided in `dtransactions`. +- Fix synonym for `dtransactions.tax`. + ## [0.1.18] - 2023-10-12 ### Changed - Fix the `Department.tax_rate` relationship. diff --git a/corepos/_version.py b/corepos/_version.py index a6dbf47..560cfb5 100644 --- a/corepos/_version.py +++ b/corepos/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.18' +__version__ = '0.1.19' From df6f0d97937272b4ae6f07eabba6262b4b0367b8 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 16 May 2024 19:13:35 -0500 Subject: [PATCH 097/136] Add enum for CORE (Office) DB types for use with typer commands --- corepos/enum.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/corepos/enum.py b/corepos/enum.py index 55619d8..28e33ea 100644 --- a/corepos/enum.py +++ b/corepos/enum.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2019 Lance Edgar +# Copyright © 2018-2024 Lance Edgar # # This file is part of pyCOREPOS. # @@ -24,12 +24,14 @@ CORE POS enumeration constants """ -from __future__ import unicode_literals, absolute_import +from collections import OrderedDict +from enum import Enum -try: - from collections import OrderedDict -except ImportError: - from ordereddict import OrderedDict + +class CoreDbType(str, Enum): + office_op = 'office_op' + office_trans = 'office_trans' + office_arch = 'office_arch' BATCH_DISCOUNT_TYPE_PRICE_CHANGE = 0 From 777b198e4441aa3ca88b92453a1b445012bf2441 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 29 May 2024 10:09:15 -0500 Subject: [PATCH 098/136] Update changelog --- CHANGELOG.md | 4 ++++ corepos/_version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a52aee7..f9feb94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.1.20] - 2024-05-29 +### Changed +- Add enum for CORE (Office) DB types. + ## [0.1.19] - 2023-11-01 ### Changed - Fix data types for tax, voided in `dtransactions`. diff --git a/corepos/_version.py b/corepos/_version.py index 560cfb5..0275ddd 100644 --- a/corepos/_version.py +++ b/corepos/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.19' +__version__ = '0.1.20' From f80d03daaa301f2b98a482945e0bfa6726dbe571 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 29 May 2024 10:10:25 -0500 Subject: [PATCH 099/136] Fix default dist filename for release task not sure why this fix was needed, did setuptools behavior change? --- tasks.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tasks.py b/tasks.py index a9b9fc3..9156d43 100644 --- a/tasks.py +++ b/tasks.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2020 Lance Edgar +# Copyright © 2018-2024 Lance Edgar # # This file is part of pyCOREPOS. # @@ -35,10 +35,10 @@ exec(open(os.path.join(here, 'corepos', '_version.py')).read()) @task -def release(ctx): +def release(c): """ Release a new version of 'pyCOREPOS'. """ shutil.rmtree('pyCOREPOS.egg-info') - ctx.run('python setup.py sdist --formats=gztar') - ctx.run('twine upload dist/pyCOREPOS-{}.tar.gz'.format(__version__)) + c.run('python setup.py sdist --formats=gztar') + c.run('twine upload dist/pycorepos-{}.tar.gz'.format(__version__)) From 5cf8d2ac05917f73001f9cdfd32fcfc74305cd56 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 10 Jun 2024 15:55:14 -0500 Subject: [PATCH 100/136] feat: switch from setup.cfg to pyproject.toml + hatchling --- .gitignore | 3 +++ corepos/_version.py | 5 ++++- pyproject.toml | 50 +++++++++++++++++++++++++++++++++++++++++++++ setup.cfg | 33 ------------------------------ setup.py | 26 ----------------------- tasks.py | 13 +++++++++++- 6 files changed, 69 insertions(+), 61 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.gitignore b/.gitignore index 9a52e5b..07ddefb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ +*~ +*.pyc +dist/ pyCOREPOS.egg-info/ diff --git a/corepos/_version.py b/corepos/_version.py index 0275ddd..555fee3 100644 --- a/corepos/_version.py +++ b/corepos/_version.py @@ -1,3 +1,6 @@ # -*- coding: utf-8; -*- -__version__ = '0.1.20' +from importlib.metadata import version + + +__version__ = version('pyCOREPOS') diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4701f62 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,50 @@ + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + + +[project] +name = "pyCOREPOS" +version = "0.1.20" +description = "Python Interface to CORE POS" +readme = "README.rst" +authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] +license = {text = "GNU GPL v3+"} +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Topic :: Office/Business", + "Topic :: Software Development :: Libraries :: Python Modules", +] + +dependencies = [ + "mysql-connector-python", + "requests", + "six", + "SQLAlchemy>=1.4", +] + + +[project.urls] +Homepage = "https://rattailproject.org" +Repository = "https://kallithea.rattailproject.org/rattail-project/pycorepos" +Issues = "https://redmine.rattailproject.org/projects/corepos-integration/issues" +Changelog = "https://kallithea.rattailproject.org/rattail-project/pycorepos/files/master/CHANGELOG.md" + + +[tool.commitizen] +version_provider = "pep621" +tag_format = "v$version" +update_changelog_on_bump = true + + +[tool.hatch.build.targets.wheel] +packages = ["corepos"] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 728f179..0000000 --- a/setup.cfg +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8; -*- - -[metadata] -name = pyCOREPOS -version = attr: corepos.__version__ -author = Lance Edgar -author_email = lance@edbob.org -url = https://rattailproject.org/ -license = GNU GPL v3 -description = Python Interface to CORE POS -long_description = file: README.rst -classifiers = - Development Status :: 3 - Alpha - Environment :: Console - Environment :: Web Environment - Intended Audience :: Developers - License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) - Natural Language :: English - Operating System :: OS Independent - Programming Language :: Python - Programming Language :: Python :: 3 - Topic :: Office/Business - Topic :: Software Development :: Libraries :: Python Modules - - -[options] -install_requires = - mysql-connector-python - requests - six - SQLAlchemy>=1.4 - -packages = find: diff --git a/setup.py b/setup.py deleted file mode 100644 index a02ef90..0000000 --- a/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2023 Lance Edgar -# -# This file is part of pyCOREPOS. -# -# pyCOREPOS is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) -# any later version. -# -# pyCOREPOS is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# pyCOREPOS. If not, see . -# -################################################################################ - -from setuptools import setup - -setup() diff --git a/tasks.py b/tasks.py index 9156d43..caf7777 100644 --- a/tasks.py +++ b/tasks.py @@ -25,13 +25,24 @@ Tasks for 'pyCOREPOS' package """ import os +import re import shutil from invoke import task here = os.path.abspath(os.path.dirname(__file__)) -exec(open(os.path.join(here, 'corepos', '_version.py')).read()) +__version__ = None +pattern = re.compile(r'^version = "(\d+\.\d+\.\d+)"$') +with open(os.path.join(here, 'pyproject.toml'), 'rt') as f: + for line in f: + line = line.rstrip('\n') + match = pattern.match(line) + if match: + __version__ = match.group(1) + break +if not __version__: + raise RuntimeError("could not parse version!") @task From 04948fb840e19d31a16d144a00256cf6c9229221 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 10 Jun 2024 15:55:51 -0500 Subject: [PATCH 101/136] =?UTF-8?q?bump:=20version=200.1.20=20=E2=86=92=20?= =?UTF-8?q?0.2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9feb94..d40d286 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.2.0 (2024-06-10) + +### Feat + +- switch from setup.cfg to pyproject.toml + hatchling + ## [0.1.20] - 2024-05-29 ### Changed - Add enum for CORE (Office) DB types. diff --git a/pyproject.toml b/pyproject.toml index 4701f62..2f3c924 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "pyCOREPOS" -version = "0.1.20" +version = "0.2.0" description = "Python Interface to CORE POS" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 40e647d5c8c8d756b29d460e37125e9dbbf7e74e Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 10 Jun 2024 15:57:09 -0500 Subject: [PATCH 102/136] build: use newer convention when building for release --- tasks.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tasks.py b/tasks.py index caf7777..6946a7f 100644 --- a/tasks.py +++ b/tasks.py @@ -50,6 +50,7 @@ def release(c): """ Release a new version of 'pyCOREPOS'. """ - shutil.rmtree('pyCOREPOS.egg-info') - c.run('python setup.py sdist --formats=gztar') + if os.path.exists('pyCOREPOS.egg-info'): + shutil.rmtree('pyCOREPOS.egg-info') + c.run('python -m build --sdist') c.run('twine upload dist/pycorepos-{}.tar.gz'.format(__version__)) From 3b54dbd0686c5afe30fe7780665f27aacd0bd5c3 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 14 Jun 2024 19:48:40 -0500 Subject: [PATCH 103/136] docs: use more specific project homepage url --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2f3c924..b5c6fb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ [project.urls] -Homepage = "https://rattailproject.org" +Homepage = "https://redmine.rattailproject.org/projects/corepos-integration" Repository = "https://kallithea.rattailproject.org/rattail-project/pycorepos" Issues = "https://redmine.rattailproject.org/projects/corepos-integration/issues" Changelog = "https://kallithea.rattailproject.org/rattail-project/pycorepos/files/master/CHANGELOG.md" From e9638c73a4c7c8ac832eeb72b1731bb422998d13 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 1 Jul 2024 16:32:39 -0500 Subject: [PATCH 104/136] fix: remove dependency for `six` package --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b5c6fb0..1e356fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,6 @@ classifiers = [ dependencies = [ "mysql-connector-python", "requests", - "six", "SQLAlchemy>=1.4", ] From 66cf108b3f1f52c3bb2ac965abd924a35250739d Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 4 Jul 2024 13:18:45 -0500 Subject: [PATCH 105/136] fix: remove `Change` data model since it isn't actually part of CORE, and is now handled in other ways with regard to datasync --- corepos/db/office_op/model.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index f6787bf..d5da2f7 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2023 Lance Edgar +# Copyright © 2018-2024 Lance Edgar # # This file is part of pyCOREPOS. # @@ -54,23 +54,6 @@ class StringableDateTime(sa.TypeDecorator): raise NotImplementedError -class Change(Base): - """ - Represents a changed (or deleted) record, which is pending synchronization - to another system(s). - - .. note:: - This table may or may not be installed to a given CORE Office Op DB. Its - presence is required if Rattail datasync needs to "watch" the DB. - """ - __tablename__ = 'datasync_changes' - - id = sa.Column(sa.Integer(), nullable=False, primary_key=True) - object_type = sa.Column(sa.String(length=255), nullable=False) - object_key = sa.Column(sa.String(length=255), nullable=False) - deleted = sa.Column(sa.Boolean(), nullable=False, default=False) - - class Parameter(Base): """ Represents a "parameter" value. From bcff5605559c64a83197c413ff754c94177ce0ee Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 4 Jul 2024 18:27:50 -0500 Subject: [PATCH 106/136] fix: add API methods, `get_employees()` and `get_employee()` --- corepos/api.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/corepos/api.py b/corepos/api.py index 97ad8c7..a24b906 100644 --- a/corepos/api.py +++ b/corepos/api.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2023 Lance Edgar +# Copyright © 2018-2024 Lance Edgar # # This file is part of pyCOREPOS. # @@ -198,6 +198,40 @@ class CoreWebAPI(object): if result: return result + def get_employees(self, **columns): + """ + Fetch some or all of Employee records from CORE. + + :returns: A (potentially empty) list of employee dict records. + """ + params = { + 'entity': 'Employees', + 'submethod': 'get', + 'columns': columns, + } + response = self.post(params) + result = self.parse_response(response) + return [json.loads(rec) for rec in result] + + def get_employee(self, emp_no, **columns): + """ + Fetch an existing Employee record from CORE. + + :returns: Either a employee dict record, or ``None``. + """ + columns['emp_no'] = emp_no + params = { + 'entity': 'Employees', + 'submethod': 'get', + 'columns': columns, + } + response = self.post(params) + result = self.parse_response(response) + if result: + if len(result) > 1: + log.warning("CORE API returned %s employee results", len(result)) + return json.loads(result[0]) + def get_stores(self, **columns): """ Fetch some or all of Store records from CORE. From 2df38854204d32b3a34c4d96e8e86b5a6fdc53bd Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 4 Jul 2024 23:59:49 -0500 Subject: [PATCH 107/136] =?UTF-8?q?bump:=20version=200.2.0=20=E2=86=92=200?= =?UTF-8?q?.2.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 8 ++++++++ pyproject.toml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d40d286..e97814c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.2.1 (2024-07-04) + +### Fix + +- add API methods, `get_employees()` and `get_employee()` +- remove `Change` data model +- remove dependency for `six` package + ## v0.2.0 (2024-06-10) ### Feat diff --git a/pyproject.toml b/pyproject.toml index 1e356fd..29dc012 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "pyCOREPOS" -version = "0.2.0" +version = "0.2.1" description = "Python Interface to CORE POS" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 27a46ed18f72cc721f65861408dc0905205368de Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 6 Aug 2024 10:38:01 -0500 Subject: [PATCH 108/136] feat: add model for `CustomReceiptLine` (`op.customReceipt`) --- corepos/db/office_op/model.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index d5da2f7..33c8313 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -1572,6 +1572,20 @@ class Tender(Base): return self.tender_name or '' +class CustomReceiptLine(Base): + """ + Represents a "text string" line for a custom receipt. + """ + __tablename__ = 'customReceipt' + + sequence = sa.Column('seq', sa.Integer(), primary_key=True, autoincrement=True, nullable=False) + type = sa.Column(sa.String(length=20), primary_key=True, autoincrement=False, nullable=False) + text = sa.Column(sa.String(length=80), nullable=True) + + def __str__(self): + return self.text or "" + + class BatchType(Base): """ Represents the definition of a batch type. From b5b29cdcf1f0a5c5c04b7b70430cf7cc37623be4 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 6 Aug 2024 11:34:07 -0500 Subject: [PATCH 109/136] feat: add model for `MemberContactPreference` (`op.memContactPrefs`) --- corepos/db/office_op/model.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 33c8313..5f6fc3d 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -1378,7 +1378,7 @@ class MemberDate(Base): class MemberContact(Base): """ - Contact preferences for members + Member contacts """ __tablename__ = 'memContact' @@ -1405,6 +1405,19 @@ class MemberContact(Base): return str(self.preference) +class MemberContactPreference(Base): + """ + Member contact preferences + """ + __tablename__ = 'memContactPrefs' + + id = sa.Column('pref_id', sa.Integer(), primary_key=True, autoincrement=False, nullable=False) + description = sa.Column('pref_description', sa.String(length=50), nullable=True) + + def __str__(self): + return self.description or "" + + class MemberBarcode(Base): """ Additional barcode for a member. From b425095a8da82f065e0c7c25de24f67c3af0b2b4 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 6 Aug 2024 23:21:26 -0500 Subject: [PATCH 110/136] =?UTF-8?q?bump:=20version=200.2.1=20=E2=86=92=200?= =?UTF-8?q?.3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 7 +++++++ pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e97814c..f6e7048 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.3.0 (2024-08-06) + +### Feat + +- add model for `MemberContactPreference` (`op.memContactPrefs`) +- add model for `CustomReceiptLine` (`op.customReceipt`) + ## v0.2.1 (2024-07-04) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 29dc012..58ee2a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "pyCOREPOS" -version = "0.2.1" +version = "0.3.0" description = "Python Interface to CORE POS" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From b44e5a3d6062fbbc1a7ee009022c3e380f918c8f Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 13 Sep 2024 18:49:14 -0500 Subject: [PATCH 111/136] docs: use markdown for readme file --- README.md | 5 +++++ README.rst | 7 ------- pyproject.toml | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) create mode 100644 README.md delete mode 100644 README.rst diff --git a/README.md b/README.md new file mode 100644 index 0000000..dd69797 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ + +# pyCOREPOS + +A Python interface to the [CORE POS](https://github.com/CORE-POS) +system. diff --git a/README.rst b/README.rst deleted file mode 100644 index 0b34d2f..0000000 --- a/README.rst +++ /dev/null @@ -1,7 +0,0 @@ - -pyCOREPOS -========= - -A Python interface to the `CORE POS`_ system. - -.. _CORE POS: https://github.com/CORE-POS diff --git a/pyproject.toml b/pyproject.toml index 58ee2a2..de0f54b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ build-backend = "hatchling.build" name = "pyCOREPOS" version = "0.3.0" description = "Python Interface to CORE POS" -readme = "README.rst" +readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] license = {text = "GNU GPL v3+"} classifiers = [ From ed48f9134a841756a455143adccc269b807f5592 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 14 Sep 2024 10:43:39 -0500 Subject: [PATCH 112/136] docs: update project links to forgejo --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index de0f54b..869d6fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,10 +33,10 @@ dependencies = [ [project.urls] -Homepage = "https://redmine.rattailproject.org/projects/corepos-integration" -Repository = "https://kallithea.rattailproject.org/rattail-project/pycorepos" -Issues = "https://redmine.rattailproject.org/projects/corepos-integration/issues" -Changelog = "https://kallithea.rattailproject.org/rattail-project/pycorepos/files/master/CHANGELOG.md" +Homepage = "https://forgejo.wuttaproject.org/rattail/pycorepos" +Repository = "https://forgejo.wuttaproject.org/rattail/pycorepos" +Issues = "https://forgejo.wuttaproject.org/rattail/pycorepos/issues" +Changelog = "https://forgejo.wuttaproject.org/rattail/pycorepos/src/branch/master/CHANGELOG.md" [tool.commitizen] From 3bb01f23979db35065276d91b0bc058d113d7c19 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 17 Dec 2024 16:45:11 -0600 Subject: [PATCH 113/136] fix: add `wicable`, `active` columns for Department model --- corepos/db/office_op/model.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 5f6fc3d..8590375 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -223,6 +223,10 @@ class Department(Base): food_stampable = sa.Column('dept_fs', sa.Boolean(), nullable=True) + wicable = sa.Column('dept_wicable', sa.SmallInteger(), nullable=True) + + active = sa.Column(sa.Boolean(), default=True) + limit = sa.Column('dept_limit', sa.Float(), nullable=True) minimum = sa.Column('dept_minimum', sa.Float(), nullable=True) From 310a261b48d93701199b6326aec4be3d277f3809 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 17 Dec 2024 16:45:36 -0600 Subject: [PATCH 114/136] =?UTF-8?q?bump:=20version=200.3.0=20=E2=86=92=200?= =?UTF-8?q?.3.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6e7048..6c20cbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.3.1 (2024-12-17) + +### Fix + +- add `wicable`, `active` columns for Department model + ## v0.3.0 (2024-08-06) ### Feat diff --git a/pyproject.toml b/pyproject.toml index 869d6fb..0bbc23a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "pyCOREPOS" -version = "0.3.0" +version = "0.3.1" description = "Python Interface to CORE POS" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From cb6ed15eb8c162a9aa9a4c4644a8105a6d8fdc6b Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 11 Jan 2025 21:51:28 -0600 Subject: [PATCH 115/136] fix: add model for `MasterSuperDepartment` --- corepos/db/office_op/model.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 8590375..12688af 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -169,6 +169,25 @@ class Store(Base): return self.description or "" +class MasterSuperDepartment(Base): + """ + A department may belong to more than one superdepartment, but has + one "master" superdepartment. This avoids duplicating rows in + some reports. By convention, a department's "master" + superdepartment is the one with the lowest superID. + """ + __tablename__ = 'MasterSuperDepts' + + super_id = sa.Column('superID', sa.Integer(), primary_key=True, autoincrement=False, nullable=False) + + department_id = sa.Column('dept_ID', sa.Integer(), primary_key=True, autoincrement=False, nullable=False) + + super_name = sa.Column(sa.String(length=50), nullable=True) + + def __str__(self): + return self.super_name or "" + + class SuperDepartment(Base): """ Represents a "super" (parent/child) department mapping. From 464f107a88ed996dc5d627206f55a3d41fa453cf Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 11 Jan 2025 21:52:22 -0600 Subject: [PATCH 116/136] fix: add `MemberType.ignore_sales` column --- corepos/db/office_op/model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 12688af..5e9a34a 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -1037,6 +1037,9 @@ class MemberType(Base): ssi = sa.Column(sa.Boolean(), nullable=True) + ignoreSales = sa.Column(sa.Boolean(), nullable=True, default=False) + ignore_sales = orm.synonym('ignoreSales') + # TODO: this was apparently added "recently" - isn't present in all DBs # (need to figure out how to conditionally include it in model?) # sales_code = sa.Column('salesCode', sa.Integer(), nullable=True) From 47c23a65aee63fa4018d3b66d6610a837e5aea8a Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 11 Jan 2025 21:55:54 -0600 Subject: [PATCH 117/136] fix: add base class for all transaction tables, views also rename some classes; add `dlogBig` for archive view --- corepos/db/office_arch/model.py | 19 +++++++++++++---- corepos/db/office_trans/model.py | 36 +++++++++++++++++++++++++------- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/corepos/db/office_arch/model.py b/corepos/db/office_arch/model.py index bf01a4d..49b0bbd 100644 --- a/corepos/db/office_arch/model.py +++ b/corepos/db/office_arch/model.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2023 Lance Edgar +# Copyright © 2018-2024 Lance Edgar # # This file is part of pyCOREPOS. # @@ -26,14 +26,25 @@ CORE Office "arch" data model from sqlalchemy import orm -from corepos.db.office_trans.model import TransactionDetailBase +from corepos.db.office_trans.model import DTransactionBase, DLogBase Base = orm.declarative_base() -class TransactionDetail(TransactionDetailBase, Base): +class BigArchive(DTransactionBase, Base): """ - Represents a POS transaction detail record. + Represents a record from ``bigArchive`` table. """ __tablename__ = 'bigArchive' + + +# TODO: deprecate / remove this +TransactionDetail = BigArchive + + +class DLogBig(DLogBase, Base): + """ + Represents a record from ``dlogBig`` view. + """ + __tablename__ = 'dlogBig' diff --git a/corepos/db/office_trans/model.py b/corepos/db/office_trans/model.py index 2848af5..b4659cd 100644 --- a/corepos/db/office_trans/model.py +++ b/corepos/db/office_trans/model.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2023 Lance Edgar +# Copyright © 2018-2024 Lance Edgar # # This file is part of pyCOREPOS. # @@ -68,7 +68,7 @@ class EquityLiveBalance(Base): start_date = sa.Column('startdate', sa.DateTime(), nullable=True) -class TransactionDetailBase(object): +class TransactionDetailBase: """ Represents a POS transaction detail record. """ @@ -86,10 +86,12 @@ class TransactionDetailBase(object): transaction_number = sa.Column('trans_no', sa.Integer(), nullable=True) transaction_type = sa.Column('trans_type', sa.String(length=1), nullable=True) transaction_subtype = sa.Column('trans_subtype', sa.String(length=2), nullable=True) - transaction_status = sa.Column('trans_status', sa.String(length=1), nullable=True) - # timestamps - date_time = sa.Column('datetime', sa.DateTime(), nullable=True) + trans_status = sa.Column(sa.String(length=1), nullable=True) + + @declared_attr + def transaction_status(self): + return orm.synonym('trans_status') # cashier employee_number = sa.Column('emp_no', sa.Integer(), nullable=True) @@ -115,7 +117,11 @@ class TransactionDetailBase(object): cost = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True) - unit_price = sa.Column('unitPrice', sa.Numeric(precision=10, scale=2), nullable=True) + unitPrice = sa.Column('unitPrice', sa.Numeric(precision=10, scale=2), nullable=True) + + @declared_attr + def unit_price(self): + return orm.synonym('unitPrice') total = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True) @@ -161,8 +167,22 @@ class TransactionDetailBase(object): return self.description or '' -class TransactionDetail(TransactionDetailBase, Base): +class DTransactionBase(TransactionDetailBase): + + date_time = sa.Column('datetime', sa.DateTime(), nullable=True) + + +class DLogBase(TransactionDetailBase): + + date_time = sa.Column('tdate', sa.DateTime(), nullable=True) + + +class DTransaction(DTransactionBase, Base): """ - Represents a POS transaction detail record. + Represents a record from ``dtransactions`` table. """ __tablename__ = 'dtransactions' + + +# TODO: deprecate / remove this +TransactionDetail = DTransaction From 465e565f7b975219647dfbe548187584cdd74192 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 11 Jan 2025 22:01:38 -0600 Subject: [PATCH 118/136] =?UTF-8?q?bump:=20version=200.3.1=20=E2=86=92=200?= =?UTF-8?q?.3.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 8 ++++++++ pyproject.toml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c20cbc..ad1845a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.3.2 (2025-01-11) + +### Fix + +- add base class for all transaction tables, views +- add `MemberType.ignore_sales` column +- add model for `MasterSuperDepartment` + ## v0.3.1 (2024-12-17) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 0bbc23a..80bf76b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "pyCOREPOS" -version = "0.3.1" +version = "0.3.2" description = "Python Interface to CORE POS" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 023d826d316ccd87035a8fe1384a662712224894 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 13 Jan 2025 12:57:41 -0600 Subject: [PATCH 119/136] fix: remove `autoincrement` option for composite PK fields --- corepos/db/office_op/model.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 5e9a34a..16bbc57 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -80,7 +80,9 @@ class TableSyncRule(Base): """ __tablename__ = 'TableSyncRules' - id = sa.Column('tableSyncRuleID', sa.Integer(), nullable=False, primary_key=True, autoincrement=True) + # nb. this should be autoincrement, but we can't do that + # automatically via sqlalchemy when PK is composite + id = sa.Column('tableSyncRuleID', sa.Integer(), nullable=False, primary_key=True) table_name = sa.Column('tableName', sa.String(length=255), nullable=False, primary_key=True) @@ -1617,8 +1619,11 @@ class CustomReceiptLine(Base): """ __tablename__ = 'customReceipt' - sequence = sa.Column('seq', sa.Integer(), primary_key=True, autoincrement=True, nullable=False) - type = sa.Column(sa.String(length=20), primary_key=True, autoincrement=False, nullable=False) + # nb. this should be autoincrement, but we can't do that + # automatically via sqlalchemy when PK is composite + sequence = sa.Column('seq', sa.Integer(), primary_key=True, nullable=False) + + type = sa.Column(sa.String(length=20), primary_key=True, nullable=False) text = sa.Column(sa.String(length=80), nullable=True) def __str__(self): From e37cf88cd91ace61ba27b0bdea53bdfca4488d38 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 13 Jan 2025 13:31:21 -0600 Subject: [PATCH 120/136] =?UTF-8?q?bump:=20version=200.3.2=20=E2=86=92=200?= =?UTF-8?q?.3.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad1845a..ccc62cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.3.3 (2025-01-13) + +### Fix + +- remove `autoincrement` option for composite PK fields + ## v0.3.2 (2025-01-11) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 80bf76b..a59e830 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "pyCOREPOS" -version = "0.3.2" +version = "0.3.3" description = "Python Interface to CORE POS" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From ab56a35acc3c08ee6d9992dee59086d8654c987c Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 14 Jan 2025 20:20:41 -0600 Subject: [PATCH 121/136] fix: add more enums for batch discount type, editor UI also fix column type for editor UI --- corepos/db/office_op/model.py | 2 +- corepos/enum.py | 23 +++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 16bbc57..68627c0 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -1646,7 +1646,7 @@ class BatchType(Base): special_order_eligible = sa.Column('specialOrderEligible', sa.Boolean(), nullable=True, default=True) - editor_ui = sa.Column('editorUI', sa.Boolean(), nullable=True, default=True) + editor_ui = sa.Column('editorUI', sa.SmallInteger(), nullable=True, default=True) allow_single_store = sa.Column('allowSingleStore', sa.Boolean(), nullable=True, default=False) diff --git a/corepos/enum.py b/corepos/enum.py index 28e33ea..93780c3 100644 --- a/corepos/enum.py +++ b/corepos/enum.py @@ -34,14 +34,33 @@ class CoreDbType(str, Enum): office_arch = 'office_arch' +BATCH_DISCOUNT_TYPE_TRACKING = -1 BATCH_DISCOUNT_TYPE_PRICE_CHANGE = 0 BATCH_DISCOUNT_TYPE_SALE_EVERYONE = 1 BATCH_DISCOUNT_TYPE_SALE_RESTRICTED = 2 +BATCH_DISCOUNT_TYPE_SLIDING_PERCENT = 3 +BATCH_DISCOUNT_TYPE_SLIDING_AMOUNT = 5 BATCH_DISCOUNT_TYPE = OrderedDict([ - (BATCH_DISCOUNT_TYPE_PRICE_CHANGE, "Price Change"), + (BATCH_DISCOUNT_TYPE_PRICE_CHANGE, "None (Change regular price)"), (BATCH_DISCOUNT_TYPE_SALE_EVERYONE, "Sale for everyone"), - (BATCH_DISCOUNT_TYPE_SALE_RESTRICTED, "Member/Owner only sale"), + (BATCH_DISCOUNT_TYPE_SALE_RESTRICTED, "Sale for Members"), + (BATCH_DISCOUNT_TYPE_SLIDING_PERCENT, "Sliding % Off for Members"), + (BATCH_DISCOUNT_TYPE_SLIDING_AMOUNT, "Sliding $ Off for Members"), + (BATCH_DISCOUNT_TYPE_TRACKING, "Tracking (does not change any prices)"), +]) + + +BATCH_EDITOR_UI_STANDARD = 1 +BATCH_EDITOR_UI_PAIRED_SALE = 2 +BATCH_EDITOR_UI_PARTIAL = 3 +BATCH_EDITOR_UI_TRACKING = 4 + +BATCH_EDITOR_UI = OrderedDict([ + (BATCH_EDITOR_UI_STANDARD, "Standard"), + (BATCH_EDITOR_UI_PAIRED_SALE, "Paired Sale"), + (BATCH_EDITOR_UI_PARTIAL, "Partial"), + (BATCH_EDITOR_UI_TRACKING, "Tracking"), ]) From 01852ceecc437ac7b5f5c429d7d1f5487c40daa4 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 15 Jan 2025 08:45:59 -0600 Subject: [PATCH 122/136] fix: misc. cleanup for sales batch models --- corepos/db/office_op/model.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 68627c0..6087f67 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -1636,7 +1636,9 @@ class BatchType(Base): """ __tablename__ = 'batchType' - id = sa.Column('batchTypeID', sa.Integer(), primary_key=True, autoincrement=False, nullable=False) + # nb. this is *not* autoincrement for some reason; must + # calculate new ID manually based on max existing + id = sa.Column('batchTypeID', sa.Integer(), nullable=False, primary_key=True, autoincrement=False) description = sa.Column('typeDesc', sa.String(length=50), nullable=True) @@ -1661,9 +1663,6 @@ class Batch(Base): Represents a batch. """ __tablename__ = 'batches' - __table_args__ = ( - sa.ForeignKeyConstraint(['batchType'], ['batchType.batchTypeID']), - ) id = sa.Column('batchID', sa.Integer(), primary_key=True, autoincrement=True, nullable=False) @@ -1673,7 +1672,8 @@ class Batch(Base): name = sa.Column('batchName', sa.String(length=80), nullable=True) - batch_type_id = sa.Column('batchType', sa.Integer(), nullable=True) + batch_type_id = sa.Column('batchType', sa.Integer(), + sa.ForeignKey('batchType.batchTypeID'), nullable=True) batch_type = orm.relationship(BatchType) discount_type = sa.Column('discountType', sa.Integer(), nullable=True) @@ -1695,16 +1695,18 @@ class BatchItem(Base): Represents a batch "list" item. """ __tablename__ = 'batchList' - __table_args__ = ( - sa.ForeignKeyConstraint(['batchID'], ['batches.batchID']), - ) id = sa.Column('listID', sa.Integer(), primary_key=True, autoincrement=True, nullable=False) - batch_id = sa.Column('batchID', sa.Integer(), nullable=True) + batch_id = sa.Column('batchID', sa.Integer(), + sa.ForeignKey('batches.batchID'), nullable=True) batch = orm.relationship(Batch, backref=orm.backref('items')) upc = sa.Column(sa.String(length=13), nullable=True) + product = orm.relationship( + Product, + primaryjoin=Product.upc == upc, + foreign_keys=[upc]) sale_price = sa.Column('salePrice', sa.Numeric(precision=9, scale=3), nullable=True) From b8ca60b508a2e3689ee7061be0d2c15841f07a64 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 15 Jan 2025 08:51:40 -0600 Subject: [PATCH 123/136] =?UTF-8?q?bump:=20version=200.3.3=20=E2=86=92=200?= =?UTF-8?q?.3.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 7 +++++++ pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccc62cb..39309fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.3.4 (2025-01-15) + +### Fix + +- misc. cleanup for sales batch models +- add more enums for batch discount type, editor UI + ## v0.3.3 (2025-01-13) ### Fix diff --git a/pyproject.toml b/pyproject.toml index a59e830..143843f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "pyCOREPOS" -version = "0.3.3" +version = "0.3.4" description = "Python Interface to CORE POS" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 7aaa35dac70b059f2d43eb1b05d98a8b2963e51d Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 15 Jan 2025 10:59:14 -0600 Subject: [PATCH 124/136] fix: add workaround to avoid missing schema columns more columns will need to be added to this workaround i'm sure, but this takes care of a couple small ones --- corepos/db/office_op/model.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 6087f67..4d79e88 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -1039,12 +1039,11 @@ class MemberType(Base): ssi = sa.Column(sa.Boolean(), nullable=True) - ignoreSales = sa.Column(sa.Boolean(), nullable=True, default=False) - ignore_sales = orm.synonym('ignoreSales') + # nb. this must be added explicitly if DB is new enough + #ignore_sales = sa.Column('ignoreSales', sa.Boolean(), nullable=True, default=False) - # TODO: this was apparently added "recently" - isn't present in all DBs - # (need to figure out how to conditionally include it in model?) - # sales_code = sa.Column('salesCode', sa.Integer(), nullable=True) + # nb. this must be added explicitly if DB is new enough + #sales_code = sa.Column('salesCode', sa.Integer(), nullable=True) def __str__(self): return self.description or "" @@ -1842,3 +1841,22 @@ class PurchaseOrderNote(Base): def __str__(self): return self.notes or "" + + +# the rest of this is a workaround to deal with the fact that some +# CORE databases have columns which others do not. i had assumed that +# all would be more or less the same but not so in practice. so if +# your DB *does* have these columns, you must invoke the function +# below in order to merge them into your schema. you should do this +# on app startup and they'll be available normally from then on. + +RUNTIME = {'added_latest_columns': False} + +def use_latest_columns(): + if RUNTIME['added_latest_columns']: + return + + MemberType.ignore_sales = sa.Column('ignoreSales', sa.Boolean(), nullable=True, default=False) + MemberType.sales_code = sa.Column('salesCode', sa.Integer(), nullable=True) + + RUNTIME['added_latest_columns'] = True From 6c1fc9a8031a7dc8c8efd682349922f3915bf4e2 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 15 Jan 2025 11:00:22 -0600 Subject: [PATCH 125/136] =?UTF-8?q?bump:=20version=200.3.4=20=E2=86=92=200?= =?UTF-8?q?.3.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39309fe..bba74ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.3.5 (2025-01-15) + +### Fix + +- add workaround to avoid missing schema columns + ## v0.3.4 (2025-01-15) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 143843f..2aaef92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "pyCOREPOS" -version = "0.3.4" +version = "0.3.5" description = "Python Interface to CORE POS" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From d21346bbffb618594d630e5230589c69839c2891 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 15 Jan 2025 14:48:18 -0600 Subject: [PATCH 126/136] fix: fix ordering of name columns for MemberInfo so they show up correctly by default e.g. in UI --- corepos/db/office_op/model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 4d79e88..ff3f493 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -1259,14 +1259,14 @@ class MemberInfo(Base): card_number = sa.Column('card_no', sa.Integer(), primary_key=True, autoincrement=False, nullable=False) - last_name = sa.Column(sa.String(length=30), nullable=True) - first_name = sa.Column(sa.String(length=30), nullable=True) - other_last_name = sa.Column('othlast_name', sa.String(length=30), nullable=True) + last_name = sa.Column(sa.String(length=30), nullable=True) other_first_name = sa.Column('othfirst_name', sa.String(length=30), nullable=True) + other_last_name = sa.Column('othlast_name', sa.String(length=30), nullable=True) + street = sa.Column(sa.String(length=255), nullable=True) city = sa.Column(sa.String(length=20), nullable=True) From 97fb0b28cba228ef435b954a5a9784e0f547f212 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 24 Jan 2025 19:14:13 -0600 Subject: [PATCH 127/136] feat: add common base class for `dtransactions` and similar models more work to be done here i'm sure, but this should hopefully be a conservative change in the right direction --- corepos/db/common/__init__.py | 0 corepos/db/common/trans.py | 123 ++++++++++++++++++++++++++++++ corepos/db/lane_trans/__init__.py | 30 ++++++++ corepos/db/lane_trans/model.py | 62 +++++++++++++++ corepos/db/office_arch/model.py | 21 ++++- corepos/db/office_trans/model.py | 111 ++------------------------- 6 files changed, 239 insertions(+), 108 deletions(-) create mode 100644 corepos/db/common/__init__.py create mode 100644 corepos/db/common/trans.py create mode 100644 corepos/db/lane_trans/__init__.py create mode 100644 corepos/db/lane_trans/model.py diff --git a/corepos/db/common/__init__.py b/corepos/db/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/corepos/db/common/trans.py b/corepos/db/common/trans.py new file mode 100644 index 0000000..2b4e503 --- /dev/null +++ b/corepos/db/common/trans.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# pyCOREPOS -- Python Interface to CORE POS +# Copyright © 2018-2025 Lance Edgar +# +# This file is part of pyCOREPOS. +# +# pyCOREPOS is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# pyCOREPOS is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# pyCOREPOS. If not, see . +# +################################################################################ +""" +Common schema for transaction data models +""" + +import sqlalchemy as sa +from sqlalchemy import orm +from sqlalchemy.ext.declarative import declared_attr + + +class TransactionDetailBase: + """ + Base class for POS transaction detail models, shared by Office + + Lane. + """ + + # register + register_number = sa.Column('register_no', sa.Integer(), nullable=True) + + # txn + transaction_id = sa.Column('trans_id', sa.Integer(), nullable=True) + transaction_number = sa.Column('trans_no', sa.Integer(), nullable=True) + transaction_type = sa.Column('trans_type', sa.String(length=1), nullable=True) + transaction_subtype = sa.Column('trans_subtype', sa.String(length=2), nullable=True) + trans_status = sa.Column(sa.String(length=1), nullable=True) + + @declared_attr + def transaction_status(self): + return orm.synonym('trans_status') + + # cashier + employee_number = sa.Column('emp_no', sa.Integer(), nullable=True) + + # customer + card_number = sa.Column('card_no', sa.Integer(), nullable=True) + member_type = sa.Column('memType', sa.Integer(), nullable=True) + staff = sa.Column(sa.Boolean(), nullable=True) + + ############################## + # remainder is "line item" ... + ############################## + + upc = sa.Column(sa.String(length=13), nullable=True) + + department_number = sa.Column('department', sa.Integer(), nullable=True) + + description = sa.Column(sa.String(length=30), nullable=True) + + quantity = sa.Column(sa.Float(), nullable=True) + + scale = sa.Column(sa.Boolean(), nullable=True, default=False) + + cost = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True) + + unitPrice = sa.Column('unitPrice', sa.Numeric(precision=10, scale=2), nullable=True) + + @declared_attr + def unit_price(self): + return orm.synonym('unitPrice') + + total = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True) + + reg_price = sa.Column('regPrice', sa.Numeric(precision=10, scale=2), nullable=True) + + tax = sa.Column(sa.SmallInteger(), nullable=True) + + @declared_attr + def tax_rate_id(self): + return orm.synonym('tax') + + food_stamp = sa.Column('foodstamp', sa.Boolean(), nullable=True) + + discount = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True) + + member_discount = sa.Column('memDiscount', sa.Numeric(precision=10, scale=2), nullable=True) + + discountable = sa.Column(sa.Boolean(), nullable=True) + + discount_type = sa.Column('discounttype', sa.Integer(), nullable=True) + + voided = sa.Column(sa.Integer(), nullable=True) + + percent_discount = sa.Column('percentDiscount', sa.Integer(), nullable=True) + + item_quantity = sa.Column('ItemQtty', sa.Float(), nullable=True) + + volume_discount_type = sa.Column('volDiscType', sa.Integer(), nullable=True) + + volume = sa.Column(sa.Integer(), nullable=True) + + volume_special = sa.Column('VolSpecial', sa.Numeric(precision=10, scale=2), nullable=True) + + mix_match = sa.Column('mixMatch', sa.String(length=13), nullable=True) + + matched = sa.Column(sa.Boolean(), nullable=True) + + num_flag = sa.Column('numflag', sa.Integer(), nullable=True, default=0) + + char_flag = sa.Column('charflag', sa.String(length=2), nullable=True) + + def __str__(self): + return self.description or '' diff --git a/corepos/db/lane_trans/__init__.py b/corepos/db/lane_trans/__init__.py new file mode 100644 index 0000000..8e8c706 --- /dev/null +++ b/corepos/db/lane_trans/__init__.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# pyCOREPOS -- Python Interface to CORE POS +# Copyright © 2018-2025 Lance Edgar +# +# This file is part of pyCOREPOS. +# +# pyCOREPOS is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# pyCOREPOS is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# pyCOREPOS. If not, see . +# +################################################################################ +""" +Lane Transaction Database +""" + +from sqlalchemy import orm + + +Session = orm.sessionmaker() diff --git a/corepos/db/lane_trans/model.py b/corepos/db/lane_trans/model.py new file mode 100644 index 0000000..b656d8f --- /dev/null +++ b/corepos/db/lane_trans/model.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# pyCOREPOS -- Python Interface to CORE POS +# Copyright © 2018-2025 Lance Edgar +# +# This file is part of pyCOREPOS. +# +# pyCOREPOS is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# pyCOREPOS is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# pyCOREPOS. If not, see . +# +################################################################################ +""" +Data model for CORE POS "lane_trans" DB +""" + +import sqlalchemy as sa +from sqlalchemy import orm + +from corepos.db.common import trans as common + + +Base = orm.declarative_base() + + +class DTransactionBase(common.TransactionDetailBase): + """ + Base class for ``dtransactions`` and similar models. + """ + pos_row_id = sa.Column(sa.Integer(), primary_key=True, nullable=False) + + store_id = sa.Column(sa.Integer(), nullable=True, default=0) + date_time = sa.Column('datetime', sa.DateTime(), nullable=True) + + +class DTransaction(DTransactionBase, Base): + """ + Represents a record from ``dtransactions`` table. + """ + __tablename__ = 'dtransactions' + + +class LocalTempTrans(common.TransactionDetailBase, Base): + """ + Represents a record from ``localtemptrans`` table. + """ + __tablename__ = 'localtemptrans' + __table_args__ = ( + sa.PrimaryKeyConstraint('trans_id'), + ) + + date_time = sa.Column('datetime', sa.DateTime(), nullable=True) diff --git a/corepos/db/office_arch/model.py b/corepos/db/office_arch/model.py index 49b0bbd..bc5838f 100644 --- a/corepos/db/office_arch/model.py +++ b/corepos/db/office_arch/model.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2024 Lance Edgar +# Copyright © 2018-2025 Lance Edgar # # This file is part of pyCOREPOS. # @@ -24,9 +24,11 @@ CORE Office "arch" data model """ +import sqlalchemy as sa from sqlalchemy import orm -from corepos.db.office_trans.model import DTransactionBase, DLogBase +from corepos.db.common import trans as common +from corepos.db.office_trans.model import DTransactionBase Base = orm.declarative_base() @@ -34,7 +36,7 @@ Base = orm.declarative_base() class BigArchive(DTransactionBase, Base): """ - Represents a record from ``bigArchive`` table. + Data model for ``bigArchive`` table. """ __tablename__ = 'bigArchive' @@ -43,8 +45,19 @@ class BigArchive(DTransactionBase, Base): TransactionDetail = BigArchive +class DLogBase(common.TransactionDetailBase): + """ + Base class for ``dlogBig`` and similar models. + """ + store_row_id = sa.Column(sa.Integer(), primary_key=True, nullable=False) + + store_id = sa.Column(sa.Integer(), nullable=True, default=0) + pos_row_id = sa.Column(sa.Integer(), nullable=True) + date_time = sa.Column('tdate', sa.DateTime(), nullable=True) + + class DLogBig(DLogBase, Base): """ - Represents a record from ``dlogBig`` view. + Data model for ``dlogBig`` view. """ __tablename__ = 'dlogBig' diff --git a/corepos/db/office_trans/model.py b/corepos/db/office_trans/model.py index b4659cd..c2b0959 100644 --- a/corepos/db/office_trans/model.py +++ b/corepos/db/office_trans/model.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2024 Lance Edgar +# Copyright © 2018-2025 Lance Edgar # # This file is part of pyCOREPOS. # @@ -26,7 +26,8 @@ CORE POS Transaction Data Model import sqlalchemy as sa from sqlalchemy import orm -from sqlalchemy.ext.declarative import declared_attr + +from corepos.db.common import trans as common Base = orm.declarative_base() @@ -68,118 +69,20 @@ class EquityLiveBalance(Base): start_date = sa.Column('startdate', sa.DateTime(), nullable=True) -class TransactionDetailBase: +class DTransactionBase(common.TransactionDetailBase): """ - Represents a POS transaction detail record. + Base class for ``dtransactions`` and similar models. """ - - # store store_row_id = sa.Column(sa.Integer(), primary_key=True, nullable=False) - store_id = sa.Column(sa.Integer(), nullable=True, default=0) - # register - register_number = sa.Column('register_no', sa.Integer(), nullable=True) pos_row_id = sa.Column(sa.Integer(), nullable=True) - - # txn - transaction_id = sa.Column('trans_id', sa.Integer(), nullable=True) - transaction_number = sa.Column('trans_no', sa.Integer(), nullable=True) - transaction_type = sa.Column('trans_type', sa.String(length=1), nullable=True) - transaction_subtype = sa.Column('trans_subtype', sa.String(length=2), nullable=True) - - trans_status = sa.Column(sa.String(length=1), nullable=True) - - @declared_attr - def transaction_status(self): - return orm.synonym('trans_status') - - # cashier - employee_number = sa.Column('emp_no', sa.Integer(), nullable=True) - - # customer - card_number = sa.Column('card_no', sa.Integer(), nullable=True) - member_type = sa.Column('memType', sa.Integer(), nullable=True) - staff = sa.Column(sa.Boolean(), nullable=True) - - ############################## - # remainder is "line item" ... - ############################## - - upc = sa.Column(sa.String(length=13), nullable=True) - - department_number = sa.Column('department', sa.Integer(), nullable=True) - - description = sa.Column(sa.String(length=30), nullable=True) - - quantity = sa.Column(sa.Float(), nullable=True) - - scale = sa.Column(sa.Boolean(), nullable=True, default=False) - - cost = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True) - - unitPrice = sa.Column('unitPrice', sa.Numeric(precision=10, scale=2), nullable=True) - - @declared_attr - def unit_price(self): - return orm.synonym('unitPrice') - - total = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True) - - reg_price = sa.Column('regPrice', sa.Numeric(precision=10, scale=2), nullable=True) - - tax = sa.Column(sa.SmallInteger(), nullable=True) - - @declared_attr - def tax_rate_id(self): - return orm.synonym('tax') - - food_stamp = sa.Column('foodstamp', sa.Boolean(), nullable=True) - - discount = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True) - - member_discount = sa.Column('memDiscount', sa.Numeric(precision=10, scale=2), nullable=True) - - discountable = sa.Column(sa.Boolean(), nullable=True) - - discount_type = sa.Column('discounttype', sa.Integer(), nullable=True) - - voided = sa.Column(sa.Integer(), nullable=True) - - percent_discount = sa.Column('percentDiscount', sa.Integer(), nullable=True) - - item_quantity = sa.Column('ItemQtty', sa.Float(), nullable=True) - - volume_discount_type = sa.Column('volDiscType', sa.Integer(), nullable=True) - - volume = sa.Column(sa.Integer(), nullable=True) - - volume_special = sa.Column('VolSpecial', sa.Numeric(precision=10, scale=2), nullable=True) - - mix_match = sa.Column('mixMatch', sa.String(length=13), nullable=True) - - matched = sa.Column(sa.Boolean(), nullable=True) - - num_flag = sa.Column('numflag', sa.Integer(), nullable=True, default=0) - - char_flag = sa.Column('charflag', sa.String(length=2), nullable=True) - - def __str__(self): - return self.description or '' - - -class DTransactionBase(TransactionDetailBase): - + store_id = sa.Column(sa.Integer(), nullable=True, default=0) date_time = sa.Column('datetime', sa.DateTime(), nullable=True) -class DLogBase(TransactionDetailBase): - - date_time = sa.Column('tdate', sa.DateTime(), nullable=True) - - class DTransaction(DTransactionBase, Base): """ - Represents a record from ``dtransactions`` table. + Data model for ``dtransactions`` table. """ __tablename__ = 'dtransactions' From c3b639390d1abfc3b13c0a5e4313b3161997de8f Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 24 Jan 2025 19:21:00 -0600 Subject: [PATCH 128/136] fix: add `Employee` model for lane_op with abstract common base schema --- corepos/db/common/op.py | 56 +++++++++++++++++++++++++++++++++++ corepos/db/lane_op/model.py | 11 ++++++- corepos/db/office_op/model.py | 31 ++++--------------- 3 files changed, 71 insertions(+), 27 deletions(-) create mode 100644 corepos/db/common/op.py diff --git a/corepos/db/common/op.py b/corepos/db/common/op.py new file mode 100644 index 0000000..7cf84fc --- /dev/null +++ b/corepos/db/common/op.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# pyCOREPOS -- Python Interface to CORE POS +# Copyright © 2018-2025 Lance Edgar +# +# This file is part of pyCOREPOS. +# +# pyCOREPOS is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# pyCOREPOS is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# pyCOREPOS. If not, see . +# +################################################################################ +""" +Common schema for operational data models +""" + +import sqlalchemy as sa + + +class EmployeeBase: + """ + Base class for Employee models, shared by Office + Lane. + """ + number = sa.Column('emp_no', sa.SmallInteger(), nullable=False, + primary_key=True, autoincrement=False) + + cashier_password = sa.Column('CashierPassword', sa.String(length=50), nullable=True) + + admin_password = sa.Column('AdminPassword', sa.String(length=50), nullable=True) + + first_name = sa.Column('FirstName', sa.String(length=255), nullable=True) + + last_name = sa.Column('LastName', sa.String(length=255), nullable=True) + + job_title = sa.Column('JobTitle', sa.String(length=255), nullable=True) + + active = sa.Column('EmpActive', sa.Boolean(), nullable=True) + + frontend_security = sa.Column('frontendsecurity', sa.SmallInteger(), nullable=True) + + backend_security = sa.Column('backendsecurity', sa.SmallInteger(), nullable=True) + + birth_date = sa.Column('birthdate', sa.DateTime(), nullable=True) + + def __str__(self): + return ' '.join([self.first_name or '', self.last_name or '']).strip() diff --git a/corepos/db/lane_op/model.py b/corepos/db/lane_op/model.py index c7ed5b2..08da4f2 100644 --- a/corepos/db/lane_op/model.py +++ b/corepos/db/lane_op/model.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2023 Lance Edgar +# Copyright © 2018-2025 Lance Edgar # # This file is part of pyCOREPOS. # @@ -27,10 +27,19 @@ Data model for CORE POS "lane_op" DB import sqlalchemy as sa from sqlalchemy import orm +from corepos.db.common import op as common + Base = orm.declarative_base() +class Employee(common.EmployeeBase, Base): + """ + Data model for ``employees`` table. + """ + __tablename__ = 'employees' + + class Department(Base): """ Represents a department within the organization. diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index ff3f493..67f405a 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2024 Lance Edgar +# Copyright © 2018-2025 Lance Edgar # # This file is part of pyCOREPOS. # @@ -31,6 +31,8 @@ import sqlalchemy as sa from sqlalchemy import orm from sqlalchemy.ext.associationproxy import association_proxy +from corepos.db.common import op as common + log = logging.getLogger(__name__) @@ -991,35 +993,12 @@ class ProductPhysicalLocation(Base): location = sa.Column(sa.SmallInteger(), nullable=True, default=0) -class Employee(Base): +class Employee(common.EmployeeBase, Base): """ - Represents an employee within the organization. + Data model for ``employees`` table. """ __tablename__ = 'employees' - number = sa.Column('emp_no', sa.SmallInteger(), primary_key=True, autoincrement=False, nullable=False) - - cashier_password = sa.Column('CashierPassword', sa.String(length=50), nullable=True) - - admin_password = sa.Column('AdminPassword', sa.String(length=50), nullable=True) - - first_name = sa.Column('FirstName', sa.String(length=255), nullable=True) - - last_name = sa.Column('LastName', sa.String(length=255), nullable=True) - - job_title = sa.Column('JobTitle', sa.String(length=255), nullable=True) - - active = sa.Column('EmpActive', sa.Boolean(), nullable=True) - - frontend_security = sa.Column('frontendsecurity', sa.SmallInteger(), nullable=True) - - backend_security = sa.Column('backendsecurity', sa.SmallInteger(), nullable=True) - - birth_date = sa.Column('birthdate', sa.DateTime(), nullable=True) - - def __str__(self): - return ' '.join([self.first_name or '', self.last_name or '']).strip() - class MemberType(Base): """ From 28cb23adc4a60cc7a3f9c37fa906281a45487f64 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 24 Jan 2025 19:59:18 -0600 Subject: [PATCH 129/136] =?UTF-8?q?bump:=20version=200.3.5=20=E2=86=92=200?= =?UTF-8?q?.4.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 11 +++++++++++ pyproject.toml | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bba74ba..ca4e4bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.4.0 (2025-01-24) + +### Feat + +- add common base class for `dtransactions` and similar models + +### Fix + +- add `Employee` model for lane_op +- fix ordering of name columns for MemberInfo + ## v0.3.5 (2025-01-15) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 2aaef92..75ef20f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "pyCOREPOS" -version = "0.3.5" +version = "0.4.0" description = "Python Interface to CORE POS" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 50351596acaced2b5b14fd4ad4f3488f9d09b111 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 24 Jan 2025 20:18:13 -0600 Subject: [PATCH 130/136] fix: add model for lane_trans `LocalTrans` --- corepos/db/lane_trans/model.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/corepos/db/lane_trans/model.py b/corepos/db/lane_trans/model.py index b656d8f..f2245f5 100644 --- a/corepos/db/lane_trans/model.py +++ b/corepos/db/lane_trans/model.py @@ -26,6 +26,7 @@ Data model for CORE POS "lane_trans" DB import sqlalchemy as sa from sqlalchemy import orm +from sqlalchemy.ext.declarative import declared_attr from corepos.db.common import trans as common @@ -45,18 +46,34 @@ class DTransactionBase(common.TransactionDetailBase): class DTransaction(DTransactionBase, Base): """ - Represents a record from ``dtransactions`` table. + Data model for ``dtransactions`` table. """ __tablename__ = 'dtransactions' -class LocalTempTrans(common.TransactionDetailBase, Base): +class LocalTransBase(common.TransactionDetailBase): """ - Represents a record from ``localtemptrans`` table. + Base class for ``localtrans`` and similar models. """ - __tablename__ = 'localtemptrans' - __table_args__ = ( - sa.PrimaryKeyConstraint('trans_id'), - ) + + @declared_attr + def __table_args__(self): + return ( + sa.PrimaryKeyConstraint('trans_id'), + ) date_time = sa.Column('datetime', sa.DateTime(), nullable=True) + + +class LocalTrans(LocalTransBase, Base): + """ + Data model for ``localtrans`` table. + """ + __tablename__ = 'localtrans' + + +class LocalTempTrans(LocalTransBase, Base): + """ + Data model for ``localtemptrans`` table. + """ + __tablename__ = 'localtemptrans' From 2fe089bd57b93388908a7b5adff3709ebde2e0fb Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 25 Jan 2025 16:41:17 -0600 Subject: [PATCH 131/136] fix: add `Parameter` model for lane_op --- corepos/db/common/op.py | 18 ++++++++++++++++++ corepos/db/lane_op/model.py | 7 +++++++ corepos/db/office_op/model.py | 17 ++--------------- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/corepos/db/common/op.py b/corepos/db/common/op.py index 7cf84fc..c0d40f5 100644 --- a/corepos/db/common/op.py +++ b/corepos/db/common/op.py @@ -27,6 +27,24 @@ Common schema for operational data models import sqlalchemy as sa +class ParameterBase: + """ + Base class for Parameter models, shared by Office + Lane. + """ + store_id = sa.Column(sa.SmallInteger(), primary_key=True, nullable=False) + + lane_id = sa.Column(sa.SmallInteger(), primary_key=True, nullable=False) + + param_key = sa.Column(sa.String(length=100), primary_key=True, nullable=False) + + param_value = sa.Column(sa.String(length=255), nullable=True) + + is_array = sa.Column(sa.Boolean(), nullable=True) + + def __str__(self): + return f"{self.store_id}-{self.lane_id} {self.param_key}" + + class EmployeeBase: """ Base class for Employee models, shared by Office + Lane. diff --git a/corepos/db/lane_op/model.py b/corepos/db/lane_op/model.py index 08da4f2..3074dd1 100644 --- a/corepos/db/lane_op/model.py +++ b/corepos/db/lane_op/model.py @@ -33,6 +33,13 @@ from corepos.db.common import op as common Base = orm.declarative_base() +class Parameter(common.ParameterBase, Base): + """ + Data model for ``parameters`` table. + """ + __tablename__ = 'parameters' + + class Employee(common.EmployeeBase, Base): """ Data model for ``employees`` table. diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 67f405a..ad4b89b 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -56,25 +56,12 @@ class StringableDateTime(sa.TypeDecorator): raise NotImplementedError -class Parameter(Base): +class Parameter(common.ParameterBase, Base): """ - Represents a "parameter" value. + Data model for ``parameters`` table. """ __tablename__ = 'parameters' - store_id = sa.Column(sa.SmallInteger(), primary_key=True, nullable=False) - - lane_id = sa.Column(sa.SmallInteger(), primary_key=True, nullable=False) - - param_key = sa.Column(sa.String(length=100), primary_key=True, nullable=False) - - param_value = sa.Column(sa.String(length=255), nullable=True) - - is_array = sa.Column(sa.Boolean(), nullable=True) - - def __str__(self): - return "{}-{} {}".format(self.store_id, self.lane_id, self.param_key) - class TableSyncRule(Base): """ From a2a1d7faee837c0a0216693a635fe5f237ce6366 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 25 Jan 2025 16:58:17 -0600 Subject: [PATCH 132/136] fix: define common base schema for Product model --- corepos/db/common/op.py | 99 +++++++++++++++++++++++++ corepos/db/lane_op/model.py | 134 +--------------------------------- corepos/db/office_op/model.py | 99 ++----------------------- 3 files changed, 109 insertions(+), 223 deletions(-) diff --git a/corepos/db/common/op.py b/corepos/db/common/op.py index c0d40f5..30ecc1c 100644 --- a/corepos/db/common/op.py +++ b/corepos/db/common/op.py @@ -72,3 +72,102 @@ class EmployeeBase: def __str__(self): return ' '.join([self.first_name or '', self.last_name or '']).strip() + + +class ProductBase: + """ + Base class for Product models, shared by Office + Lane. + """ + id = sa.Column(sa.Integer(), nullable=False, primary_key=True, autoincrement=True) + + upc = sa.Column(sa.String(length=13), nullable=True) + + description = sa.Column(sa.String(length=30), nullable=True) + + brand = sa.Column(sa.String(length=30), nullable=True) + + formatted_name = sa.Column(sa.String(length=30), nullable=True) + + normal_price = sa.Column(sa.Float(), nullable=True) + + price_method = sa.Column('pricemethod', sa.SmallInteger(), nullable=True) + + group_price = sa.Column('groupprice', sa.Float(), nullable=True) + + quantity = sa.Column(sa.SmallInteger(), nullable=True) + + special_price = sa.Column(sa.Float(), nullable=True) + + special_price_method = sa.Column('specialpricemethod', sa.SmallInteger(), nullable=True) + + special_group_price = sa.Column('specialgroupprice', sa.Float(), nullable=True) + + special_quantity = sa.Column('specialquantity', sa.SmallInteger(), nullable=True) + + special_limit = sa.Column(sa.SmallInteger(), nullable=True) + + start_date = sa.Column(sa.DateTime(), nullable=True) + + end_date = sa.Column(sa.DateTime(), nullable=True) + + department_number = sa.Column('department', sa.SmallInteger(), nullable=True) + + size = sa.Column(sa.String(length=9), nullable=True) + + tax_rate_id = sa.Column('tax', sa.SmallInteger(), nullable=True) + + foodstamp = sa.Column(sa.Boolean(), nullable=True) + + scale = sa.Column(sa.Boolean(), nullable=True) + + scale_price = sa.Column('scaleprice', sa.Float(), nullable=True) + + mix_match_code = sa.Column('mixmatchcode', sa.String(length=13), nullable=True) + + created = sa.Column(sa.DateTime(), nullable=True) + + modified = sa.Column(sa.DateTime(), nullable=True) + + tare_weight = sa.Column('tareweight', sa.Float(), nullable=True) + + discount = sa.Column(sa.SmallInteger(), nullable=True) + + discount_type = sa.Column('discounttype', sa.SmallInteger(), nullable=True) + + line_item_discountable = sa.Column(sa.Boolean(), nullable=True) + + unit_of_measure = sa.Column('unitofmeasure', sa.String(length=15), nullable=True) + + wicable = sa.Column(sa.SmallInteger(), nullable=True) + + quantity_enforced = sa.Column('qttyEnforced', sa.Boolean(), nullable=True) + + id_enforced = sa.Column('idEnforced', sa.SmallInteger(), nullable=True) + + cost = sa.Column(sa.Float(), nullable=True) + + special_cost = sa.Column(sa.Float(), nullable=True) + + received_cost = sa.Column(sa.Float(), nullable=True) + + in_use = sa.Column('inUse', sa.Boolean(), nullable=True) + + numflag = sa.Column(sa.Integer(), nullable=True) + + subdepartment_number = sa.Column('subdept', sa.SmallInteger(), nullable=True) + + deposit = sa.Column(sa.Float(), nullable=True) + + local = sa.Column(sa.Integer(), nullable=True, default=0) + + store_id = sa.Column(sa.SmallInteger(), nullable=True) + + default_vendor_id = sa.Column(sa.Integer(), nullable=True) + + current_origin_id = sa.Column(sa.Integer(), nullable=True) + + auto_par = sa.Column(sa.Float(), nullable=True, default=0) + + price_rule_id = sa.Column(sa.Integer(), nullable=True, default=0) + + last_sold = sa.Column(sa.DateTime(), nullable=True) diff --git a/corepos/db/lane_op/model.py b/corepos/db/lane_op/model.py index 3074dd1..456b1b8 100644 --- a/corepos/db/lane_op/model.py +++ b/corepos/db/lane_op/model.py @@ -88,141 +88,11 @@ class Department(Base): return self.name or "" -class Product(Base): +class Product(common.ProductBase, Base): """ - Represents a product, purchased and/or sold by the organization. + Data model for ``products`` table. """ __tablename__ = 'products' - # __table_args__ = ( - # sa.ForeignKeyConstraint(['department'], ['departments.dept_no']), - # sa.ForeignKeyConstraint(['subdept'], ['subdepts.subdept_no']), - # sa.ForeignKeyConstraint(['tax'], ['taxrates.id']), - # ) - - id = sa.Column(sa.Integer(), nullable=False, - primary_key=True, autoincrement=True) - - upc = sa.Column(sa.String(length=13), nullable=True) - - description = sa.Column(sa.String(length=30), nullable=True) - - brand = sa.Column(sa.String(length=30), nullable=True) - - formatted_name = sa.Column(sa.String(length=30), nullable=True) - - normal_price = sa.Column(sa.Float(), nullable=True) - - price_method = sa.Column('pricemethod', sa.SmallInteger(), nullable=True) - - group_price = sa.Column('groupprice', sa.Float(), nullable=True) - - quantity = sa.Column(sa.SmallInteger(), nullable=True) - - special_price = sa.Column(sa.Float(), nullable=True) - - special_price_method = sa.Column('specialpricemethod', sa.SmallInteger(), nullable=True) - - special_group_price = sa.Column('specialgroupprice', sa.Float(), nullable=True) - - special_quantity = sa.Column('specialquantity', sa.SmallInteger(), nullable=True) - - special_limit = sa.Column(sa.SmallInteger(), nullable=True) - - start_date = sa.Column(sa.DateTime(), nullable=True) - - end_date = sa.Column(sa.DateTime(), nullable=True) - - department_number = sa.Column('department', sa.SmallInteger(), nullable=True) - # department = orm.relationship( - # Department, - # primaryjoin=Department.number == department_number, - # foreign_keys=[department_number], - # doc=""" - # Reference to the :class:`Department` to which the product belongs. - # """) - - size = sa.Column(sa.String(length=9), nullable=True) - - tax_rate_id = sa.Column('tax', sa.SmallInteger(), nullable=True) - # tax_rate = orm.relationship(TaxRate) - - foodstamp = sa.Column(sa.Boolean(), nullable=True) - - scale = sa.Column(sa.Boolean(), nullable=True) - - scale_price = sa.Column('scaleprice', sa.Float(), nullable=True) - - mix_match_code = sa.Column('mixmatchcode', sa.String(length=13), nullable=True) - - created = sa.Column(sa.DateTime(), nullable=True) - - modified = sa.Column(sa.DateTime(), nullable=True) - - # TODO: what to do about this 'replaces' thing? - # 'batchID'=>array('type'=>'TINYINT', 'replaces'=>'advertised'), - # batch_id = sa.Column('batchID', sa.SmallInteger(), nullable=True) - # advertised = sa.Column(sa.Boolean(), nullable=True) - - tare_weight = sa.Column('tareweight', sa.Float(), nullable=True) - - discount = sa.Column(sa.SmallInteger(), nullable=True) - - discount_type = sa.Column('discounttype', sa.SmallInteger(), nullable=True) - - line_item_discountable = sa.Column(sa.Boolean(), nullable=True) - - unit_of_measure = sa.Column('unitofmeasure', sa.String(length=15), nullable=True) - - wicable = sa.Column(sa.SmallInteger(), nullable=True) - - quantity_enforced = sa.Column('qttyEnforced', sa.Boolean(), nullable=True) - - id_enforced = sa.Column('idEnforced', sa.SmallInteger(), nullable=True) - - cost = sa.Column(sa.Float(), nullable=True) - - special_cost = sa.Column(sa.Float(), nullable=True) - - received_cost = sa.Column(sa.Float(), nullable=True) - - in_use = sa.Column('inUse', sa.Boolean(), nullable=True) - - flags = sa.Column('numflag', sa.Integer(), nullable=True) - - subdepartment_number = sa.Column('subdept', sa.SmallInteger(), nullable=True) - # subdepartment = orm.relationship( - # Subdepartment, - # primaryjoin=Subdepartment.number == subdepartment_number, - # foreign_keys=[subdepartment_number], - # doc=""" - # Reference to the :class:`Subdepartment` to which the product belongs. - # """) - - deposit = sa.Column(sa.Float(), nullable=True) - - local = sa.Column(sa.Integer(), nullable=True, - default=0) # TODO: do we want a default here? - - store_id = sa.Column(sa.SmallInteger(), nullable=True) - - default_vendor_id = sa.Column(sa.Integer(), nullable=True) - # default_vendor = orm.relationship( - # Vendor, - # primaryjoin=Vendor.id == default_vendor_id, - # foreign_keys=[default_vendor_id], - # doc=""" - # Reference to the default :class:`Vendor` from which the product is obtained. - # """) - - current_origin_id = sa.Column(sa.Integer(), nullable=True) - - auto_par = sa.Column(sa.Float(), nullable=True, - default=0) # TODO: do we want a default here? - - price_rule_id = sa.Column(sa.Integer(), nullable=True) - - # TODO: some older DB's might not have this? guess we'll see - last_sold = sa.Column(sa.DateTime(), nullable=True) class CustomerClassic(Base): diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index ad4b89b..e374d37 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -536,7 +536,7 @@ class Origin(Base): return self.name or self.short_name or "" -class Product(Base): +class Product(common.ProductBase, Base): """ Represents a product, purchased and/or sold by the organization. """ @@ -547,120 +547,37 @@ class Product(Base): sa.ForeignKeyConstraint(['tax'], ['taxrates.id']), ) - id = sa.Column(sa.Integer(), primary_key=True, autoincrement=True, nullable=False) - - upc = sa.Column(sa.String(length=13), nullable=True) - - description = sa.Column(sa.String(length=30), nullable=True) - - brand = sa.Column(sa.String(length=30), nullable=True) - - formatted_name = sa.Column(sa.String(length=30), nullable=True) - - normal_price = sa.Column(sa.Float(), nullable=True) - - price_method = sa.Column('pricemethod', sa.SmallInteger(), nullable=True) - - group_price = sa.Column('groupprice', sa.Float(), nullable=True) - - quantity = sa.Column(sa.SmallInteger(), nullable=True) - - special_price = sa.Column(sa.Float(), nullable=True) - - special_price_method = sa.Column('specialpricemethod', sa.SmallInteger(), nullable=True) - - special_group_price = sa.Column('specialgroupprice', sa.Float(), nullable=True) - - special_quantity = sa.Column('specialquantity', sa.SmallInteger(), nullable=True) - - start_date = sa.Column(sa.DateTime(), nullable=True) - - end_date = sa.Column(sa.DateTime(), nullable=True) - - department_number = sa.Column('department', sa.SmallInteger(), nullable=True) department = orm.relationship( Department, - primaryjoin=Department.number == department_number, - foreign_keys=[department_number], + primaryjoin='Department.number == Product.department_number', + foreign_keys='Product.department_number', doc=""" Reference to the :class:`Department` to which the product belongs. """) - size = sa.Column(sa.String(length=9), nullable=True) - - tax_rate_id = sa.Column('tax', sa.SmallInteger(), nullable=True) tax_rate = orm.relationship(TaxRate) - foodstamp = sa.Column(sa.Boolean(), nullable=True) - - scale = sa.Column(sa.Boolean(), nullable=True) - - # TODO: yikes, did i just code this all wrong the first time? pretty sure - # this needs to change to a decimal column... - scale_price = sa.Column('scaleprice', sa.Boolean(), nullable=True) - # scale_price = sa.Column('scaleprice', sa.Numeric(precision=10, scale=2), nullable=True) - - mix_match_code = sa.Column('mixmatchcode', sa.String(length=13), nullable=True) - - created = sa.Column(StringableDateTime(), nullable=True) - - modified = sa.Column(sa.DateTime(), nullable=True) - # advertised = sa.Column(sa.Boolean(), nullable=True) - tare_weight = sa.Column('tareweight', sa.Float(), nullable=True) - - discount = sa.Column(sa.SmallInteger(), nullable=True) - - discount_type = sa.Column('discounttype', sa.SmallInteger(), nullable=True) - - line_item_discountable = sa.Column(sa.Boolean(), nullable=True) - - unit_of_measure = sa.Column('unitofmeasure', sa.String(length=15), nullable=True) - - wicable = sa.Column(sa.SmallInteger(), nullable=True) - - quantity_enforced = sa.Column('qttyEnforced', sa.Boolean(), nullable=True) - - id_enforced = sa.Column('idEnforced', sa.SmallInteger(), nullable=True) - - cost = sa.Column(sa.Float(), nullable=True) - - in_use = sa.Column('inUse', sa.Boolean(), nullable=True) - - flags = sa.Column('numflag', sa.Integer(), nullable=True) - - subdepartment_number = sa.Column('subdept', sa.SmallInteger(), nullable=True) subdepartment = orm.relationship( Subdepartment, - primaryjoin=Subdepartment.number == subdepartment_number, - foreign_keys=[subdepartment_number], + primaryjoin='Subdepartment.number == Product.subdepartment_number', + foreign_keys='Product.subdepartment_number', doc=""" Reference to the :class:`Subdepartment` to which the product belongs. """) - deposit = sa.Column(sa.Float(), nullable=True) - - local = sa.Column(sa.Integer(), nullable=True) - - store_id = sa.Column(sa.SmallInteger(), nullable=True) - - default_vendor_id = sa.Column(sa.Integer(), nullable=True) default_vendor = orm.relationship( Vendor, - primaryjoin=Vendor.id == default_vendor_id, - foreign_keys=[default_vendor_id], + primaryjoin='Vendor.id == Product.default_vendor_id', + foreign_keys='Product.default_vendor_id', doc=""" Reference to the default :class:`Vendor` from which the product is obtained. """) + # TODO: deprecate / remove this? vendor = orm.synonym('default_vendor') - current_origin_id = sa.Column(sa.Integer(), nullable=True) - - # TODO: some older DB's might not have this? guess we'll see - last_sold = sa.Column(sa.DateTime(), nullable=True) - like_code = association_proxy( '_like_code', 'like_code', creator=lambda lc: ProductLikeCode(like_code=lc), From 97a1396a543e052caf9bd24c5ad1837f9e68eb39 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 25 Jan 2025 17:01:10 -0600 Subject: [PATCH 133/136] feat: use true column names for transaction data models as much as i kind of want to "rename" some of these for convenience, it seems safest here to just stick with true names to avoid confusion --- corepos/db/common/trans.py | 59 ++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/corepos/db/common/trans.py b/corepos/db/common/trans.py index 2b4e503..9ec5601 100644 --- a/corepos/db/common/trans.py +++ b/corepos/db/common/trans.py @@ -36,25 +36,21 @@ class TransactionDetailBase: """ # register - register_number = sa.Column('register_no', sa.Integer(), nullable=True) + register_no = sa.Column(sa.Integer(), nullable=True) # txn - transaction_id = sa.Column('trans_id', sa.Integer(), nullable=True) - transaction_number = sa.Column('trans_no', sa.Integer(), nullable=True) - transaction_type = sa.Column('trans_type', sa.String(length=1), nullable=True) - transaction_subtype = sa.Column('trans_subtype', sa.String(length=2), nullable=True) + trans_id = sa.Column(sa.Integer(), nullable=True) + trans_no = sa.Column(sa.Integer(), nullable=True) + trans_type = sa.Column(sa.String(length=1), nullable=True) + trans_subtype = sa.Column(sa.String(length=2), nullable=True) trans_status = sa.Column(sa.String(length=1), nullable=True) - @declared_attr - def transaction_status(self): - return orm.synonym('trans_status') - # cashier - employee_number = sa.Column('emp_no', sa.Integer(), nullable=True) + emp_no = sa.Column(sa.Integer(), nullable=True) # customer - card_number = sa.Column('card_no', sa.Integer(), nullable=True) - member_type = sa.Column('memType', sa.Integer(), nullable=True) + card_no = sa.Column(sa.Integer(), nullable=True) + memType = sa.Column(sa.Integer(), nullable=True) staff = sa.Column(sa.Boolean(), nullable=True) ############################## @@ -63,7 +59,7 @@ class TransactionDetailBase: upc = sa.Column(sa.String(length=13), nullable=True) - department_number = sa.Column('department', sa.Integer(), nullable=True) + department = sa.Column(sa.Integer(), nullable=True) description = sa.Column(sa.String(length=30), nullable=True) @@ -73,51 +69,46 @@ class TransactionDetailBase: cost = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True) - unitPrice = sa.Column('unitPrice', sa.Numeric(precision=10, scale=2), nullable=True) - - @declared_attr - def unit_price(self): - return orm.synonym('unitPrice') + unitPrice = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True) total = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True) - reg_price = sa.Column('regPrice', sa.Numeric(precision=10, scale=2), nullable=True) + regPrice = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True) tax = sa.Column(sa.SmallInteger(), nullable=True) - @declared_attr - def tax_rate_id(self): - return orm.synonym('tax') - - food_stamp = sa.Column('foodstamp', sa.Boolean(), nullable=True) + foodstamp = sa.Column(sa.Boolean(), nullable=True) discount = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True) - member_discount = sa.Column('memDiscount', sa.Numeric(precision=10, scale=2), nullable=True) + memDiscount = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True) discountable = sa.Column(sa.Boolean(), nullable=True) - discount_type = sa.Column('discounttype', sa.Integer(), nullable=True) + discounttype = sa.Column(sa.Integer(), nullable=True) voided = sa.Column(sa.Integer(), nullable=True) - percent_discount = sa.Column('percentDiscount', sa.Integer(), nullable=True) + percentDiscount = sa.Column(sa.Integer(), nullable=True) - item_quantity = sa.Column('ItemQtty', sa.Float(), nullable=True) + ItemQtty = sa.Column(sa.Float(), nullable=True) - volume_discount_type = sa.Column('volDiscType', sa.Integer(), nullable=True) + volDiscType = sa.Column(sa.Integer(), nullable=True) volume = sa.Column(sa.Integer(), nullable=True) - volume_special = sa.Column('VolSpecial', sa.Numeric(precision=10, scale=2), nullable=True) + VolSpecial = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True) - mix_match = sa.Column('mixMatch', sa.String(length=13), nullable=True) + mixMatch = sa.Column(sa.String(length=13), nullable=True) matched = sa.Column(sa.Boolean(), nullable=True) - num_flag = sa.Column('numflag', sa.Integer(), nullable=True, default=0) + numflag = sa.Column(sa.Integer(), nullable=True, default=0) - char_flag = sa.Column('charflag', sa.String(length=2), nullable=True) + charflag = sa.Column(sa.String(length=2), nullable=True) def __str__(self): - return self.description or '' + txnid = '-'.join([str(val) for val in [self.register_no, + self.trans_no, + self.trans_id]]) + return f"{txnid} {self.description or ''}" From 8359e5692e62e5c289f90871b48be160d141c8c3 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 1 Feb 2025 15:19:12 -0600 Subject: [PATCH 134/136] =?UTF-8?q?bump:=20version=200.4.0=20=E2=86=92=200?= =?UTF-8?q?.5.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 12 ++++++++++++ pyproject.toml | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca4e4bb..0c46d8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.5.0 (2025-02-01) + +### Feat + +- use true column names for transaction data models + +### Fix + +- define common base schema for Product model +- add `Parameter` model for lane_op +- add model for lane_trans `LocalTrans` + ## v0.4.0 (2025-01-24) ### Feat diff --git a/pyproject.toml b/pyproject.toml index 75ef20f..35eb3a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "pyCOREPOS" -version = "0.4.0" +version = "0.5.0" description = "Python Interface to CORE POS" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 9b9260ba4b9428da5f2ced0d542e1307c88a90ec Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 20 Feb 2025 09:30:24 -0600 Subject: [PATCH 135/136] fix: add `Product.default_vendor_item` convenience property --- corepos/db/office_op/model.py | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index e374d37..60ad478 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -583,6 +583,32 @@ class Product(common.ProductBase, Base): creator=lambda lc: ProductLikeCode(like_code=lc), ) + vendor_items = orm.relationship( + 'VendorItem', + back_populates='product', + primaryjoin='VendorItem.upc == Product.upc', + foreign_keys='VendorItem.upc', + order_by='VendorItem.vendor_item_id', + doc=""" + List of :class:`VendorItem` records for this product. + """) + + @property + def default_vendor_item(self): + """ + Returns the "default" vendor item record. This will + correspond to the :attr:`default_vendor` if possible. + + :rtype: :class:`VendorItem` or ``None`` + """ + if self.default_vendor: + for item in self.vendor_items: + if item.vendor_id == self.default_vendor.id: + return item + + if self.vendor_items: + return self.vendor_items[0] + @property def full_description(self): fields = ['brand', 'description', 'size'] @@ -732,17 +758,12 @@ class VendorItem(Base): upc = sa.Column(sa.String(length=13), nullable=False) product = orm.relationship( Product, + back_populates='vendor_items', primaryjoin=Product.upc == upc, foreign_keys=[upc], doc=""" Reference to the :class:`Product` to which this record applies. - """, - backref=orm.backref( - 'vendor_items', - order_by=vendor_item_id, - doc=""" - List of :class:`VendorItem` records for this product. - """)) + """) brand = sa.Column(sa.String(length=50), nullable=True) From feb2d3471e3451a674ca77c26fac6dba0e371f28 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 20 Feb 2025 09:31:29 -0600 Subject: [PATCH 136/136] =?UTF-8?q?bump:=20version=200.5.0=20=E2=86=92=200?= =?UTF-8?q?.5.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c46d8c..2c26baa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to pyCOREPOS will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.5.1 (2025-02-20) + +### Fix + +- add `Product.default_vendor_item` convenience property + ## v0.5.0 (2025-02-01) ### Feat diff --git a/pyproject.toml b/pyproject.toml index 35eb3a8..cf50f41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "pyCOREPOS" -version = "0.5.0" +version = "0.5.1" description = "Python Interface to CORE POS" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]