diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c26baa..bba74ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,35 +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 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..c7ed5b2 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,11 +72,141 @@ 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): 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 index bc5838f..49b0bbd 100644 --- a/corepos/db/office_arch/model.py +++ b/corepos/db/office_arch/model.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2025 Lance Edgar +# Copyright © 2018-2024 Lance Edgar # # This file is part of pyCOREPOS. # @@ -24,11 +24,9 @@ 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 +from corepos.db.office_trans.model import DTransactionBase, DLogBase Base = orm.declarative_base() @@ -36,7 +34,7 @@ Base = orm.declarative_base() class BigArchive(DTransactionBase, Base): """ - Data model for ``bigArchive`` table. + Represents a record from ``bigArchive`` table. """ __tablename__ = 'bigArchive' @@ -45,19 +43,8 @@ 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): """ - Data model for ``dlogBig`` view. + Represents a record from ``dlogBig`` view. """ __tablename__ = 'dlogBig' diff --git a/corepos/db/office_op/model.py b/corepos/db/office_op/model.py index 60ad478..4d79e88 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-2024 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,25 @@ class StringableDateTime(sa.TypeDecorator): raise NotImplementedError -class Parameter(common.ParameterBase, Base): +class Parameter(Base): """ - Data model for ``parameters`` table. + 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): """ @@ -536,7 +547,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 +558,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 +826,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 +991,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): """ @@ -1163,14 +1259,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) diff --git a/corepos/db/office_trans/model.py b/corepos/db/office_trans/model.py index c2b0959..b4659cd 100644 --- a/corepos/db/office_trans/model.py +++ b/corepos/db/office_trans/model.py @@ -2,7 +2,7 @@ ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS -# Copyright © 2018-2025 Lance Edgar +# Copyright © 2018-2024 Lance Edgar # # This file is part of pyCOREPOS. # @@ -26,8 +26,7 @@ CORE POS Transaction Data Model import sqlalchemy as sa from sqlalchemy import orm - -from corepos.db.common import trans as common +from sqlalchemy.ext.declarative import declared_attr Base = orm.declarative_base() @@ -69,20 +68,118 @@ class EquityLiveBalance(Base): start_date = sa.Column('startdate', sa.DateTime(), nullable=True) -class DTransactionBase(common.TransactionDetailBase): +class TransactionDetailBase: """ - Base class for ``dtransactions`` and similar models. + Represents a POS transaction detail record. """ - store_row_id = sa.Column(sa.Integer(), primary_key=True, nullable=False) - pos_row_id = sa.Column(sa.Integer(), nullable=True) + # 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): + 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): """ - Data model for ``dtransactions`` table. + Represents a record from ``dtransactions`` table. """ __tablename__ = 'dtransactions' diff --git a/pyproject.toml b/pyproject.toml index cf50f41..2aaef92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "pyCOREPOS" -version = "0.5.1" +version = "0.3.5" description = "Python Interface to CORE POS" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]