diff --git a/.gitignore b/.gitignore index 07ddefb..9a52e5b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1 @@ -*~ -*.pyc -dist/ pyCOREPOS.egg-info/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c26baa..5b1c709 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,177 +5,6 @@ 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 - -- 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 - -- 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 - -- add workaround to avoid missing schema columns - -## 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 - -- remove `autoincrement` option for composite PK fields - -## 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 - -- add `wicable`, `active` columns for Department model - -## 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 - -- add API methods, `get_employees()` and `get_employee()` -- remove `Change` data model -- remove dependency for `six` package - -## 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. - -## [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. -- Let `MemberInfo.dates` be an object, not a list. - -## [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`. - -## [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. - -## [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. -- 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. - -## [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. - -## [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`. -- Add model for `UserGroup`. - -## [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. - -## [0.1.4] - 2021-08-02 -### Changed -- Add schema for `TableSyncRules`. - -## [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. - -## [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/README.md b/README.md deleted file mode 100644 index dd69797..0000000 --- a/README.md +++ /dev/null @@ -1,5 +0,0 @@ - -# pyCOREPOS - -A Python interface to the [CORE POS](https://github.com/CORE-POS) -system. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..0b34d2f --- /dev/null +++ b/README.rst @@ -0,0 +1,7 @@ + +pyCOREPOS +========= + +A Python interface to the `CORE POS`_ system. + +.. _CORE POS: https://github.com/CORE-POS diff --git a/corepos/__init__.py b/corepos/__init__.py index 22b270b..e69de29 100644 --- a/corepos/__init__.py +++ b/corepos/__init__.py @@ -1,27 +0,0 @@ -# -*- 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__ diff --git a/corepos/_version.py b/corepos/_version.py index 555fee3..e41b669 100644 --- a/corepos/_version.py +++ b/corepos/_version.py @@ -1,6 +1,3 @@ # -*- coding: utf-8; -*- -from importlib.metadata import version - - -__version__ = version('pyCOREPOS') +__version__ = '0.1.0' diff --git a/corepos/api.py b/corepos/api.py deleted file mode 100644 index a24b906..0000000 --- a/corepos/api.py +++ /dev/null @@ -1,578 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2024 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 -from requests.auth import HTTPDigestAuth - - -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. - - :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, - 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 - specified, ``method`` will be CORE's ``FannieEntity`` webservice. - """ - if not method: - method = 'FannieEntity' - if '\\' not in method: - method = r'\COREPOS\Fannie\API\webservices\{}'.format(method) - - 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 = self.session.post(self.url, data=json.dumps(payload), - verify=self.verify) - response.raise_for_status() - return 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 - 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']) - - # 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_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. - - :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. - - :returns: Either a member 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 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_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. - - :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. - - :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 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. - - :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 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. - - :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. - - .. 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 = { - 'entity': 'Vendors', - 'submethod': 'set', - 'columns': columns, - } - 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]) - - 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) - - 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, sku, vendorID, **columns): - """ - Fetch an existing VendorItem record from CORE. - - :returns: Either a vendor item dict record, or ``None``. - """ - columns['sku'] = sku - 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]) - - 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) diff --git a/corepos/db/__init__.py b/corepos/db/__init__.py index db5c837..174fee4 100644 --- a/corepos/db/__init__.py +++ b/corepos/db/__init__.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2021 Lance Edgar +# Copyright © 2018-2020 Lance Edgar # # This file is part of pyCOREPOS. # @@ -23,3 +23,12 @@ """ 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 * diff --git a/corepos/db/common/__init__.py b/corepos/db/common/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/corepos/db/common/op.py b/corepos/db/common/op.py deleted file mode 100644 index 30ecc1c..0000000 --- a/corepos/db/common/op.py +++ /dev/null @@ -1,173 +0,0 @@ -# -*- 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 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. - """ - 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() - - -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/common/trans.py b/corepos/db/common/trans.py deleted file mode 100644 index 9ec5601..0000000 --- a/corepos/db/common/trans.py +++ /dev/null @@ -1,114 +0,0 @@ -# -*- 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_no = sa.Column(sa.Integer(), nullable=True) - - # txn - 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) - - # cashier - emp_no = sa.Column(sa.Integer(), nullable=True) - - # customer - card_no = sa.Column(sa.Integer(), nullable=True) - memType = sa.Column(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 = sa.Column(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(sa.Numeric(precision=10, scale=2), nullable=True) - - total = sa.Column(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) - - foodstamp = sa.Column(sa.Boolean(), nullable=True) - - discount = sa.Column(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) - - discounttype = sa.Column(sa.Integer(), nullable=True) - - voided = sa.Column(sa.Integer(), nullable=True) - - percentDiscount = sa.Column(sa.Integer(), nullable=True) - - ItemQtty = sa.Column(sa.Float(), nullable=True) - - volDiscType = sa.Column(sa.Integer(), nullable=True) - - volume = sa.Column(sa.Integer(), nullable=True) - - VolSpecial = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True) - - mixMatch = sa.Column(sa.String(length=13), nullable=True) - - matched = sa.Column(sa.Boolean(), nullable=True) - - numflag = sa.Column(sa.Integer(), nullable=True, default=0) - - charflag = sa.Column(sa.String(length=2), nullable=True) - - def __str__(self): - txnid = '-'.join([str(val) for val in [self.register_no, - self.trans_no, - self.trans_id]]) - return f"{txnid} {self.description or ''}" diff --git a/corepos/db/lane_op/__init__.py b/corepos/db/lane_op/__init__.py deleted file mode 100644 index 884fe9f..0000000 --- a/corepos/db/lane_op/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- 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 deleted file mode 100644 index 456b1b8..0000000 --- a/corepos/db/lane_op/model.py +++ /dev/null @@ -1,167 +0,0 @@ -# -*- 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_op" DB -""" - -import sqlalchemy as sa -from sqlalchemy import orm - -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. - """ - __tablename__ = 'employees' - - -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(common.ProductBase, Base): - """ - Data model for ``products`` table. - """ - __tablename__ = 'products' - - -class CustomerClassic(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() - - -# TODO: deprecate / remove this -CustData = CustomerClassic diff --git a/corepos/db/lane_trans/__init__.py b/corepos/db/lane_trans/__init__.py deleted file mode 100644 index 8e8c706..0000000 --- a/corepos/db/lane_trans/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- 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 deleted file mode 100644 index f2245f5..0000000 --- a/corepos/db/lane_trans/model.py +++ /dev/null @@ -1,79 +0,0 @@ -# -*- 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 sqlalchemy.ext.declarative import declared_attr - -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): - """ - Data model for ``dtransactions`` table. - """ - __tablename__ = 'dtransactions' - - -class LocalTransBase(common.TransactionDetailBase): - """ - Base class for ``localtrans`` and similar models. - """ - - @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' diff --git a/corepos/db/office_arch/__init__.py b/corepos/db/office_arch/__init__.py deleted file mode 100644 index 70292e9..0000000 --- a/corepos/db/office_arch/__init__.py +++ /dev/null @@ -1,30 +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 . -# -################################################################################ -""" -"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 deleted file mode 100644 index bc5838f..0000000 --- a/corepos/db/office_arch/model.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- 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 . -# -################################################################################ -""" -CORE Office "arch" data model -""" - -import sqlalchemy as sa -from sqlalchemy import orm - -from corepos.db.common import trans as common -from corepos.db.office_trans.model import DTransactionBase - - -Base = orm.declarative_base() - - -class BigArchive(DTransactionBase, Base): - """ - Data model for ``bigArchive`` table. - """ - __tablename__ = 'bigArchive' - - -# TODO: deprecate / remove this -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): - """ - Data model for ``dlogBig`` view. - """ - __tablename__ = 'dlogBig' diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 60ad478..824ec07 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-2025 Lance Edgar +# Copyright © 2018-2020 Lance Edgar # # This file is part of pyCOREPOS. # @@ -21,244 +21,81 @@ # ################################################################################ """ -Data model for CORE POS "office_op" DB +CORE POS Data Model """ -import datetime -import logging +from __future__ import unicode_literals, absolute_import +import six import sqlalchemy as sa from sqlalchemy import orm +from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.associationproxy import association_proxy -from corepos.db.common import op as common + +Base = declarative_base() -log = logging.getLogger(__name__) - -Base = orm.declarative_base() - - -class StringableDateTime(sa.TypeDecorator): +@six.python_2_unicode_compatible +class Parameter(Base): """ - 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 Parameter(common.ParameterBase, Base): - """ - Data model for ``parameters`` table. + Represents a "parameter" value. """ __tablename__ = 'parameters' + store_id = sa.Column(sa.SmallInteger(), primary_key=True, nullable=False) -class TableSyncRule(Base): - """ - Represents a "table sync rule" value. - """ - __tablename__ = 'TableSyncRules' + lane_id = sa.Column(sa.SmallInteger(), primary_key=True, nullable=False) - # 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) + param_key = sa.Column(sa.String(length=100), primary_key=True, nullable=False) - table_name = sa.Column('tableName', sa.String(length=255), nullable=False, primary_key=True) + param_value = sa.Column(sa.String(length=255), nullable=True) - rule = sa.Column(sa.String(length=255), nullable=True) + is_array = sa.Column(sa.Boolean(), nullable=True) def __str__(self): - 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. - """ - __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. - """ - __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 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. - """ - __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_parents', - order_by=parent_id)) - - def __str__(self): - return "{} / {}".format(self.parent, self.child) + 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. """ __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_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') + tax = sa.Column('dept_tax', sa.Boolean(), nullable=True) 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) discount = sa.Column('dept_discount', sa.Boolean(), nullable=True) - see_id = sa.Column('dept_see_id', sa.SmallInteger(), nullable=True) + # TODO: probably should rename this attribute, but to what? + dept_see_id = sa.Column(sa.Boolean(), 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) + margin = sa.Column(sa.Float(), nullable=False, default=0) - sales_code = sa.Column('salesCode', sa.Integer(), nullable=False) + sales_code = sa.Column('salesCode', sa.Integer(), nullable=False, default=0) - member_only = sa.Column('memberOnly', sa.SmallInteger(), nullable=False) + member_only = sa.Column('memberOnly', sa.SmallInteger(), nullable=False, default=0) def __str__(self): return self.name or '' +@six.python_2_unicode_compatible class Subdepartment(Base): """ Represents a subdepartment within the organization. @@ -283,16 +120,14 @@ class Subdepartment(Base): return self.name or '' +@six.python_2_unicode_compatible class Vendor(Base): """ Represents a vendor from which product may be purchased. """ __tablename__ = 'vendors' - # 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') + id = sa.Column('vendorID', sa.Integer(), primary_key=True, autoincrement=False, nullable=False) name = sa.Column('vendorName', sa.String(length=50), nullable=True) @@ -349,266 +184,114 @@ 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 - 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) - - # 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): - """ - Represents a "component" of a tax rate. - """ - __tablename__ = 'TaxRateComponents' - __table_args__ = ( - sa.ForeignKeyConstraint(['taxRateID'], ['taxrates.id']), - ) - - 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') - - rate = sa.Column(sa.Float(), nullable=True) - - 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 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(common.ProductBase, Base): +@six.python_2_unicode_compatible +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(), 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) + + size = sa.Column(sa.String(length=9), nullable=True) + + tax = sa.Column(sa.SmallInteger(), nullable=True) + + foodstamp = sa.Column(sa.Boolean(), nullable=True) + + scale = sa.Column(sa.Boolean(), nullable=True) + + scale_price = sa.Column('scaleprice', sa.Boolean(), nullable=True, default=False) + + mix_match_code = sa.Column('mixmatchcode', sa.String(length=13), 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.Boolean(), nullable=True) + + cost = sa.Column(sa.Float(), nullable=True, default=0) + + in_use = sa.Column('inUse', sa.Boolean(), nullable=True) + + flags = sa.Column('numflag', sa.Integer(), nullable=True, default=0) + + 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=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 == Product.department_number', - foreign_keys='Product.department_number', + primaryjoin=Department.number == department_number, + foreign_keys=[department_number], doc=""" Reference to the :class:`Department` to which the product belongs. """) - tax_rate = orm.relationship(TaxRate) - - # advertised = sa.Column(sa.Boolean(), nullable=True) - - subdepartment = orm.relationship( - Subdepartment, - primaryjoin='Subdepartment.number == Product.subdepartment_number', - foreign_keys='Product.subdepartment_number', - doc=""" - Reference to the :class:`Subdepartment` to which the product belongs. - """) - - default_vendor = orm.relationship( + vendor = orm.relationship( Vendor, - primaryjoin='Vendor.id == Product.default_vendor_id', - foreign_keys='Product.default_vendor_id', + 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') - - like_code = association_proxy( - '_like_code', 'like_code', - 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'] @@ -616,59 +299,11 @@ class Product(common.ProductBase, 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 '' -class ProductLikeCode(Base): - """ - Represents the association between a product and like code. - """ - __tablename__ = 'upcLike' - __table_args__ = ( - sa.ForeignKeyConstraint(['upc'], ['products.upc']), - 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. - """)) - - +@six.python_2_unicode_compatible class ProductFlag(Base): """ Represents a product flag attribute. @@ -685,246 +320,38 @@ class ProductFlag(Base): return self.description or '' -class ProductUser(Base): +@six.python_2_unicode_compatible +class Employee(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, - cascade='all, delete-orphan', - 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) - - # 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) - - def __str__(self): - return str(self.product 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, - back_populates='vendor_items', - primaryjoin=Product.upc == upc, - foreign_keys=[upc], - doc=""" - Reference to the :class:`Product` to which this record applies. - """) - - 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) - - def __str__(self): - return "{} from {}".format(self.sku, self.vendor) - - -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) - - 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: the following 3 columns are not in some older DBs; maybe need to - # figure out a "simple" way to conditionally include them? - - linked_plu = sa.Column('linkedPLU', sa.String(length=13), 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): - """ - 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(common.EmployeeBase, Base): - """ - Data model for ``employees`` table. + Represents an employee within the organization. """ __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() + + +@six.python_2_unicode_compatible class MemberType(Base): """ Represents a type of membership within the organization. @@ -943,146 +370,20 @@ class MemberType(Base): ssi = sa.Column(sa.Boolean(), nullable=True) - # nb. this must be added explicitly if DB is new enough - #ignore_sales = sa.Column('ignoreSales', sa.Boolean(), nullable=True, default=False) - - # nb. this must be added explicitly if DB is new enough - #sales_code = sa.Column('salesCode', sa.Integer(), nullable=True) + # 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) def __str__(self): 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) - 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) - - 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) - - +@six.python_2_unicode_compatible 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 CustomerClassic(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' - __table_args__ = ( - sa.ForeignKeyConstraint(['memType'], ['memtype.memtype']), - ) id = sa.Column(sa.Integer(), primary_key=True, autoincrement=True, nullable=False) @@ -1094,15 +395,15 @@ class CustomerClassic(Base): last_name = sa.Column('LastName', sa.String(length=30), nullable=True) - cash_back = sa.Column('CashBack', sa.Numeric(precision=10, scale=2), nullable=False, default=60) + cash_back = sa.Column('CashBack', sa.Float(), nullable=False, default=60) - balance = sa.Column('Balance', sa.Numeric(precision=10, scale=2), nullable=False, default=0) + balance = sa.Column('Balance', sa.Float(), nullable=False, default=0) discount = sa.Column('Discount', sa.SmallInteger(), nullable=True) - member_discount_limit = sa.Column('MemDiscountLimit', sa.Numeric(precision=10, scale=2), nullable=False, default=0) + member_discount_limit = sa.Column('MemDiscountLimit', sa.Float(), nullable=False, default=0) - charge_limit = sa.Column('ChargeLimit', sa.Numeric(precision=10, scale=2), nullable=False, default=0) + charge_limit = sa.Column('ChargeLimit', sa.Float(), nullable=False, default=0) charge_ok = sa.Column('ChargeOk', sa.Boolean(), nullable=False, default=False) @@ -1125,7 +426,7 @@ class CustomerClassic(Base): ssi = sa.Column('SSI', sa.Boolean(), nullable=False, default=False) - purchases = sa.Column('Purchases', sa.Numeric(precision=10, scale=2), nullable=False, default=0) + purchases = sa.Column('Purchases', sa.Float(), nullable=False, default=0) number_of_checks = sa.Column('NumberOfChecks', sa.SmallInteger(), nullable=False, default=0) @@ -1139,7 +440,7 @@ class CustomerClassic(Base): member_info = orm.relationship( 'MemberInfo', - primaryjoin='MemberInfo.card_number == CustomerClassic.card_number', + primaryjoin='MemberInfo.card_number == Customer.card_number', foreign_keys=[card_number], uselist=False, back_populates='customers', @@ -1151,10 +452,7 @@ class CustomerClassic(Base): return "{} {}".format(self.first_name or '', self.last_name or '').strip() -# TODO: deprecate / remove this -CustData = CustomerClassic - - +@six.python_2_unicode_compatible class MemberInfo(Base): """ Contact info regarding a member of the organization. @@ -1163,14 +461,14 @@ class MemberInfo(Base): card_number = sa.Column('card_no', sa.Integer(), primary_key=True, autoincrement=False, nullable=False) - first_name = sa.Column(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) + first_name = sa.Column(sa.String(length=30), nullable=True) other_last_name = sa.Column('othlast_name', sa.String(length=30), nullable=True) + other_first_name = sa.Column('othfirst_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) @@ -1183,22 +481,18 @@ 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, doc=""" - NB. this is labeled "Alt. Phone" in CORE Office member view, and - is named `altPhone` when dealing with CORE Office webservices API. - """) + email2 = sa.Column('email_2', sa.String(length=50), nullable=True) ads_ok = sa.Column('ads_OK', sa.Boolean(), nullable=True, default=True) customers = orm.relationship( - CustomerClassic, - primaryjoin=CustomerClassic.card_number == card_number, - order_by=CustomerClassic.person_number, - foreign_keys=[CustomerClassic.card_number], + Customer, + primaryjoin=Customer.card_number == card_number, + foreign_keys=[Customer.card_number], back_populates='member_info', - remote_side=CustomerClassic.card_number, + remote_side=Customer.card_number, doc=""" - List of :class:`CustomerClassic` instances which are associated with this member info. + List of :class:`Customer` instances which are associated with this member info. """) dates = orm.relationship( @@ -1206,7 +500,6 @@ 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. """, @@ -1216,79 +509,15 @@ 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', - 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. - """)) - - 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() def __str__(self): - name = self.full_name - if name: - return name - return "Member Info #{}".format(self.card_number) - - 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) + return self.full_name +@six.python_2_unicode_compatible class MemberDate(Base): """ Join/exit dates for members @@ -1307,9 +536,10 @@ class MemberDate(Base): self.end_date.date() if self.end_date else "??") +@six.python_2_unicode_compatible class MemberContact(Base): """ - Member contacts + Contact preferences for members """ __tablename__ = 'memContact' @@ -1336,123 +566,7 @@ 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. - """ - __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. - """ - __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 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. - """ - __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) - - def __str__(self): - return "#{} on {}".format(self.card_number, - self.suspension_date) - - +@six.python_2_unicode_compatible class HouseCoupon(Base): """ Represents a "house" (store) coupon. @@ -1492,275 +606,3 @@ class HouseCoupon(Base): def __str__(self): 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 CustomReceiptLine(Base): - """ - Represents a "text string" line for a custom receipt. - """ - __tablename__ = 'customReceipt' - - # 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): - return self.text or "" - - -class BatchType(Base): - """ - Represents the definition of a batch type. - """ - __tablename__ = 'batchType' - - # 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) - - 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.SmallInteger(), 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' - - 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(), - sa.ForeignKey('batchType.batchTypeID'), 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' - - id = sa.Column('listID', sa.Integer(), primary_key=True, autoincrement=True, nullable=False) - - 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) - - 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 "" - - -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 "" - - -# 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 diff --git a/corepos/db/office_trans/model.py b/corepos/db/office_trans/model.py index c2b0959..55aff86 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-2025 Lance Edgar +# Copyright © 2018-2020 Lance Edgar # # This file is part of pyCOREPOS. # @@ -24,68 +24,102 @@ CORE POS Transaction Data Model """ +from __future__ import unicode_literals, absolute_import + +import six import sqlalchemy as sa -from sqlalchemy import orm - -from corepos.db.common import trans as common +from sqlalchemy.ext.declarative import declarative_base -Base = orm.declarative_base() +Base = declarative_base() -# TODO: not sure what primary key should be for this? am trying a -# 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): +@six.python_2_unicode_compatible +class TransactionDetail(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, 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, primary_key=True, autoincrement=False) - - 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 DTransactionBase(common.TransactionDetailBase): - """ - Base class for ``dtransactions`` and similar models. - """ - store_row_id = sa.Column(sa.Integer(), primary_key=True, nullable=False) - - pos_row_id = sa.Column(sa.Integer(), nullable=True) - store_id = sa.Column(sa.Integer(), nullable=True, default=0) - date_time = sa.Column('datetime', sa.DateTime(), nullable=True) - - -class DTransaction(DTransactionBase, Base): - """ - Data model for ``dtransactions`` table. + Represents a POS transaction detail record. """ __tablename__ = 'dtransactions' + # store + store_row_id = sa.Column(sa.Integer(), primary_key=True, nullable=False) + store_id = sa.Column(sa.Integer(), nullable=True, default=0) -# TODO: deprecate / remove this -TransactionDetail = DTransaction + # 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) + transaction_status = sa.Column('trans_status', sa.String(length=1), nullable=True) + + # timestamps + date_time = sa.Column('datetime', sa.DateTime(), nullable=True) + + # 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) + + unit_price = sa.Column('unitPrice', 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) + + tax = sa.Column(sa.Boolean(), nullable=True) + + 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.Boolean(), 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/office_trans_archive/__init__.py b/corepos/db/office_trans_archive/__init__.py deleted file mode 100644 index 6ddb31d..0000000 --- a/corepos/db/office_trans_archive/__init__.py +++ /dev/null @@ -1,32 +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 . -# -################################################################################ -""" -"Archive" Transaction Database Interface -""" - -import warnings -warnings.warn("The `corepos.db.office_trans_archive` module is deprecated! " - "Please use `corepos.db.office_arch` instead.", - DeprecationWarning, stacklevel=2) - -from corepos.db.office_arch import * diff --git a/corepos/db/office_trans_archive/model.py b/corepos/db/office_trans_archive/model.py deleted file mode 100644 index e57d071..0000000 --- a/corepos/db/office_trans_archive/model.py +++ /dev/null @@ -1,32 +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 . -# -################################################################################ -""" -CORE POS Transaction Data Model -""" - -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_arch.model import * diff --git a/corepos/db/util.py b/corepos/db/util.py index f16da72..582aa17 100644 --- a/corepos/db/util.py +++ b/corepos/db/util.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2020 Lance Edgar +# Copyright © 2018-2019 Lance Edgar # # This file is part of pyCOREPOS. # @@ -24,9 +24,11 @@ CORE POS Database Utilities """ +from __future__ import unicode_literals, absolute_import + import sqlalchemy as sa -from corepos.db.office_op import model as corepos +from corepos.db import model as corepos def get_last_card_number(session): @@ -36,25 +38,3 @@ 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 diff --git a/corepos/enum.py b/corepos/enum.py index 93780c3..62c16ac 100644 --- a/corepos/enum.py +++ b/corepos/enum.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2024 Lance Edgar +# Copyright © 2018-2019 Lance Edgar # # This file is part of pyCOREPOS. # @@ -24,44 +24,12 @@ CORE POS enumeration constants """ -from collections import OrderedDict -from enum import Enum +from __future__ import unicode_literals, absolute_import - -class CoreDbType(str, Enum): - office_op = 'office_op' - office_trans = 'office_trans' - 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, "None (Change regular price)"), - (BATCH_DISCOUNT_TYPE_SALE_EVERYONE, "Sale for everyone"), - (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"), -]) +try: + from collections import OrderedDict +except ImportError: + from ordereddict import OrderedDict HOUSE_COUPON_MEMBER_ONLY_NO = 0 @@ -124,14 +92,3 @@ 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"), -]) diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index cf50f41..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,49 +0,0 @@ - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - - -[project] -name = "pyCOREPOS" -version = "0.5.1" -description = "Python Interface to CORE POS" -readme = "README.md" -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", - "SQLAlchemy>=1.4", -] - - -[project.urls] -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] -version_provider = "pep621" -tag_format = "v$version" -update_changelog_on_bump = true - - -[tool.hatch.build.targets.wheel] -packages = ["corepos"] diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d9afeec --- /dev/null +++ b/setup.py @@ -0,0 +1,96 @@ +# -*- 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 . +# +################################################################################ + +import os +import sys +from setuptools import setup, find_packages + + +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 + 'six', # 1.12.0 + 'SQLAlchemy', # 0.9.8 +] + + +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', + 'Programming Language :: Python :: 3.5', + 'Topic :: Office/Business', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + + install_requires = requires, + packages = find_packages(), +) diff --git a/tasks.py b/tasks.py index 6946a7f..38d2267 100644 --- a/tasks.py +++ b/tasks.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2024 Lance Edgar +# Copyright © 2018 Lance Edgar # # This file is part of pyCOREPOS. # @@ -24,33 +24,17 @@ Tasks for 'pyCOREPOS' package """ -import os -import re +from __future__ import unicode_literals, absolute_import + import shutil from invoke import task -here = os.path.abspath(os.path.dirname(__file__)) -__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 -def release(c): +def release(ctx): """ Release a new version of 'pyCOREPOS'. """ - 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__)) + shutil.rmtree('pyCOREPOS.egg-info') + ctx.run('python setup.py sdist --formats=gztar upload')