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..20d31e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,129 +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. 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/_version.py b/corepos/_version.py index 555fee3..862eaaa 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.11' diff --git a/corepos/api.py b/corepos/api.py index a24b906..c93ff9d 100644 --- a/corepos/api.py +++ b/corepos/api.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2024 Lance Edgar +# Copyright © 2018-2023 Lance Edgar # # This file is part of pyCOREPOS. # @@ -132,22 +132,6 @@ class CoreWebAPI(object): assert set(js['result'].keys()) == set(['result']) return js['result']['result'] - def get_member_types(self): - """ - Fetch all Member Type records from CORE. - - :returns: A (potentially empty) list of member type dict records. - """ - params = { - 'entity': 'Memtype', - 'submethod': 'get', - 'columns': {}, - } - - response = self.post(params) - result = self.parse_response(response) - return [json.loads(rec) for rec in result] - def get_members(self): """ Fetch all Member records from CORE. @@ -198,40 +182,6 @@ class CoreWebAPI(object): if result: return result - def get_employees(self, **columns): - """ - Fetch some or all of Employee records from CORE. - - :returns: A (potentially empty) list of employee dict records. - """ - params = { - 'entity': 'Employees', - 'submethod': 'get', - 'columns': columns, - } - response = self.post(params) - result = self.parse_response(response) - return [json.loads(rec) for rec in result] - - def get_employee(self, emp_no, **columns): - """ - Fetch an existing Employee record from CORE. - - :returns: Either a employee dict record, or ``None``. - """ - columns['emp_no'] = emp_no - params = { - 'entity': 'Employees', - 'submethod': 'get', - 'columns': columns, - } - response = self.post(params) - result = self.parse_response(response) - if result: - if len(result) > 1: - log.warning("CORE API returned %s employee results", len(result)) - return json.loads(result[0]) - def get_stores(self, **columns): """ Fetch some or all of Store records from CORE. 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/model.py b/corepos/db/lane_op/model.py index 456b1b8..1c687ab 100644 --- a/corepos/db/lane_op/model.py +++ b/corepos/db/lane_op/model.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2025 Lance Edgar +# Copyright © 2018-2023 Lance Edgar # # This file is part of pyCOREPOS. # @@ -27,26 +27,10 @@ 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. @@ -88,14 +72,144 @@ class Department(Base): return self.name or "" -class Product(common.ProductBase, Base): +class Product(Base): """ - Data model for ``products`` table. + Represents a product, purchased and/or sold by the organization. """ __tablename__ = 'products' + # __table_args__ = ( + # sa.ForeignKeyConstraint(['department'], ['departments.dept_no']), + # sa.ForeignKeyConstraint(['subdept'], ['subdepts.subdept_no']), + # sa.ForeignKeyConstraint(['tax'], ['taxrates.id']), + # ) + + id = sa.Column(sa.Integer(), nullable=False, + primary_key=True, autoincrement=True) + + upc = sa.Column(sa.String(length=13), nullable=True) + + description = sa.Column(sa.String(length=30), nullable=True) + + brand = sa.Column(sa.String(length=30), nullable=True) + + formatted_name = sa.Column(sa.String(length=30), nullable=True) + + normal_price = sa.Column(sa.Float(), nullable=True) + + price_method = sa.Column('pricemethod', sa.SmallInteger(), nullable=True) + + group_price = sa.Column('groupprice', sa.Float(), nullable=True) + + quantity = sa.Column(sa.SmallInteger(), nullable=True) + + special_price = sa.Column(sa.Float(), nullable=True) + + special_price_method = sa.Column('specialpricemethod', sa.SmallInteger(), nullable=True) + + special_group_price = sa.Column('specialgroupprice', sa.Float(), nullable=True) + + special_quantity = sa.Column('specialquantity', sa.SmallInteger(), nullable=True) + + special_limit = sa.Column(sa.SmallInteger(), nullable=True) + + start_date = sa.Column(sa.DateTime(), nullable=True) + + end_date = sa.Column(sa.DateTime(), nullable=True) + + department_number = sa.Column('department', sa.SmallInteger(), nullable=True) + # department = orm.relationship( + # Department, + # primaryjoin=Department.number == department_number, + # foreign_keys=[department_number], + # doc=""" + # Reference to the :class:`Department` to which the product belongs. + # """) + + size = sa.Column(sa.String(length=9), nullable=True) + + tax_rate_id = sa.Column('tax', sa.SmallInteger(), nullable=True) + # tax_rate = orm.relationship(TaxRate) + + foodstamp = sa.Column(sa.Boolean(), nullable=True) + + scale = sa.Column(sa.Boolean(), nullable=True) + + scale_price = sa.Column('scaleprice', sa.Float(), nullable=True) + + mix_match_code = sa.Column('mixmatchcode', sa.String(length=13), nullable=True) + + created = sa.Column(sa.DateTime(), nullable=True) + + modified = sa.Column(sa.DateTime(), nullable=True) + + # TODO: what to do about this 'replaces' thing? + # 'batchID'=>array('type'=>'TINYINT', 'replaces'=>'advertised'), + # batch_id = sa.Column('batchID', sa.SmallInteger(), nullable=True) + # advertised = sa.Column(sa.Boolean(), nullable=True) + + tare_weight = sa.Column('tareweight', sa.Float(), nullable=True) + + discount = sa.Column(sa.SmallInteger(), nullable=True) + + discount_type = sa.Column('discounttype', sa.SmallInteger(), nullable=True) + + line_item_discountable = sa.Column(sa.Boolean(), nullable=True) + + unit_of_measure = sa.Column('unitofmeasure', sa.String(length=15), nullable=True) + + wicable = sa.Column(sa.SmallInteger(), nullable=True) + + quantity_enforced = sa.Column('qttyEnforced', sa.Boolean(), nullable=True) + + id_enforced = sa.Column('idEnforced', sa.SmallInteger(), nullable=True) + + cost = sa.Column(sa.Float(), nullable=True) + + special_cost = sa.Column(sa.Float(), nullable=True) + + received_cost = sa.Column(sa.Float(), nullable=True) + + in_use = sa.Column('inUse', sa.Boolean(), nullable=True) + + flags = sa.Column('numflag', sa.Integer(), nullable=True) + + subdepartment_number = sa.Column('subdept', sa.SmallInteger(), nullable=True) + # subdepartment = orm.relationship( + # Subdepartment, + # primaryjoin=Subdepartment.number == subdepartment_number, + # foreign_keys=[subdepartment_number], + # doc=""" + # Reference to the :class:`Subdepartment` to which the product belongs. + # """) + + deposit = sa.Column(sa.Float(), nullable=True) + + local = sa.Column(sa.Integer(), nullable=True, + default=0) # TODO: do we want a default here? + + store_id = sa.Column(sa.SmallInteger(), nullable=True) + + default_vendor_id = sa.Column(sa.Integer(), nullable=True) + # default_vendor = orm.relationship( + # Vendor, + # primaryjoin=Vendor.id == default_vendor_id, + # foreign_keys=[default_vendor_id], + # doc=""" + # Reference to the default :class:`Vendor` from which the product is obtained. + # """) + + current_origin_id = sa.Column(sa.Integer(), nullable=True) + + auto_par = sa.Column(sa.Float(), nullable=True, + default=0) # TODO: do we want a default here? + + price_rule_id = sa.Column(sa.Integer(), nullable=True) + + # TODO: some older DB's might not have this? guess we'll see + last_sold = sa.Column(sa.DateTime(), nullable=True) -class CustomerClassic(Base): +class CustData(Base): """ Represents a customer of the organization. @@ -161,7 +275,3 @@ class CustomerClassic(Base): def __str__(self): return "{} {}".format(self.first_name or '', self.last_name or '').strip() - - -# TODO: deprecate / remove this -CustData = CustomerClassic diff --git a/corepos/db/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/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..132fd33 100644 --- a/corepos/db/office_op/model.py +++ b/corepos/db/office_op/model.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2025 Lance Edgar +# Copyright © 2018-2023 Lance Edgar # # This file is part of pyCOREPOS. # @@ -31,8 +31,6 @@ import sqlalchemy as sa from sqlalchemy import orm from sqlalchemy.ext.associationproxy import association_proxy -from corepos.db.common import op as common - log = logging.getLogger(__name__) @@ -56,12 +54,42 @@ class StringableDateTime(sa.TypeDecorator): raise NotImplementedError -class Parameter(common.ParameterBase, Base): +class Change(Base): """ - Data model for ``parameters`` table. + Represents a changed (or deleted) record, which is pending synchronization + to another system(s). + + .. note:: + This table may or may not be installed to a given CORE Office Op DB. Its + presence is required if Rattail datasync needs to "watch" the DB. + """ + __tablename__ = 'datasync_changes' + + id = sa.Column(sa.Integer(), nullable=False, primary_key=True) + object_type = sa.Column(sa.String(length=255), nullable=False) + object_key = sa.Column(sa.String(length=255), nullable=False) + deleted = sa.Column(sa.Boolean(), nullable=False, default=False) + + +class Parameter(Base): + """ + Represents a "parameter" value. """ __tablename__ = 'parameters' + store_id = sa.Column(sa.SmallInteger(), primary_key=True, nullable=False) + + lane_id = sa.Column(sa.SmallInteger(), primary_key=True, nullable=False) + + param_key = sa.Column(sa.String(length=100), primary_key=True, nullable=False) + + param_value = sa.Column(sa.String(length=255), nullable=True) + + is_array = sa.Column(sa.Boolean(), nullable=True) + + def __str__(self): + return "{}-{} {}".format(self.store_id, self.lane_id, self.param_key) + class TableSyncRule(Base): """ @@ -69,9 +97,7 @@ class TableSyncRule(Base): """ __tablename__ = 'TableSyncRules' - # 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) + id = sa.Column('tableSyncRuleID', sa.Integer(), nullable=False, primary_key=True, autoincrement=True) table_name = sa.Column('tableName', sa.String(length=255), nullable=False, primary_key=True) @@ -160,25 +186,6 @@ class Store(Base): return self.description or "" -class MasterSuperDepartment(Base): - """ - A department may belong to more than one superdepartment, but has - one "master" superdepartment. This avoids duplicating rows in - some reports. By convention, a department's "master" - superdepartment is the one with the lowest superID. - """ - __tablename__ = 'MasterSuperDepts' - - super_id = sa.Column('superID', sa.Integer(), primary_key=True, autoincrement=False, nullable=False) - - department_id = sa.Column('dept_ID', sa.Integer(), primary_key=True, autoincrement=False, nullable=False) - - super_name = sa.Column(sa.String(length=50), nullable=True) - - def __str__(self): - return self.super_name or "" - - class SuperDepartment(Base): """ Represents a "super" (parent/child) department mapping. @@ -218,25 +225,15 @@ 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) @@ -536,7 +533,7 @@ class Origin(Base): return self.name or self.short_name or "" -class Product(common.ProductBase, Base): +class Product(Base): """ Represents a product, purchased and/or sold by the organization. """ @@ -547,68 +544,125 @@ class Product(common.ProductBase, Base): sa.ForeignKeyConstraint(['tax'], ['taxrates.id']), ) + id = sa.Column(sa.Integer(), primary_key=True, autoincrement=True, nullable=False) + + upc = sa.Column(sa.String(length=13), nullable=True) + + description = sa.Column(sa.String(length=30), nullable=True) + + brand = sa.Column(sa.String(length=30), nullable=True) + + formatted_name = sa.Column(sa.String(length=30), nullable=True) + + normal_price = sa.Column(sa.Float(), nullable=True) + + price_method = sa.Column('pricemethod', sa.SmallInteger(), nullable=True) + + group_price = sa.Column('groupprice', sa.Float(), nullable=True) + + quantity = sa.Column(sa.SmallInteger(), nullable=True) + + special_price = sa.Column(sa.Float(), nullable=True) + + special_price_method = sa.Column('specialpricemethod', sa.SmallInteger(), nullable=True) + + special_group_price = sa.Column('specialgroupprice', sa.Float(), nullable=True) + + special_quantity = sa.Column('specialquantity', sa.SmallInteger(), nullable=True) + + start_date = sa.Column(sa.DateTime(), nullable=True) + + end_date = sa.Column(sa.DateTime(), nullable=True) + + department_number = sa.Column('department', sa.SmallInteger(), nullable=True) department = orm.relationship( Department, - primaryjoin='Department.number == 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. """) + size = sa.Column(sa.String(length=9), nullable=True) + + tax_rate_id = sa.Column('tax', sa.SmallInteger(), nullable=True) tax_rate = orm.relationship(TaxRate) + foodstamp = sa.Column(sa.Boolean(), nullable=True) + + scale = sa.Column(sa.Boolean(), nullable=True) + + # TODO: yikes, did i just code this all wrong the first time? pretty sure + # this needs to change to a decimal column... + scale_price = sa.Column('scaleprice', sa.Boolean(), nullable=True) + # scale_price = sa.Column('scaleprice', sa.Numeric(precision=10, scale=2), nullable=True) + + mix_match_code = sa.Column('mixmatchcode', sa.String(length=13), nullable=True) + + created = sa.Column(StringableDateTime(), nullable=True) + + modified = sa.Column(sa.DateTime(), nullable=True) + # advertised = sa.Column(sa.Boolean(), nullable=True) + tare_weight = sa.Column('tareweight', sa.Float(), nullable=True) + + discount = sa.Column(sa.SmallInteger(), nullable=True) + + discount_type = sa.Column('discounttype', sa.SmallInteger(), nullable=True) + + line_item_discountable = sa.Column(sa.Boolean(), nullable=True) + + unit_of_measure = sa.Column('unitofmeasure', sa.String(length=15), nullable=True) + + wicable = sa.Column(sa.SmallInteger(), nullable=True) + + quantity_enforced = sa.Column('qttyEnforced', sa.Boolean(), nullable=True) + + id_enforced = sa.Column('idEnforced', sa.SmallInteger(), nullable=True) + + cost = sa.Column(sa.Float(), nullable=True) + + in_use = sa.Column('inUse', sa.Boolean(), nullable=True) + + flags = sa.Column('numflag', sa.Integer(), nullable=True) + + subdepartment_number = sa.Column('subdept', sa.SmallInteger(), nullable=True) subdepartment = orm.relationship( Subdepartment, - primaryjoin='Subdepartment.number == Product.subdepartment_number', - foreign_keys='Product.subdepartment_number', + primaryjoin=Subdepartment.number == subdepartment_number, + foreign_keys=[subdepartment_number], doc=""" Reference to the :class:`Subdepartment` to which the product belongs. """) + deposit = sa.Column(sa.Float(), nullable=True) + + local = sa.Column(sa.Integer(), nullable=True) + + store_id = sa.Column(sa.SmallInteger(), nullable=True) + + default_vendor_id = sa.Column(sa.Integer(), nullable=True) default_vendor = orm.relationship( Vendor, - primaryjoin='Vendor.id == 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') + current_origin_id = sa.Column(sa.Integer(), nullable=True) + + # TODO: some older DB's might not have this? guess we'll see + last_sold = sa.Column(sa.DateTime(), nullable=True) + like_code = association_proxy( '_like_code', 'like_code', creator=lambda lc: ProductLikeCode(like_code=lc), ) - 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'] @@ -758,12 +812,17 @@ class VendorItem(Base): upc = sa.Column(sa.String(length=13), nullable=False) product = orm.relationship( Product, - back_populates='vendor_items', primaryjoin=Product.upc == upc, foreign_keys=[upc], doc=""" Reference to the :class:`Product` to which this record applies. - """) + """, + backref=orm.backref( + 'vendor_items', + order_by=vendor_item_id, + doc=""" + List of :class:`VendorItem` records for this product. + """)) brand = sa.Column(sa.String(length=50), nullable=True) @@ -918,12 +977,35 @@ class ProductPhysicalLocation(Base): location = sa.Column(sa.SmallInteger(), nullable=True, default=0) -class Employee(common.EmployeeBase, Base): +class Employee(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() + class MemberType(Base): """ @@ -943,11 +1025,9 @@ 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 "" @@ -1073,7 +1153,7 @@ class Customer(Base): return "{} {}".format(self.first_name or '', self.last_name or '').strip() -class CustomerClassic(Base): +class CustData(Base): """ Represents a customer of the organization. @@ -1139,7 +1219,7 @@ class CustomerClassic(Base): member_info = orm.relationship( 'MemberInfo', - primaryjoin='MemberInfo.card_number == CustomerClassic.card_number', + primaryjoin='MemberInfo.card_number == CustData.card_number', foreign_keys=[card_number], uselist=False, back_populates='customers', @@ -1151,10 +1231,6 @@ class CustomerClassic(Base): return "{} {}".format(self.first_name or '', self.last_name or '').strip() -# TODO: deprecate / remove this -CustData = CustomerClassic - - class MemberInfo(Base): """ Contact info regarding a member of the organization. @@ -1163,14 +1239,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 +1259,19 @@ 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], + CustData, + primaryjoin=CustData.card_number == card_number, + order_by=CustData.person_number, + foreign_keys=[CustData.card_number], back_populates='member_info', - remote_side=CustomerClassic.card_number, + remote_side=CustData.card_number, doc=""" - List of :class:`CustomerClassic` instances which are associated with this member info. + List of :class:`CustData` instances which are associated with this member info. """) dates = orm.relationship( @@ -1206,7 +1279,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. """, @@ -1309,7 +1381,7 @@ class MemberDate(Base): class MemberContact(Base): """ - Member contacts + Contact preferences for members """ __tablename__ = 'memContact' @@ -1336,19 +1408,6 @@ 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. @@ -1384,26 +1443,6 @@ class MemberNote(Base): return self.note or "" -class CustomerNotification(Base): - """ - Represents a customer notification for display at the lane. - - https://github.com/CORE-POS/IS4C/blob/master/fannie/classlib2.0/data/models/op/CustomerNotificationsModel.php - """ - __tablename__ = 'CustomerNotifications' - - id = sa.Column('customerNotificationID', sa.Integer(), primary_key=True, autoincrement=True, nullable=False) - card_number = sa.Column('cardNo', sa.Integer(), nullable=True) - customer_id = sa.Column('customerID', sa.Integer(), nullable=True) - source = sa.Column(sa.String(length=50), nullable=True) - type = sa.Column(sa.String(length=50), nullable=True) - message = sa.Column(sa.String(length=255), nullable=True) - modifier_module = sa.Column('modifierModule', sa.String(length=50), nullable=True) - - def __str__(self): - return self.message or '' - - class ReasonCode(Base): """ Reason codes for legacy account suspensions. @@ -1494,54 +1533,13 @@ class HouseCoupon(Base): return self.description or '' -class Tender(Base): - """ - Represents a tender for payment at POS - """ - __tablename__ = 'tenders' - - tender_id = sa.Column('TenderID', sa.Integer(), primary_key=True, nullable=False) - - tender_code = sa.Column('TenderCode', sa.String(length=2), nullable=True) - tender_name = sa.Column('TenderName', sa.String(length=25), nullable=True) - tender_type = sa.Column('TenderType', sa.String(length=2), nullable=True) - change_message = sa.Column('ChangeMessage', sa.String(length=25), nullable=True) - min_amount = sa.Column('MinAmount', sa.Numeric(precision=10, scale=2), nullable=True) - max_amount = sa.Column('MaxAmount', sa.Numeric(precision=10, scale=2), nullable=True) - max_refund = sa.Column('MaxRefund', sa.Numeric(precision=10, scale=2), nullable=True) - tender_module = sa.Column('TenderModule', sa.String(length=50), nullable=True) - sales_code = sa.Column('SalesCode', sa.Integer(), nullable=True) - - def __str__(self): - return self.tender_name or '' - - -class 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) + id = sa.Column('batchTypeID', sa.Integer(), primary_key=True, autoincrement=False, nullable=False) description = sa.Column('typeDesc', sa.String(length=50), nullable=True) @@ -1551,7 +1549,7 @@ class BatchType(Base): special_order_eligible = sa.Column('specialOrderEligible', sa.Boolean(), nullable=True, default=True) - editor_ui = sa.Column('editorUI', sa.SmallInteger(), nullable=True, default=True) + editor_ui = sa.Column('editorUI', sa.Boolean(), nullable=True, default=True) allow_single_store = sa.Column('allowSingleStore', sa.Boolean(), nullable=True, default=False) @@ -1566,6 +1564,9 @@ class Batch(Base): Represents a batch. """ __tablename__ = 'batches' + __table_args__ = ( + sa.ForeignKeyConstraint(['batchType'], ['batchType.batchTypeID']), + ) id = sa.Column('batchID', sa.Integer(), primary_key=True, autoincrement=True, nullable=False) @@ -1575,8 +1576,7 @@ class Batch(Base): 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_id = sa.Column('batchType', sa.Integer(), nullable=True) batch_type = orm.relationship(BatchType) discount_type = sa.Column('discountType', sa.Integer(), nullable=True) @@ -1598,18 +1598,16 @@ class BatchItem(Base): Represents a batch "list" item. """ __tablename__ = 'batchList' + __table_args__ = ( + sa.ForeignKeyConstraint(['batchID'], ['batches.batchID']), + ) id = sa.Column('listID', sa.Integer(), primary_key=True, autoincrement=True, nullable=False) - batch_id = sa.Column('batchID', sa.Integer(), - sa.ForeignKey('batches.batchID'), nullable=True) + batch_id = sa.Column('batchID', sa.Integer(), nullable=True) batch = orm.relationship(Batch, backref=orm.backref('items')) upc = sa.Column(sa.String(length=13), nullable=True) - product = orm.relationship( - Product, - primaryjoin=Product.upc == upc, - foreign_keys=[upc]) sale_price = sa.Column('salePrice', sa.Numeric(precision=9, scale=3), nullable=True) @@ -1745,22 +1743,3 @@ class PurchaseOrderNote(Base): def __str__(self): return self.notes or "" - - -# the rest of this is a workaround to deal with the fact that some -# CORE databases have columns which others do not. i had assumed that -# all would be more or less the same but not so in practice. so if -# your DB *does* have these columns, you must invoke the function -# below in order to merge them into your schema. you should do this -# on app startup and they'll be available normally from then on. - -RUNTIME = {'added_latest_columns': False} - -def use_latest_columns(): - if RUNTIME['added_latest_columns']: - return - - MemberType.ignore_sales = sa.Column('ignoreSales', sa.Boolean(), nullable=True, default=False) - MemberType.sales_code = sa.Column('salesCode', sa.Integer(), nullable=True) - - RUNTIME['added_latest_columns'] = True diff --git a/corepos/db/office_trans/model.py b/corepos/db/office_trans/model.py index c2b0959..20c244c 100644 --- a/corepos/db/office_trans/model.py +++ b/corepos/db/office_trans/model.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2025 Lance Edgar +# Copyright © 2018-2023 Lance Edgar # # This file is part of pyCOREPOS. # @@ -27,65 +27,101 @@ CORE POS Transaction Data Model import sqlalchemy as sa from sqlalchemy import orm -from corepos.db.common import trans as common - Base = orm.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): +class TransactionDetailBase(object): """ - Represents a member equity payment. + Represents a POS transaction detail record. """ - __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 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) + + # 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) -class DTransaction(DTransactionBase, Base): + # 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 '' + + +class TransactionDetail(TransactionDetailBase, Base): """ - Data model for ``dtransactions`` table. + Represents a POS transaction detail record. """ __tablename__ = 'dtransactions' - - -# TODO: deprecate / remove this -TransactionDetail = DTransaction diff --git a/corepos/db/office_trans_archive/__init__.py b/corepos/db/office_trans_archive/__init__.py index 6ddb31d..e25b200 100644 --- a/corepos/db/office_trans_archive/__init__.py +++ b/corepos/db/office_trans_archive/__init__.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2023 Lance Edgar +# Copyright © 2018-2022 Lance Edgar # # This file is part of pyCOREPOS. # @@ -24,9 +24,7 @@ "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 sqlalchemy import orm -from corepos.db.office_arch import * + +Session = orm.sessionmaker() diff --git a/corepos/db/office_trans_archive/model.py b/corepos/db/office_trans_archive/model.py index e57d071..8f4a079 100644 --- a/corepos/db/office_trans_archive/model.py +++ b/corepos/db/office_trans_archive/model.py @@ -24,9 +24,16 @@ 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 sqlalchemy import orm -from corepos.db.office_arch.model import * +from corepos.db.office_trans.model import TransactionDetailBase + + +Base = orm.declarative_base() + + +class TransactionDetail(TransactionDetailBase, Base): + """ + Represents a POS transaction detail record. + """ + __tablename__ = 'bigArchive' diff --git a/corepos/enum.py b/corepos/enum.py index 93780c3..55619d8 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,43 +24,22 @@ CORE POS enumeration constants """ -from collections import OrderedDict -from enum import Enum +from __future__ import unicode_literals, absolute_import + +try: + from collections import OrderedDict +except ImportError: + from ordereddict import OrderedDict -class CoreDbType(str, Enum): - office_op = 'office_op' - office_trans = 'office_trans' - office_arch = 'office_arch' - - -BATCH_DISCOUNT_TYPE_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_PRICE_CHANGE, "Price Change"), (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"), + (BATCH_DISCOUNT_TYPE_SALE_RESTRICTED, "Member/Owner only sale"), ]) 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.cfg b/setup.cfg new file mode 100644 index 0000000..728f179 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,33 @@ +# -*- coding: utf-8; -*- + +[metadata] +name = pyCOREPOS +version = attr: corepos.__version__ +author = Lance Edgar +author_email = lance@edbob.org +url = https://rattailproject.org/ +license = GNU GPL v3 +description = Python Interface to CORE POS +long_description = file: README.rst +classifiers = + Development Status :: 3 - Alpha + Environment :: Console + Environment :: Web Environment + Intended Audience :: Developers + License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) + Natural Language :: English + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 3 + Topic :: Office/Business + Topic :: Software Development :: Libraries :: Python Modules + + +[options] +install_requires = + mysql-connector-python + requests + six + SQLAlchemy>=1.4 + +packages = find: diff --git a/corepos/db/office_arch/__init__.py b/setup.py similarity index 89% rename from corepos/db/office_arch/__init__.py rename to setup.py index 70292e9..a02ef90 100644 --- a/corepos/db/office_arch/__init__.py +++ b/setup.py @@ -20,11 +20,7 @@ # pyCOREPOS. If not, see . # ################################################################################ -""" -"Archive" Transaction Database Interface -""" -from sqlalchemy import orm +from setuptools import setup - -Session = orm.sessionmaker() +setup() diff --git a/tasks.py b/tasks.py index 6946a7f..a9b9fc3 100644 --- a/tasks.py +++ b/tasks.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2024 Lance Edgar +# Copyright © 2018-2020 Lance Edgar # # This file is part of pyCOREPOS. # @@ -25,32 +25,20 @@ Tasks for 'pyCOREPOS' package """ import os -import re 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!") +exec(open(os.path.join(here, 'corepos', '_version.py')).read()) @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') + ctx.run('twine upload dist/pyCOREPOS-{}.tar.gz'.format(__version__))