diff --git a/CHANGELOG.md b/CHANGELOG.md
index 39309fe..2c26baa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,41 @@ 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
diff --git a/corepos/db/common/__init__.py b/corepos/db/common/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/corepos/db/common/op.py b/corepos/db/common/op.py
new file mode 100644
index 0000000..30ecc1c
--- /dev/null
+++ b/corepos/db/common/op.py
@@ -0,0 +1,173 @@
+# -*- 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
new file mode 100644
index 0000000..9ec5601
--- /dev/null
+++ b/corepos/db/common/trans.py
@@ -0,0 +1,114 @@
+# -*- 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 c7ed5b2..456b1b8 100644
--- a/corepos/db/lane_op/model.py
+++ b/corepos/db/lane_op/model.py
@@ -2,7 +2,7 @@
################################################################################
#
# pyCOREPOS -- Python Interface to CORE POS
-# Copyright © 2018-2023 Lance Edgar
+# Copyright © 2018-2025 Lance Edgar
#
# This file is part of pyCOREPOS.
#
@@ -27,10 +27,26 @@ 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.
@@ -72,141 +88,11 @@ class Department(Base):
return self.name or ""
-class Product(Base):
+class Product(common.ProductBase, Base):
"""
- Represents a product, purchased and/or sold by the organization.
+ Data model for ``products`` table.
"""
__tablename__ = 'products'
- # __table_args__ = (
- # sa.ForeignKeyConstraint(['department'], ['departments.dept_no']),
- # sa.ForeignKeyConstraint(['subdept'], ['subdepts.subdept_no']),
- # sa.ForeignKeyConstraint(['tax'], ['taxrates.id']),
- # )
-
- id = sa.Column(sa.Integer(), nullable=False,
- primary_key=True, autoincrement=True)
-
- upc = sa.Column(sa.String(length=13), nullable=True)
-
- description = sa.Column(sa.String(length=30), nullable=True)
-
- brand = sa.Column(sa.String(length=30), nullable=True)
-
- formatted_name = sa.Column(sa.String(length=30), nullable=True)
-
- normal_price = sa.Column(sa.Float(), nullable=True)
-
- price_method = sa.Column('pricemethod', sa.SmallInteger(), nullable=True)
-
- group_price = sa.Column('groupprice', sa.Float(), nullable=True)
-
- quantity = sa.Column(sa.SmallInteger(), nullable=True)
-
- special_price = sa.Column(sa.Float(), nullable=True)
-
- special_price_method = sa.Column('specialpricemethod', sa.SmallInteger(), nullable=True)
-
- special_group_price = sa.Column('specialgroupprice', sa.Float(), nullable=True)
-
- special_quantity = sa.Column('specialquantity', sa.SmallInteger(), nullable=True)
-
- special_limit = sa.Column(sa.SmallInteger(), nullable=True)
-
- start_date = sa.Column(sa.DateTime(), nullable=True)
-
- end_date = sa.Column(sa.DateTime(), nullable=True)
-
- department_number = sa.Column('department', sa.SmallInteger(), nullable=True)
- # department = orm.relationship(
- # Department,
- # primaryjoin=Department.number == department_number,
- # foreign_keys=[department_number],
- # doc="""
- # Reference to the :class:`Department` to which the product belongs.
- # """)
-
- size = sa.Column(sa.String(length=9), nullable=True)
-
- tax_rate_id = sa.Column('tax', sa.SmallInteger(), nullable=True)
- # tax_rate = orm.relationship(TaxRate)
-
- foodstamp = sa.Column(sa.Boolean(), nullable=True)
-
- scale = sa.Column(sa.Boolean(), nullable=True)
-
- scale_price = sa.Column('scaleprice', sa.Float(), nullable=True)
-
- mix_match_code = sa.Column('mixmatchcode', sa.String(length=13), nullable=True)
-
- created = sa.Column(sa.DateTime(), nullable=True)
-
- modified = sa.Column(sa.DateTime(), nullable=True)
-
- # TODO: what to do about this 'replaces' thing?
- # 'batchID'=>array('type'=>'TINYINT', 'replaces'=>'advertised'),
- # batch_id = sa.Column('batchID', sa.SmallInteger(), nullable=True)
- # advertised = sa.Column(sa.Boolean(), nullable=True)
-
- tare_weight = sa.Column('tareweight', sa.Float(), nullable=True)
-
- discount = sa.Column(sa.SmallInteger(), nullable=True)
-
- discount_type = sa.Column('discounttype', sa.SmallInteger(), nullable=True)
-
- line_item_discountable = sa.Column(sa.Boolean(), nullable=True)
-
- unit_of_measure = sa.Column('unitofmeasure', sa.String(length=15), nullable=True)
-
- wicable = sa.Column(sa.SmallInteger(), nullable=True)
-
- quantity_enforced = sa.Column('qttyEnforced', sa.Boolean(), nullable=True)
-
- id_enforced = sa.Column('idEnforced', sa.SmallInteger(), nullable=True)
-
- cost = sa.Column(sa.Float(), nullable=True)
-
- special_cost = sa.Column(sa.Float(), nullable=True)
-
- received_cost = sa.Column(sa.Float(), nullable=True)
-
- in_use = sa.Column('inUse', sa.Boolean(), nullable=True)
-
- flags = sa.Column('numflag', sa.Integer(), nullable=True)
-
- subdepartment_number = sa.Column('subdept', sa.SmallInteger(), nullable=True)
- # subdepartment = orm.relationship(
- # Subdepartment,
- # primaryjoin=Subdepartment.number == subdepartment_number,
- # foreign_keys=[subdepartment_number],
- # doc="""
- # Reference to the :class:`Subdepartment` to which the product belongs.
- # """)
-
- deposit = sa.Column(sa.Float(), nullable=True)
-
- local = sa.Column(sa.Integer(), nullable=True,
- default=0) # TODO: do we want a default here?
-
- store_id = sa.Column(sa.SmallInteger(), nullable=True)
-
- default_vendor_id = sa.Column(sa.Integer(), nullable=True)
- # default_vendor = orm.relationship(
- # Vendor,
- # primaryjoin=Vendor.id == default_vendor_id,
- # foreign_keys=[default_vendor_id],
- # doc="""
- # Reference to the default :class:`Vendor` from which the product is obtained.
- # """)
-
- current_origin_id = sa.Column(sa.Integer(), nullable=True)
-
- auto_par = sa.Column(sa.Float(), nullable=True,
- default=0) # TODO: do we want a default here?
-
- price_rule_id = sa.Column(sa.Integer(), nullable=True)
-
- # TODO: some older DB's might not have this? guess we'll see
- last_sold = sa.Column(sa.DateTime(), nullable=True)
class CustomerClassic(Base):
diff --git a/corepos/db/lane_trans/__init__.py b/corepos/db/lane_trans/__init__.py
new file mode 100644
index 0000000..8e8c706
--- /dev/null
+++ b/corepos/db/lane_trans/__init__.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8; -*-
+################################################################################
+#
+# pyCOREPOS -- Python Interface to CORE POS
+# Copyright © 2018-2025 Lance Edgar
+#
+# This file is part of pyCOREPOS.
+#
+# pyCOREPOS is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# pyCOREPOS is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# pyCOREPOS. If not, see .
+#
+################################################################################
+"""
+Lane Transaction Database
+"""
+
+from sqlalchemy import orm
+
+
+Session = orm.sessionmaker()
diff --git a/corepos/db/lane_trans/model.py b/corepos/db/lane_trans/model.py
new file mode 100644
index 0000000..f2245f5
--- /dev/null
+++ b/corepos/db/lane_trans/model.py
@@ -0,0 +1,79 @@
+# -*- 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
index 49b0bbd..bc5838f 100644
--- a/corepos/db/office_arch/model.py
+++ b/corepos/db/office_arch/model.py
@@ -2,7 +2,7 @@
################################################################################
#
# pyCOREPOS -- Python Interface to CORE POS
-# Copyright © 2018-2024 Lance Edgar
+# Copyright © 2018-2025 Lance Edgar
#
# This file is part of pyCOREPOS.
#
@@ -24,9 +24,11 @@
CORE Office "arch" data model
"""
+import sqlalchemy as sa
from sqlalchemy import orm
-from corepos.db.office_trans.model import DTransactionBase, DLogBase
+from corepos.db.common import trans as common
+from corepos.db.office_trans.model import DTransactionBase
Base = orm.declarative_base()
@@ -34,7 +36,7 @@ Base = orm.declarative_base()
class BigArchive(DTransactionBase, Base):
"""
- Represents a record from ``bigArchive`` table.
+ Data model for ``bigArchive`` table.
"""
__tablename__ = 'bigArchive'
@@ -43,8 +45,19 @@ class BigArchive(DTransactionBase, Base):
TransactionDetail = BigArchive
+class DLogBase(common.TransactionDetailBase):
+ """
+ Base class for ``dlogBig`` and similar models.
+ """
+ store_row_id = sa.Column(sa.Integer(), primary_key=True, nullable=False)
+
+ store_id = sa.Column(sa.Integer(), nullable=True, default=0)
+ pos_row_id = sa.Column(sa.Integer(), nullable=True)
+ date_time = sa.Column('tdate', sa.DateTime(), nullable=True)
+
+
class DLogBig(DLogBase, Base):
"""
- Represents a record from ``dlogBig`` view.
+ Data model for ``dlogBig`` view.
"""
__tablename__ = 'dlogBig'
diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py
index 6087f67..60ad478 100644
--- a/corepos/db/office_op/model.py
+++ b/corepos/db/office_op/model.py
@@ -2,7 +2,7 @@
################################################################################
#
# pyCOREPOS -- Python Interface to CORE POS
-# Copyright © 2018-2024 Lance Edgar
+# Copyright © 2018-2025 Lance Edgar
#
# This file is part of pyCOREPOS.
#
@@ -31,6 +31,8 @@ import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.ext.associationproxy import association_proxy
+from corepos.db.common import op as common
+
log = logging.getLogger(__name__)
@@ -54,25 +56,12 @@ class StringableDateTime(sa.TypeDecorator):
raise NotImplementedError
-class Parameter(Base):
+class Parameter(common.ParameterBase, Base):
"""
- Represents a "parameter" value.
+ Data model for ``parameters`` table.
"""
__tablename__ = 'parameters'
- store_id = sa.Column(sa.SmallInteger(), primary_key=True, nullable=False)
-
- lane_id = sa.Column(sa.SmallInteger(), primary_key=True, nullable=False)
-
- param_key = sa.Column(sa.String(length=100), primary_key=True, nullable=False)
-
- param_value = sa.Column(sa.String(length=255), nullable=True)
-
- is_array = sa.Column(sa.Boolean(), nullable=True)
-
- def __str__(self):
- return "{}-{} {}".format(self.store_id, self.lane_id, self.param_key)
-
class TableSyncRule(Base):
"""
@@ -547,7 +536,7 @@ class Origin(Base):
return self.name or self.short_name or ""
-class Product(Base):
+class Product(common.ProductBase, Base):
"""
Represents a product, purchased and/or sold by the organization.
"""
@@ -558,125 +547,68 @@ class Product(Base):
sa.ForeignKeyConstraint(['tax'], ['taxrates.id']),
)
- id = sa.Column(sa.Integer(), primary_key=True, autoincrement=True, nullable=False)
-
- upc = sa.Column(sa.String(length=13), nullable=True)
-
- description = sa.Column(sa.String(length=30), nullable=True)
-
- brand = sa.Column(sa.String(length=30), nullable=True)
-
- formatted_name = sa.Column(sa.String(length=30), nullable=True)
-
- normal_price = sa.Column(sa.Float(), nullable=True)
-
- price_method = sa.Column('pricemethod', sa.SmallInteger(), nullable=True)
-
- group_price = sa.Column('groupprice', sa.Float(), nullable=True)
-
- quantity = sa.Column(sa.SmallInteger(), nullable=True)
-
- special_price = sa.Column(sa.Float(), nullable=True)
-
- special_price_method = sa.Column('specialpricemethod', sa.SmallInteger(), nullable=True)
-
- special_group_price = sa.Column('specialgroupprice', sa.Float(), nullable=True)
-
- special_quantity = sa.Column('specialquantity', sa.SmallInteger(), nullable=True)
-
- start_date = sa.Column(sa.DateTime(), nullable=True)
-
- end_date = sa.Column(sa.DateTime(), nullable=True)
-
- department_number = sa.Column('department', sa.SmallInteger(), nullable=True)
department = orm.relationship(
Department,
- primaryjoin=Department.number == department_number,
- foreign_keys=[department_number],
+ primaryjoin='Department.number == Product.department_number',
+ foreign_keys='Product.department_number',
doc="""
Reference to the :class:`Department` to which the product belongs.
""")
- size = sa.Column(sa.String(length=9), nullable=True)
-
- tax_rate_id = sa.Column('tax', sa.SmallInteger(), nullable=True)
tax_rate = orm.relationship(TaxRate)
- foodstamp = sa.Column(sa.Boolean(), nullable=True)
-
- scale = sa.Column(sa.Boolean(), nullable=True)
-
- # TODO: yikes, did i just code this all wrong the first time? pretty sure
- # this needs to change to a decimal column...
- scale_price = sa.Column('scaleprice', sa.Boolean(), nullable=True)
- # scale_price = sa.Column('scaleprice', sa.Numeric(precision=10, scale=2), nullable=True)
-
- mix_match_code = sa.Column('mixmatchcode', sa.String(length=13), nullable=True)
-
- created = sa.Column(StringableDateTime(), nullable=True)
-
- modified = sa.Column(sa.DateTime(), nullable=True)
-
# advertised = sa.Column(sa.Boolean(), nullable=True)
- tare_weight = sa.Column('tareweight', sa.Float(), nullable=True)
-
- discount = sa.Column(sa.SmallInteger(), nullable=True)
-
- discount_type = sa.Column('discounttype', sa.SmallInteger(), nullable=True)
-
- line_item_discountable = sa.Column(sa.Boolean(), nullable=True)
-
- unit_of_measure = sa.Column('unitofmeasure', sa.String(length=15), nullable=True)
-
- wicable = sa.Column(sa.SmallInteger(), nullable=True)
-
- quantity_enforced = sa.Column('qttyEnforced', sa.Boolean(), nullable=True)
-
- id_enforced = sa.Column('idEnforced', sa.SmallInteger(), nullable=True)
-
- cost = sa.Column(sa.Float(), nullable=True)
-
- in_use = sa.Column('inUse', sa.Boolean(), nullable=True)
-
- flags = sa.Column('numflag', sa.Integer(), nullable=True)
-
- subdepartment_number = sa.Column('subdept', sa.SmallInteger(), nullable=True)
subdepartment = orm.relationship(
Subdepartment,
- primaryjoin=Subdepartment.number == subdepartment_number,
- foreign_keys=[subdepartment_number],
+ primaryjoin='Subdepartment.number == Product.subdepartment_number',
+ foreign_keys='Product.subdepartment_number',
doc="""
Reference to the :class:`Subdepartment` to which the product belongs.
""")
- deposit = sa.Column(sa.Float(), nullable=True)
-
- local = sa.Column(sa.Integer(), nullable=True)
-
- store_id = sa.Column(sa.SmallInteger(), nullable=True)
-
- default_vendor_id = sa.Column(sa.Integer(), nullable=True)
default_vendor = orm.relationship(
Vendor,
- primaryjoin=Vendor.id == default_vendor_id,
- foreign_keys=[default_vendor_id],
+ primaryjoin='Vendor.id == Product.default_vendor_id',
+ foreign_keys='Product.default_vendor_id',
doc="""
Reference to the default :class:`Vendor` from which the product is obtained.
""")
+
# TODO: deprecate / remove this?
vendor = orm.synonym('default_vendor')
- current_origin_id = sa.Column(sa.Integer(), nullable=True)
-
- # TODO: some older DB's might not have this? guess we'll see
- last_sold = sa.Column(sa.DateTime(), nullable=True)
-
like_code = association_proxy(
'_like_code', 'like_code',
creator=lambda lc: ProductLikeCode(like_code=lc),
)
+ 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']
@@ -826,17 +758,12 @@ class VendorItem(Base):
upc = sa.Column(sa.String(length=13), nullable=False)
product = orm.relationship(
Product,
+ back_populates='vendor_items',
primaryjoin=Product.upc == upc,
foreign_keys=[upc],
doc="""
Reference to the :class:`Product` to which this record applies.
- """,
- backref=orm.backref(
- 'vendor_items',
- order_by=vendor_item_id,
- doc="""
- List of :class:`VendorItem` records for this product.
- """))
+ """)
brand = sa.Column(sa.String(length=50), nullable=True)
@@ -991,35 +918,12 @@ class ProductPhysicalLocation(Base):
location = sa.Column(sa.SmallInteger(), nullable=True, default=0)
-class Employee(Base):
+class Employee(common.EmployeeBase, Base):
"""
- Represents an employee within the organization.
+ Data model for ``employees`` table.
"""
__tablename__ = 'employees'
- number = sa.Column('emp_no', sa.SmallInteger(), primary_key=True, autoincrement=False, nullable=False)
-
- cashier_password = sa.Column('CashierPassword', sa.String(length=50), nullable=True)
-
- admin_password = sa.Column('AdminPassword', sa.String(length=50), nullable=True)
-
- first_name = sa.Column('FirstName', sa.String(length=255), nullable=True)
-
- last_name = sa.Column('LastName', sa.String(length=255), nullable=True)
-
- job_title = sa.Column('JobTitle', sa.String(length=255), nullable=True)
-
- active = sa.Column('EmpActive', sa.Boolean(), nullable=True)
-
- frontend_security = sa.Column('frontendsecurity', sa.SmallInteger(), nullable=True)
-
- backend_security = sa.Column('backendsecurity', sa.SmallInteger(), nullable=True)
-
- birth_date = sa.Column('birthdate', sa.DateTime(), nullable=True)
-
- def __str__(self):
- return ' '.join([self.first_name or '', self.last_name or '']).strip()
-
class MemberType(Base):
"""
@@ -1039,12 +943,11 @@ class MemberType(Base):
ssi = sa.Column(sa.Boolean(), nullable=True)
- ignoreSales = sa.Column(sa.Boolean(), nullable=True, default=False)
- ignore_sales = orm.synonym('ignoreSales')
+ # nb. this must be added explicitly if DB is new enough
+ #ignore_sales = sa.Column('ignoreSales', sa.Boolean(), nullable=True, default=False)
- # TODO: this was apparently added "recently" - isn't present in all DBs
- # (need to figure out how to conditionally include it in model?)
- # sales_code = sa.Column('salesCode', sa.Integer(), nullable=True)
+ # nb. this must be added explicitly if DB is new enough
+ #sales_code = sa.Column('salesCode', sa.Integer(), nullable=True)
def __str__(self):
return self.description or ""
@@ -1260,14 +1163,14 @@ class MemberInfo(Base):
card_number = sa.Column('card_no', sa.Integer(), primary_key=True, autoincrement=False, nullable=False)
- last_name = sa.Column(sa.String(length=30), nullable=True)
-
first_name = sa.Column(sa.String(length=30), nullable=True)
- other_last_name = sa.Column('othlast_name', sa.String(length=30), nullable=True)
+ last_name = sa.Column(sa.String(length=30), nullable=True)
other_first_name = sa.Column('othfirst_name', sa.String(length=30), nullable=True)
+ other_last_name = sa.Column('othlast_name', sa.String(length=30), nullable=True)
+
street = sa.Column(sa.String(length=255), nullable=True)
city = sa.Column(sa.String(length=20), nullable=True)
@@ -1842,3 +1745,22 @@ class PurchaseOrderNote(Base):
def __str__(self):
return self.notes or ""
+
+
+# the rest of this is a workaround to deal with the fact that some
+# CORE databases have columns which others do not. i had assumed that
+# all would be more or less the same but not so in practice. so if
+# your DB *does* have these columns, you must invoke the function
+# below in order to merge them into your schema. you should do this
+# on app startup and they'll be available normally from then on.
+
+RUNTIME = {'added_latest_columns': False}
+
+def use_latest_columns():
+ if RUNTIME['added_latest_columns']:
+ return
+
+ MemberType.ignore_sales = sa.Column('ignoreSales', sa.Boolean(), nullable=True, default=False)
+ MemberType.sales_code = sa.Column('salesCode', sa.Integer(), nullable=True)
+
+ RUNTIME['added_latest_columns'] = True
diff --git a/corepos/db/office_trans/model.py b/corepos/db/office_trans/model.py
index b4659cd..c2b0959 100644
--- a/corepos/db/office_trans/model.py
+++ b/corepos/db/office_trans/model.py
@@ -2,7 +2,7 @@
################################################################################
#
# pyCOREPOS -- Python Interface to CORE POS
-# Copyright © 2018-2024 Lance Edgar
+# Copyright © 2018-2025 Lance Edgar
#
# This file is part of pyCOREPOS.
#
@@ -26,7 +26,8 @@ CORE POS Transaction Data Model
import sqlalchemy as sa
from sqlalchemy import orm
-from sqlalchemy.ext.declarative import declared_attr
+
+from corepos.db.common import trans as common
Base = orm.declarative_base()
@@ -68,118 +69,20 @@ class EquityLiveBalance(Base):
start_date = sa.Column('startdate', sa.DateTime(), nullable=True)
-class TransactionDetailBase:
+class DTransactionBase(common.TransactionDetailBase):
"""
- Represents a POS transaction detail record.
+ Base class for ``dtransactions`` and similar models.
"""
-
- # store
store_row_id = sa.Column(sa.Integer(), primary_key=True, nullable=False)
- store_id = sa.Column(sa.Integer(), nullable=True, default=0)
- # register
- register_number = sa.Column('register_no', sa.Integer(), nullable=True)
pos_row_id = sa.Column(sa.Integer(), nullable=True)
-
- # txn
- transaction_id = sa.Column('trans_id', sa.Integer(), nullable=True)
- transaction_number = sa.Column('trans_no', sa.Integer(), nullable=True)
- transaction_type = sa.Column('trans_type', sa.String(length=1), nullable=True)
- transaction_subtype = sa.Column('trans_subtype', sa.String(length=2), nullable=True)
-
- trans_status = sa.Column(sa.String(length=1), nullable=True)
-
- @declared_attr
- def transaction_status(self):
- return orm.synonym('trans_status')
-
- # cashier
- employee_number = sa.Column('emp_no', sa.Integer(), nullable=True)
-
- # customer
- card_number = sa.Column('card_no', sa.Integer(), nullable=True)
- member_type = sa.Column('memType', sa.Integer(), nullable=True)
- staff = sa.Column(sa.Boolean(), nullable=True)
-
- ##############################
- # remainder is "line item" ...
- ##############################
-
- upc = sa.Column(sa.String(length=13), nullable=True)
-
- department_number = sa.Column('department', sa.Integer(), nullable=True)
-
- description = sa.Column(sa.String(length=30), nullable=True)
-
- quantity = sa.Column(sa.Float(), nullable=True)
-
- scale = sa.Column(sa.Boolean(), nullable=True, default=False)
-
- cost = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True)
-
- unitPrice = sa.Column('unitPrice', sa.Numeric(precision=10, scale=2), nullable=True)
-
- @declared_attr
- def unit_price(self):
- return orm.synonym('unitPrice')
-
- total = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True)
-
- reg_price = sa.Column('regPrice', sa.Numeric(precision=10, scale=2), nullable=True)
-
- tax = sa.Column(sa.SmallInteger(), nullable=True)
-
- @declared_attr
- def tax_rate_id(self):
- return orm.synonym('tax')
-
- food_stamp = sa.Column('foodstamp', sa.Boolean(), nullable=True)
-
- discount = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True)
-
- member_discount = sa.Column('memDiscount', sa.Numeric(precision=10, scale=2), nullable=True)
-
- discountable = sa.Column(sa.Boolean(), nullable=True)
-
- discount_type = sa.Column('discounttype', sa.Integer(), nullable=True)
-
- voided = sa.Column(sa.Integer(), nullable=True)
-
- percent_discount = sa.Column('percentDiscount', sa.Integer(), nullable=True)
-
- item_quantity = sa.Column('ItemQtty', sa.Float(), nullable=True)
-
- volume_discount_type = sa.Column('volDiscType', sa.Integer(), nullable=True)
-
- volume = sa.Column(sa.Integer(), nullable=True)
-
- volume_special = sa.Column('VolSpecial', sa.Numeric(precision=10, scale=2), nullable=True)
-
- mix_match = sa.Column('mixMatch', sa.String(length=13), nullable=True)
-
- matched = sa.Column(sa.Boolean(), nullable=True)
-
- num_flag = sa.Column('numflag', sa.Integer(), nullable=True, default=0)
-
- char_flag = sa.Column('charflag', sa.String(length=2), nullable=True)
-
- def __str__(self):
- return self.description or ''
-
-
-class DTransactionBase(TransactionDetailBase):
-
+ store_id = sa.Column(sa.Integer(), nullable=True, default=0)
date_time = sa.Column('datetime', sa.DateTime(), nullable=True)
-class DLogBase(TransactionDetailBase):
-
- date_time = sa.Column('tdate', sa.DateTime(), nullable=True)
-
-
class DTransaction(DTransactionBase, Base):
"""
- Represents a record from ``dtransactions`` table.
+ Data model for ``dtransactions`` table.
"""
__tablename__ = 'dtransactions'
diff --git a/pyproject.toml b/pyproject.toml
index 143843f..cf50f41 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
[project]
name = "pyCOREPOS"
-version = "0.3.4"
+version = "0.5.1"
description = "Python Interface to CORE POS"
readme = "README.md"
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]