Compare commits

..

95 commits

Author SHA1 Message Date
feb2d3471e bump: version 0.5.0 → 0.5.1 2025-02-20 09:31:29 -06:00
9b9260ba4b fix: add Product.default_vendor_item convenience property 2025-02-20 09:30:24 -06:00
8359e5692e bump: version 0.4.0 → 0.5.0 2025-02-01 15:19:12 -06:00
97a1396a54 feat: use true column names for transaction data models
as much as i kind of want to "rename" some of these for convenience,
it seems safest here to just stick with true names to avoid confusion
2025-01-25 17:04:43 -06:00
a2a1d7faee fix: define common base schema for Product model 2025-01-25 17:04:40 -06:00
2fe089bd57 fix: add Parameter model for lane_op 2025-01-25 16:41:17 -06:00
50351596ac fix: add model for lane_trans LocalTrans 2025-01-24 20:18:13 -06:00
28cb23adc4 bump: version 0.3.5 → 0.4.0 2025-01-24 19:59:18 -06:00
c3b639390d fix: add Employee model for lane_op
with abstract common base schema
2025-01-24 19:21:00 -06:00
97fb0b28cb feat: add common base class for dtransactions and similar models
more work to be done here i'm sure, but this should hopefully be a
conservative change in the right direction
2025-01-24 19:20:18 -06:00
d21346bbff fix: fix ordering of name columns for MemberInfo
so they show up correctly by default e.g. in UI
2025-01-15 14:48:18 -06:00
6c1fc9a803 bump: version 0.3.4 → 0.3.5 2025-01-15 11:00:22 -06:00
7aaa35dac7 fix: add workaround to avoid missing schema columns
more columns will need to be added to this workaround i'm sure, but
this takes care of a couple small ones
2025-01-15 10:59:14 -06:00
b8ca60b508 bump: version 0.3.3 → 0.3.4 2025-01-15 08:51:40 -06:00
01852ceecc fix: misc. cleanup for sales batch models 2025-01-15 08:45:59 -06:00
ab56a35acc fix: add more enums for batch discount type, editor UI
also fix column type for editor UI
2025-01-14 20:20:41 -06:00
e37cf88cd9 bump: version 0.3.2 → 0.3.3 2025-01-13 13:31:21 -06:00
023d826d31 fix: remove autoincrement option for composite PK fields 2025-01-13 12:57:41 -06:00
465e565f7b bump: version 0.3.1 → 0.3.2 2025-01-11 22:01:38 -06:00
47c23a65ae fix: add base class for all transaction tables, views
also rename some classes; add `dlogBig` for archive view
2025-01-11 21:55:54 -06:00
464f107a88 fix: add MemberType.ignore_sales column 2025-01-11 21:52:22 -06:00
cb6ed15eb8 fix: add model for MasterSuperDepartment 2025-01-11 21:51:28 -06:00
310a261b48 bump: version 0.3.0 → 0.3.1 2024-12-17 16:45:36 -06:00
3bb01f2397 fix: add wicable, active columns for Department model 2024-12-17 16:45:11 -06:00
Lance Edgar
ed48f9134a docs: update project links to forgejo 2024-09-14 10:43:39 -05:00
Lance Edgar
b44e5a3d60 docs: use markdown for readme file 2024-09-13 18:49:14 -05:00
Lance Edgar
b425095a8d bump: version 0.2.1 → 0.3.0 2024-08-06 23:21:26 -05:00
Lance Edgar
b5b29cdcf1 feat: add model for MemberContactPreference (op.memContactPrefs) 2024-08-06 11:34:07 -05:00
Lance Edgar
27a46ed18f feat: add model for CustomReceiptLine (op.customReceipt) 2024-08-06 10:38:01 -05:00
Lance Edgar
2df3885420 bump: version 0.2.0 → 0.2.1 2024-07-04 23:59:49 -05:00
Lance Edgar
bcff560555 fix: add API methods, get_employees() and get_employee() 2024-07-04 18:27:50 -05:00
Lance Edgar
66cf108b3f fix: remove Change data model
since it isn't actually part of CORE, and is now handled in other ways
with regard to datasync
2024-07-04 13:18:45 -05:00
Lance Edgar
e9638c73a4 fix: remove dependency for six package 2024-07-01 16:32:39 -05:00
Lance Edgar
3b54dbd068 docs: use more specific project homepage url 2024-06-14 19:48:40 -05:00
Lance Edgar
40e647d5c8 build: use newer convention when building for release 2024-06-10 15:57:09 -05:00
Lance Edgar
04948fb840 bump: version 0.1.20 → 0.2.0 2024-06-10 15:55:51 -05:00
Lance Edgar
5cf8d2ac05 feat: switch from setup.cfg to pyproject.toml + hatchling 2024-06-10 15:55:14 -05:00
Lance Edgar
f80d03daaa Fix default dist filename for release task
not sure why this fix was needed, did setuptools behavior change?
2024-05-29 10:10:25 -05:00
Lance Edgar
777b198e44 Update changelog 2024-05-29 10:09:15 -05:00
Lance Edgar
df6f0d9793 Add enum for CORE (Office) DB types
for use with typer commands
2024-05-16 19:13:35 -05:00
Lance Edgar
7cd89029ac Update changelog 2023-11-01 08:15:37 -05:00
Lance Edgar
909d75796b Fix synonym for dtransactions.tax 2023-10-20 19:03:20 -05:00
Lance Edgar
f217f00a8f Fix data types for tax, voided in dtransactions 2023-10-20 14:31:13 -05:00
Lance Edgar
67dd9777ba Update changelog 2023-10-12 10:38:44 -05:00
Lance Edgar
1597c163c6 Let MemberInfo.dates be an object, not a list 2023-10-12 10:34:13 -05:00
Lance Edgar
3a3fba19e4 Fix the Department.tax_rate relationship
whoops i mistook `dept_tax` for a boolean previously
2023-10-11 18:36:12 -05:00
Lance Edgar
7171531dce Update changelog 2023-10-07 18:58:17 -05:00
Lance Edgar
e5988102ad Rename module to corepos.db.office_arch 2023-10-05 11:52:39 -05:00
Lance Edgar
f06f236a60 Update changelog 2023-09-15 12:59:52 -05:00
Lance Edgar
83eecad28d Add model for office_op.Tender 2023-09-15 12:43:02 -05:00
Lance Edgar
541adb6979 Update changelog 2023-09-13 21:51:10 -05:00
Lance Edgar
5214db0a83 Add model for CustomerNotifications table 2023-09-13 16:23:33 -05:00
Lance Edgar
0dda359094 Update changelog 2023-09-07 18:37:48 -05:00
Lance Edgar
0e30303947 Tweak primary key for StockPurchase model
per having a better understanding now, i think..
2023-09-07 17:45:14 -05:00
Lance Edgar
5921a18f12 Update changelog 2023-09-02 13:56:59 -05:00
Lance Edgar
5a77d14a26 Add models for StockPurchase and EquityLiveBalance 2023-06-18 11:38:14 -05:00
Lance Edgar
b2622c473d Update changelog 2023-06-12 20:39:12 -05:00
Lance Edgar
c6144ab310 Add note about meminfo.email_2 field, aka. "alt. phone" 2023-06-12 20:38:12 -05:00
Lance Edgar
4952d2fa3d Rename custdata model to CustomerClassic 2023-06-12 17:28:55 -05:00
Lance Edgar
852a989bd5 Add get_member_types() method for CORE API 2023-06-06 13:13:51 -05:00
Lance Edgar
bb8278dcc8 Update changelog 2023-06-02 14:26:51 -05:00
Lance Edgar
d58426c073 Add support for htdigest auth when using CORE webservices API 2023-05-22 21:34:46 -05:00
Lance Edgar
757fb50a96 Update changelog 2023-05-17 06:57:14 -05:00
Lance Edgar
6e43449ecb Replace setup.py contents with setup.cfg 2023-05-16 13:16:41 -05:00
Lance Edgar
24fb8c8fea Update changelog 2023-05-01 22:17:38 -05:00
Lance Edgar
ad5837405f Require SQLAlchemy 1.4.x 2023-02-15 12:51:50 -06:00
Lance Edgar
2444628c13 Update changelog 2023-01-02 16:56:30 -06:00
Lance Edgar
c2723de467 Delete productUser record when products record is deleted 2022-08-21 00:11:24 -05:00
Lance Edgar
2b80fd6a6b Add basic TransactionDetail for trans archive model 2022-03-26 23:04:05 -05:00
Lance Edgar
af6189b237 Update changelog 2022-03-02 21:35:57 -06:00
Lance Edgar
b46282264c Add model for UserGroup 2022-01-17 18:59:19 -06:00
Lance Edgar
a6306a4882 Remove deprecation warning for corepos.db
everything should be using the right imports now
2021-12-30 21:47:55 -06:00
Lance Edgar
ae45615717 Update changelog 2021-11-04 21:26:22 -05:00
Lance Edgar
41c142b837 Add the custdata model for lane_op DB
also use Numeric instead of Float for "MONEY" columns, for custdata in
both lane_op and office_op
2021-11-04 17:41:39 -05:00
Lance Edgar
754d8697e8 Add proper support for str(Suspension) 2021-09-30 18:44:53 -04:00
Lance Edgar
0d81a41b54 Add User model for office_op 2021-09-07 15:45:51 -05:00
Lance Edgar
69c7be6356 Update changelog 2021-08-31 22:42:22 -05:00
Lance Edgar
092884eab3 Add lane_op model for Department 2021-08-31 22:40:58 -05:00
Lance Edgar
07e3c62b6c Update changelog 2021-08-02 09:07:50 -05:00
Lance Edgar
e4c46b3fa4 Add schema for TableSyncRules 2021-08-02 08:56:05 -05:00
Lance Edgar
b4a07f9875 Update changelog 2021-07-21 20:17:56 -05:00
Lance Edgar
178acdac31 Add basic 'lane_op' DB schema
just the 'products' table so far
2021-07-21 20:02:17 -05:00
Lance Edgar
0d3d007bd1 Update changelog 2021-06-11 18:04:42 -05:00
Lance Edgar
170f0a769a Remove duplicated column name 2021-05-07 11:12:44 -05:00
Lance Edgar
d94189c404 Add FK constraint for ProductLikeCode.upc
not sure why that wasn't already present?
2021-05-04 20:10:14 -05:00
Lance Edgar
b40fbf7cab Add the Product.complete_size convenience attribute
also define `str(VendorItem)`
2021-02-15 12:59:21 -06:00
Lance Edgar
7cf4ec1295 Fetch single vendorItems record by sku instead of upc
b/c that is what we must use as PK when updating the record etc.
2021-02-09 16:11:50 -06:00
Lance Edgar
76f743f3b8 Add set_vendor_item() method for API client 2021-02-09 14:25:18 -06:00
Lance Edgar
1757d09781 Add schema model for Purchase Orders 2021-02-01 15:35:22 -06:00
Lance Edgar
ee9451588c Add basic support for Stores schema and API 2021-01-27 22:19:38 -06:00
Lance Edgar
d398e706c4 Add MemberBarcode to op model 2021-01-13 19:18:17 -06:00
Lance Edgar
19d62b535f Tweak that thing again... 2020-12-31 19:25:33 -06:00
Lance Edgar
52d9595331 Fix Department.see_id field definition
this is not a flag, but a minimum age requirement
2020-12-31 19:01:40 -06:00
Lance Edgar
ff428c4635 Misc. tweaks to product-related schema, for sake of generating SQL
e.g. from IFPS data
2020-12-09 13:09:58 -06:00
Lance Edgar
29638c062c Remove "default" values from Product model definition
i'm a bit torn about this.  on the one hand i like there being some default
values here, but realistically they didn't all make sense.  also it could be
said that default values amount to business logic and that should stay in CORE
basically.

but in the end, i needed these to go away in order to simplify generating some
SQL INSERT statements.  was using SQLAlchemy core and it kept insisting that
all fields with a "default" defined should be part of the INSERT statement, but
really i didn't want them to be.  so they no longer have defaults.
2020-12-07 17:41:01 -06:00
24 changed files with 1703 additions and 421 deletions

3
.gitignore vendored
View file

@ -1 +1,4 @@
*~
*.pyc
dist/
pyCOREPOS.egg-info/

View file

@ -5,6 +5,173 @@ All notable changes to pyCOREPOS will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## v0.5.1 (2025-02-20)
### Fix
- add `Product.default_vendor_item` convenience property
## v0.5.0 (2025-02-01)
### Feat
- use true column names for transaction data models
### Fix
- define common base schema for Product model
- add `Parameter` model for lane_op
- add model for lane_trans `LocalTrans`
## v0.4.0 (2025-01-24)
### Feat
- add common base class for `dtransactions` and similar models
### Fix
- add `Employee` model for lane_op
- fix ordering of name columns for MemberInfo
## v0.3.5 (2025-01-15)
### Fix
- add workaround to avoid missing schema columns
## v0.3.4 (2025-01-15)
### Fix
- misc. cleanup for sales batch models
- add more enums for batch discount type, editor UI
## v0.3.3 (2025-01-13)
### Fix
- remove `autoincrement` option for composite PK fields
## v0.3.2 (2025-01-11)
### Fix
- add base class for all transaction tables, views
- add `MemberType.ignore_sales` column
- add model for `MasterSuperDepartment`
## v0.3.1 (2024-12-17)
### Fix
- add `wicable`, `active` columns for Department model
## v0.3.0 (2024-08-06)
### Feat
- add model for `MemberContactPreference` (`op.memContactPrefs`)
- add model for `CustomReceiptLine` (`op.customReceipt`)
## v0.2.1 (2024-07-04)
### Fix
- add API methods, `get_employees()` and `get_employee()`
- remove `Change` data model
- remove dependency for `six` package
## v0.2.0 (2024-06-10)
### Feat
- switch from setup.cfg to pyproject.toml + hatchling
## [0.1.20] - 2024-05-29
### Changed
- Add enum for CORE (Office) DB types.
## [0.1.19] - 2023-11-01
### Changed
- Fix data types for tax, voided in `dtransactions`.
- Fix synonym for `dtransactions.tax`.
## [0.1.18] - 2023-10-12
### Changed
- Fix the `Department.tax_rate` relationship.
- Let `MemberInfo.dates` be an object, not a list.
## [0.1.17] - 2023-10-07
### Changed
- Rename module to `corepos.db.office_arch`.
## [0.1.16] - 2023-09-15
### Changed
- Add model for `office_op.Tender`.
## [0.1.15] - 2023-09-13
### Changed
- Add model for `CustomerNotifications` table.
## [0.1.14] - 2023-09-07
### Changed
- Tweak primary key for StockPurchase model.
## [0.1.13] - 2023-09-02
### Changed
- Add models for StockPurchase and EquityLiveBalance.
## [0.1.12] - 2023-06-12
### Changed
- Add `get_member_types()` method for CORE API.
- Rename model for `custdata` to `CustomerClassic`.
- Add note about `meminfo.email_2` field, aka. "alt. phone".
## [0.1.11] - 2023-06-02
### Changed
- Add support for htdigest auth when using CORE webservices API.
## [0.1.10] - 2023-05-17
### Changed
- Replace `setup.py` contents with `setup.cfg`.
## [0.1.9] - 2023-05-01
### Changed
- Require SQLAlchemy 1.4.x.
## [0.1.8] - 2023-01-02
### Changed
- Add basic `TransactionDetail` for trans archive model.
- Delete `productUser` record when `products` record is deleted.
## [0.1.7] - 2022-03-02
### Changed
- Remove deprecation warning for `corepos.db`.
- Add model for `UserGroup`.
## [0.1.6] - 2021-11-04
### Changed
- Add `User` model for office_op.
- Add proper support for `str(Suspension)`.
- Add the `custdata` model for lane_op DB.
## [0.1.5] - 2021-08-31
### Changed
- Add lane_op model for Department.
## [0.1.4] - 2021-08-02
### Changed
- Add schema for `TableSyncRules`.
## [0.1.3] - 2021-07-21
### Changed
- Add basic 'lane_op' DB schema.
## [0.1.2] - 2021-06-11
### Changed
- Several more updates, mostly a "save point" release.
## [0.1.1] - 2020-09-16
### Added
- A ton of updates, mostly a "save point" release.

5
README.md Normal file
View file

@ -0,0 +1,5 @@
# pyCOREPOS
A Python interface to the [CORE POS](https://github.com/CORE-POS)
system.

View file

@ -1,7 +0,0 @@
pyCOREPOS
=========
A Python interface to the `CORE POS`_ system.
.. _CORE POS: https://github.com/CORE-POS

View file

@ -1,3 +1,6 @@
# -*- coding: utf-8; -*-
__version__ = '0.1.1'
from importlib.metadata import version
__version__ = version('pyCOREPOS')

View file

@ -2,7 +2,7 @@
################################################################################
#
# pyCOREPOS -- Python Interface to CORE POS
# Copyright © 2018-2020 Lance Edgar
# Copyright © 2018-2024 Lance Edgar
#
# This file is part of pyCOREPOS.
#
@ -28,6 +28,7 @@ import json
import logging
import requests
from requests.auth import HTTPDigestAuth
log = logging.getLogger(__name__)
@ -48,27 +49,42 @@ class CoreAPIError(Exception):
class CoreWebAPI(object):
"""
Client implementation for the CORE webservices API.
"""
def __init__(self, url, verify=True):
"""
Constructor for the API client.
:param str url: URL to the CORE webservices API,
e.g. ``'http://localhost/fannie/ws/'``
:param bool verify: How to handle certificate validation for HTTPS
URLs. This value is passed as-is to the ``requests`` library, so
see those docs for more info. The default value for this is
``True`` because the assumption is that security should be on by
default. Set it to ``False`` in order to disable validation
entirely, e.g. for self-signed certs. (This may also be needed for
basic HTTP URLs?) Other values may be possible also; again see the
``requests`` docs for more info.
URLs. This value is passed as-is to the ``requests`` library,
so see those docs for more info. The default value for this is
``True`` because the assumption is that security should be on
by default. Set it to ``False`` in order to disable validation
entirely, e.g. for self-signed certs. (This may also be needed
for basic HTTP URLs?) Other values may be possible also; again
see the ``requests`` docs for more info.
:param htdigest_username: Username for htdigest authentication, if
applicable.
:param htdigest_password: Password for htdigest authentication, if
applicable.
"""
def __init__(
self,
url,
verify=True,
htdigest_username=None,
htdigest_password=None,
):
self.url = url
self.verify = verify
self.session = requests.Session()
if htdigest_username and htdigest_password:
self.session.auth = HTTPDigestAuth(htdigest_username,
htdigest_password)
def post(self, params, method=None):
"""
Issue a POST request to the API, with the given ``params``. If not
@ -88,7 +104,7 @@ class CoreWebAPI(object):
'id': 1,
}
response = requests.post(self.url, data=json.dumps(payload),
response = self.session.post(self.url, data=json.dumps(payload),
verify=self.verify)
response.raise_for_status()
return response
@ -116,6 +132,22 @@ class CoreWebAPI(object):
assert set(js['result'].keys()) == set(['result'])
return js['result']['result']
def get_member_types(self):
"""
Fetch all Member Type records from CORE.
:returns: A (potentially empty) list of member type dict records.
"""
params = {
'entity': 'Memtype',
'submethod': 'get',
'columns': {},
}
response = self.post(params)
result = self.parse_response(response)
return [json.loads(rec) for rec in result]
def get_members(self):
"""
Fetch all Member records from CORE.
@ -166,6 +198,63 @@ 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.
:returns: A (potentially empty) list of store dict records.
To fetch all stores::
api.get_stores()
To fetch only stores named "Headquarters"::
api.get_stores(description='Headquarters')
"""
params = {
'entity': 'Stores',
'submethod': 'get',
'columns': columns,
}
response = self.post(params)
result = self.parse_response(response)
return [json.loads(rec) for rec in result]
def get_departments(self, **columns):
"""
Fetch some or all of Department records from CORE.
@ -445,13 +534,13 @@ class CoreWebAPI(object):
result = self.parse_response(response)
return [json.loads(rec) for rec in result]
def get_vendor_item(self, upc, vendorID, **columns):
def get_vendor_item(self, sku, vendorID, **columns):
"""
Fetch an existing VendorItem record from CORE.
:returns: Either a vendor item dict record, or ``None``.
"""
columns['upc'] = upc
columns['sku'] = sku
columns['vendorID'] = vendorID
params = {
'entity': 'VendorItems',
@ -464,3 +553,26 @@ class CoreWebAPI(object):
if len(result) > 1:
log.warning("CORE API returned %s VendorItem results", len(result))
return json.loads(result[0])
def set_vendor_item(self, sku, vendorID, **columns):
"""
Update an existing VendorItem record in CORE.
:returns: Boolean indicating success of the operation.
.. note::
Currently this is being used to create a *new* product also. CORE's
``vendorItems`` table does not use auto-increment for its PK, which
means we must provide one even when creating; therefore this method
may be used for that.
"""
columns['sku'] = sku
columns['vendorID'] = vendorID
params = {
'entity': 'VendorItems',
'submethod': 'set',
'columns': columns,
}
response = self.post(params)
result = self.parse_response(response)
return json.loads(result)

View file

@ -2,7 +2,7 @@
################################################################################
#
# pyCOREPOS -- Python Interface to CORE POS
# Copyright © 2018-2020 Lance Edgar
# Copyright © 2018-2021 Lance Edgar
#
# This file is part of pyCOREPOS.
#
@ -23,12 +23,3 @@
"""
Database Interface
"""
from __future__ import unicode_literals, absolute_import
import warnings
warnings.warn("The `corepos.db` module is deprecated! "
"Please use `corepos.db.office_op` instead.",
DeprecationWarning)
from corepos.db.office_op import *

View file

173
corepos/db/common/op.py Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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)

114
corepos/db/common/trans.py Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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 ''}"

View file

@ -0,0 +1,30 @@
# -*- coding: utf-8; -*-
################################################################################
#
# pyCOREPOS -- Python Interface to CORE POS
# Copyright © 2018-2021 Lance Edgar
#
# This file is part of pyCOREPOS.
#
# pyCOREPOS is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# pyCOREPOS is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# pyCOREPOS. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
"Lane Operational" Database Interface
"""
from sqlalchemy import orm
Session = orm.sessionmaker()

167
corepos/db/lane_op/model.py Normal file
View file

@ -0,0 +1,167 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Data model for CORE POS "lane_op" DB
"""
import sqlalchemy as sa
from sqlalchemy import orm
from corepos.db.common import op as common
Base = orm.declarative_base()
class Parameter(common.ParameterBase, Base):
"""
Data model for ``parameters`` table.
"""
__tablename__ = 'parameters'
class Employee(common.EmployeeBase, Base):
"""
Data model for ``employees`` table.
"""
__tablename__ = 'employees'
class Department(Base):
"""
Represents a department within the organization.
"""
__tablename__ = 'departments'
number = sa.Column('dept_no', sa.SmallInteger(), nullable=False,
primary_key=True, autoincrement=False)
name = sa.Column('dept_name', sa.String(length=30), nullable=True)
tax = sa.Column('dept_tax', sa.Boolean(), nullable=True)
food_stampable = sa.Column('dept_fs', sa.Boolean(), nullable=True)
limit = sa.Column('dept_limit', sa.Float(), nullable=True)
minimum = sa.Column('dept_minimum', sa.Float(), nullable=True)
discount = sa.Column('dept_discount', sa.Boolean(), nullable=True)
see_id = sa.Column('dept_see_id', sa.SmallInteger(), nullable=True)
modified = sa.Column(sa.DateTime(), nullable=True)
modified_by_id = sa.Column('modifiedby', sa.Integer(), nullable=True)
margin = sa.Column(sa.Float(), nullable=False)
sales_code = sa.Column('salesCode', sa.Integer(), nullable=False)
member_only = sa.Column('memberOnly', sa.SmallInteger(), nullable=False)
line_item_discount = sa.Column(sa.Boolean(), nullable=True)
wicable = sa.Column('dept_wicable', sa.Boolean(), nullable=True)
def __str__(self):
return self.name or ""
class Product(common.ProductBase, Base):
"""
Data model for ``products`` table.
"""
__tablename__ = 'products'
class CustomerClassic(Base):
"""
Represents a customer of the organization.
https://github.com/CORE-POS/IS4C/blob/master/pos/is4c-nf/lib/models/op/CustdataModel.php
"""
__tablename__ = 'custdata'
# __table_args__ = (
# sa.ForeignKeyConstraint(['memType'], ['memtype.memtype']),
# )
id = sa.Column(sa.Integer(), nullable=False, primary_key=True, autoincrement=True)
card_number = sa.Column('CardNo', sa.Integer(), nullable=True)
person_number = sa.Column('personNum', sa.SmallInteger(), nullable=True)
first_name = sa.Column('FirstName', sa.String(length=30), nullable=True)
last_name = sa.Column('LastName', sa.String(length=30), nullable=True)
cash_back = sa.Column('CashBack', sa.Numeric(precision=10, scale=2), nullable=True)
balance = sa.Column('Balance', sa.Numeric(precision=10, scale=2), nullable=True)
discount = sa.Column('Discount', sa.SmallInteger(), nullable=True)
member_discount_limit = sa.Column('MemDiscountLimit', sa.Numeric(precision=10, scale=2), nullable=True)
charge_limit = sa.Column('ChargeLimit', sa.Numeric(precision=10, scale=2), nullable=True)
charge_ok = sa.Column('ChargeOk', sa.Boolean(), nullable=True, default=True)
write_checks = sa.Column('WriteChecks', sa.Boolean(), nullable=True, default=True)
store_coupons = sa.Column('StoreCoupons', sa.Boolean(), nullable=True, default=True)
type = sa.Column('Type', sa.String(length=10), nullable=True, default='PC')
member_type_id = sa.Column('memType', sa.SmallInteger(), nullable=True)
# member_type = orm.relationship(
# MemberType,
# primaryjoin=MemberType.id == member_type_id,
# foreign_keys=[member_type_id],
# doc="""
# Reference to the :class:`MemberType` to which this member belongs.
# """)
staff = sa.Column(sa.Boolean(), nullable=True, default=False)
ssi = sa.Column('SSI', sa.Boolean(), nullable=True, default=False)
purchases = sa.Column('Purchases', sa.Numeric(precision=10, scale=2), nullable=True, default=0)
number_of_checks = sa.Column('NumberOfChecks', sa.SmallInteger(), nullable=True, default=0)
member_coupons = sa.Column('memCoupons', sa.Integer(), nullable=True, default=1)
blue_line = sa.Column('blueLine', sa.String(length=50), nullable=True)
shown = sa.Column('Shown', sa.Boolean(), nullable=True, default=True)
last_change = sa.Column('LastChange', sa.DateTime(), nullable=True)
def __str__(self):
return "{} {}".format(self.first_name or '', self.last_name or '').strip()
# TODO: deprecate / remove this
CustData = CustomerClassic

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Lane Transaction Database
"""
from sqlalchemy import orm
Session = orm.sessionmaker()

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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'

View file

@ -0,0 +1,30 @@
# -*- coding: utf-8; -*-
################################################################################
#
# pyCOREPOS -- Python Interface to CORE POS
# Copyright © 2018-2023 Lance Edgar
#
# This file is part of pyCOREPOS.
#
# pyCOREPOS is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# pyCOREPOS is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# pyCOREPOS. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
"Archive" Transaction Database Interface
"""
from sqlalchemy import orm
Session = orm.sessionmaker()

View file

@ -0,0 +1,63 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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'

View file

@ -2,7 +2,7 @@
################################################################################
#
# pyCOREPOS -- Python Interface to CORE POS
# Copyright © 2018-2020 Lance Edgar
# Copyright © 2018-2025 Lance Edgar
#
# This file is part of pyCOREPOS.
#
@ -24,54 +24,159 @@
Data model for CORE POS "office_op" DB
"""
import datetime
import logging
import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.associationproxy import association_proxy
from corepos.db.common import op as common
log = logging.getLogger(__name__)
Base = declarative_base()
Base = orm.declarative_base()
class Change(Base):
class StringableDateTime(sa.TypeDecorator):
"""
Represents a changed (or deleted) record, which is pending synchronization
to another system(s).
Sort of a hack, to let us string-ify certain DateTime values when
generating "raw" SQL output.
.. 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.
cf. https://docs.sqlalchemy.org/en/14/faq/sqlexpressions.html#rendering-bound-parameters-inline
"""
__tablename__ = 'datasync_changes'
impl = sa.DateTime
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)
def process_literal_param(self, value, dialect):
if value is None:
return 'NULL'
if isinstance(value, datetime.datetime):
return "'{}'".format(value.strftime('%Y-%m-%d %H:%M:%S'))
raise NotImplementedError
class Parameter(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)
class TableSyncRule(Base):
"""
Represents a "table sync rule" value.
"""
__tablename__ = 'TableSyncRules'
param_key = sa.Column(sa.String(length=100), primary_key=True, nullable=False)
# nb. this should be autoincrement, but we can't do that
# automatically via sqlalchemy when PK is composite
id = sa.Column('tableSyncRuleID', sa.Integer(), nullable=False, primary_key=True)
param_value = sa.Column(sa.String(length=255), nullable=True)
table_name = sa.Column('tableName', sa.String(length=255), nullable=False, primary_key=True)
is_array = sa.Column(sa.Boolean(), nullable=True)
rule = sa.Column(sa.String(length=255), nullable=True)
def __str__(self):
return "{}-{} {}".format(self.store_id, self.lane_id, self.param_key)
return "{}: {}".format(self.table_name, self.rule)
class UserGroup(Base):
"""
Represents a user/group assignment.
"""
__tablename__ = 'userGroups'
group_id = sa.Column('gid', sa.Integer(), nullable=False,
primary_key=True, autoincrement=False)
name = sa.Column(sa.String(length=50), nullable=True)
username = sa.Column(sa.String(length=50), nullable=False,
primary_key=True)
def __str__(self):
return self.name or ""
class User(Base):
"""
Represents a user within CORE Office.
"""
__tablename__ = 'Users'
name = sa.Column(sa.String(length=50), nullable=False, primary_key=True)
password = sa.Column(sa.String(length=255), nullable=True)
salt = sa.Column(sa.String(length=10), nullable=True)
uid = sa.Column(sa.String(length=4), nullable=True)
session_id = sa.Column(sa.String(length=50), nullable=True)
real_name = sa.Column(sa.String(length=75), nullable=True)
email = sa.Column(sa.String(length=75), nullable=True)
totp_url = sa.Column('totpURL', sa.String(length=255), nullable=True)
def __str__(self):
return self.name or ""
class Store(Base):
"""
Represents a known store.
"""
__tablename__ = 'Stores'
storeID = sa.Column(sa.Integer(), nullable=False, primary_key=True, autoincrement=True)
id = orm.synonym('storeID')
description = sa.Column(sa.String(length=50), nullable=True)
db_host = sa.Column('dbHost', sa.String(length=50), nullable=True)
db_driver = sa.Column('dbDriver', sa.String(length=15), nullable=True)
db_user = sa.Column('dbUser', sa.String(length=25), nullable=True)
db_password = sa.Column('dbPassword', sa.String(length=25), nullable=True)
trans_db = sa.Column('transDB', sa.String(length=20), nullable=True)
op_db = sa.Column('opDB', sa.String(length=20), nullable=True)
push = sa.Column(sa.Boolean(), nullable=True, default=True)
pull = sa.Column(sa.Boolean(), nullable=True, default=True)
has_own_items = sa.Column('hasOwnItems', sa.Boolean(), nullable=True, default=True)
web_service_url = sa.Column('webServiceUrl', sa.String(length=255), nullable=True)
def __str__(self):
return self.description or ""
class MasterSuperDepartment(Base):
"""
A department may belong to more than one superdepartment, but has
one "master" superdepartment. This avoids duplicating rows in
some reports. By convention, a department's "master"
superdepartment is the one with the lowest superID.
"""
__tablename__ = 'MasterSuperDepts'
super_id = sa.Column('superID', sa.Integer(), primary_key=True, autoincrement=False, nullable=False)
department_id = sa.Column('dept_ID', sa.Integer(), primary_key=True, autoincrement=False, nullable=False)
super_name = sa.Column(sa.String(length=50), nullable=True)
def __str__(self):
return self.super_name or ""
class SuperDepartment(Base):
@ -113,33 +218,42 @@ class Department(Base):
Represents a department within the organization.
"""
__tablename__ = 'departments'
__table_args__ = (
sa.ForeignKeyConstraint(['dept_tax'], ['taxrates.id']),
)
number = sa.Column('dept_no', sa.SmallInteger(), primary_key=True, autoincrement=False, nullable=False)
name = sa.Column('dept_name', sa.String(length=30), nullable=True)
tax = sa.Column('dept_tax', sa.Boolean(), nullable=True)
tax_rate_id = sa.Column('dept_tax', sa.SmallInteger(), nullable=True)
tax_rate = orm.relationship('TaxRate')
# TODO: deprecate / remove this
tax = orm.synonym('tax_rate_id')
food_stampable = sa.Column('dept_fs', sa.Boolean(), nullable=True)
wicable = sa.Column('dept_wicable', sa.SmallInteger(), nullable=True)
active = sa.Column(sa.Boolean(), default=True)
limit = sa.Column('dept_limit', sa.Float(), nullable=True)
minimum = sa.Column('dept_minimum', sa.Float(), nullable=True)
discount = sa.Column('dept_discount', sa.Boolean(), nullable=True)
# TODO: probably should rename this attribute, but to what?
dept_see_id = sa.Column(sa.Boolean(), nullable=True)
see_id = sa.Column('dept_see_id', sa.SmallInteger(), nullable=True)
modified = sa.Column(sa.DateTime(), nullable=True)
modified_by_id = sa.Column('modifiedby', sa.Integer(), nullable=True)
margin = sa.Column(sa.Float(), nullable=False, default=0)
margin = sa.Column(sa.Float(), nullable=False)
sales_code = sa.Column('salesCode', sa.Integer(), nullable=False, default=0)
sales_code = sa.Column('salesCode', sa.Integer(), nullable=False)
member_only = sa.Column('memberOnly', sa.SmallInteger(), nullable=False, default=0)
member_only = sa.Column('memberOnly', sa.SmallInteger(), nullable=False)
def __str__(self):
return self.name or ''
@ -422,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.
"""
@ -433,120 +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)
scale_price = sa.Column('scaleprice', sa.Boolean(), nullable=True, default=False)
mix_match_code = sa.Column('mixmatchcode', sa.String(length=13), nullable=True)
modified = sa.Column(sa.DateTime(), nullable=True)
# advertised = sa.Column(sa.Boolean(), nullable=True)
tare_weight = sa.Column('tareweight', sa.Float(), nullable=True)
discount = sa.Column(sa.SmallInteger(), nullable=True)
discount_type = sa.Column('discounttype', sa.SmallInteger(), nullable=True)
line_item_discountable = sa.Column(sa.Boolean(), nullable=True)
unit_of_measure = sa.Column('unitofmeasure', sa.String(length=15), nullable=True)
wicable = sa.Column(sa.SmallInteger(), nullable=True)
quantity_enforced = sa.Column('qttyEnforced', sa.Boolean(), nullable=True)
id_enforced = sa.Column('idEnforced', sa.SmallInteger(), nullable=True)
cost = sa.Column(sa.Float(), nullable=True, default=0)
in_use = sa.Column('inUse', sa.Boolean(), nullable=True)
flags = sa.Column('numflag', sa.Integer(), nullable=True, default=0)
subdepartment_number = sa.Column('subdept', sa.SmallInteger(), nullable=True)
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, default=0)
store_id = sa.Column(sa.SmallInteger(), nullable=True, default=0)
default_vendor_id = sa.Column(sa.Integer(), nullable=True, default=0)
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, default=0)
# TODO: some older DB's might not have this? guess we'll see
last_sold = sa.Column(sa.DateTime(), nullable=True)
like_code = association_proxy(
'_like_code', 'like_code',
creator=lambda lc: ProductLikeCode(like_code=lc),
)
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']
@ -554,6 +616,18 @@ class Product(Base):
fields = filter(bool, fields)
return ' '.join(fields)
@property
def complete_size(self):
"""
Returns the "complete" size string for the product. This is based on
the :attr:`size` and :attr:`unit_of_measure` attributes.
"""
parts = [self.size, self.unit_of_measure]
parts = [(part or '').strip()
for part in parts]
parts = [part for part in parts if part]
return " ".join(parts).strip()
def __str__(self):
return self.description or ''
@ -564,6 +638,7 @@ class ProductLikeCode(Base):
"""
__tablename__ = 'upcLike'
__table_args__ = (
sa.ForeignKeyConstraint(['upc'], ['products.upc']),
sa.ForeignKeyConstraint(['likeCode'], ['likeCodes.likeCode']),
)
@ -627,6 +702,7 @@ class ProductUser(Base):
backref=orm.backref(
'user_info',
uselist=False,
cascade='all, delete-orphan',
doc="""
Reference to the :class:`ProductUser` record for this product, if any.
"""))
@ -646,7 +722,7 @@ class ProductUser(Base):
enable_online = sa.Column('enableOnline', sa.Boolean(), nullable=True)
sold_out = sa.Column('soldOut', sa.Boolean(), nullable=True, default=False)
sold_out = sa.Column('soldOut', sa.Boolean(), nullable=True)
# TODO: this was not in some older DBs
# sign_count = sa.Column('signCount', sa.SmallInteger(), nullable=True, default=1)
@ -654,6 +730,9 @@ class ProductUser(Base):
# TODO: this was not in some older DBs
# narrow = sa.Column(sa.Boolean(), nullable=True, default=False)
def __str__(self):
return str(self.product or '')
class VendorItem(Base):
"""
@ -679,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)
@ -711,6 +785,9 @@ class VendorItem(Base):
modified = sa.Column(sa.DateTime(), nullable=True)
def __str__(self):
return "{} from {}".format(self.sku, self.vendor)
class ScaleItem(Base):
"""
@ -739,8 +816,6 @@ class ScaleItem(Base):
exception_price = sa.Column('exceptionprice', sa.Numeric(precision=10, scale=2), nullable=True)
exception_price = sa.Column('exceptionprice', sa.Numeric(precision=10, scale=2), nullable=True)
weight = sa.Column(sa.SmallInteger(), nullable=True, default=0)
by_count = sa.Column('bycount', sa.Boolean(), nullable=True, default=False)
@ -843,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):
"""
@ -891,9 +943,11 @@ class MemberType(Base):
ssi = sa.Column(sa.Boolean(), 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)
# 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)
def __str__(self):
return self.description or ""
@ -1019,7 +1073,7 @@ class Customer(Base):
return "{} {}".format(self.first_name or '', self.last_name or '').strip()
class CustData(Base):
class CustomerClassic(Base):
"""
Represents a customer of the organization.
@ -1040,15 +1094,15 @@ class CustData(Base):
last_name = sa.Column('LastName', sa.String(length=30), nullable=True)
cash_back = sa.Column('CashBack', sa.Float(), nullable=False, default=60)
cash_back = sa.Column('CashBack', sa.Numeric(precision=10, scale=2), nullable=False, default=60)
balance = sa.Column('Balance', sa.Float(), nullable=False, default=0)
balance = sa.Column('Balance', sa.Numeric(precision=10, scale=2), nullable=False, default=0)
discount = sa.Column('Discount', sa.SmallInteger(), nullable=True)
member_discount_limit = sa.Column('MemDiscountLimit', sa.Float(), nullable=False, default=0)
member_discount_limit = sa.Column('MemDiscountLimit', sa.Numeric(precision=10, scale=2), nullable=False, default=0)
charge_limit = sa.Column('ChargeLimit', sa.Float(), nullable=False, default=0)
charge_limit = sa.Column('ChargeLimit', sa.Numeric(precision=10, scale=2), nullable=False, default=0)
charge_ok = sa.Column('ChargeOk', sa.Boolean(), nullable=False, default=False)
@ -1071,7 +1125,7 @@ class CustData(Base):
ssi = sa.Column('SSI', sa.Boolean(), nullable=False, default=False)
purchases = sa.Column('Purchases', sa.Float(), nullable=False, default=0)
purchases = sa.Column('Purchases', sa.Numeric(precision=10, scale=2), nullable=False, default=0)
number_of_checks = sa.Column('NumberOfChecks', sa.SmallInteger(), nullable=False, default=0)
@ -1085,7 +1139,7 @@ class CustData(Base):
member_info = orm.relationship(
'MemberInfo',
primaryjoin='MemberInfo.card_number == CustData.card_number',
primaryjoin='MemberInfo.card_number == CustomerClassic.card_number',
foreign_keys=[card_number],
uselist=False,
back_populates='customers',
@ -1097,6 +1151,10 @@ class CustData(Base):
return "{} {}".format(self.first_name or '', self.last_name or '').strip()
# TODO: deprecate / remove this
CustData = CustomerClassic
class MemberInfo(Base):
"""
Contact info regarding a member of the organization.
@ -1105,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)
@ -1125,19 +1183,22 @@ class MemberInfo(Base):
email = sa.Column('email_1', sa.String(length=50), nullable=True)
email2 = sa.Column('email_2', sa.String(length=50), nullable=True)
email2 = sa.Column('email_2', sa.String(length=50), nullable=True, doc="""
NB. this is labeled "Alt. Phone" in CORE Office member view, and
is named `altPhone` when dealing with CORE Office webservices API.
""")
ads_ok = sa.Column('ads_OK', sa.Boolean(), nullable=True, default=True)
customers = orm.relationship(
CustData,
primaryjoin=CustData.card_number == card_number,
order_by=CustData.person_number,
foreign_keys=[CustData.card_number],
CustomerClassic,
primaryjoin=CustomerClassic.card_number == card_number,
order_by=CustomerClassic.person_number,
foreign_keys=[CustomerClassic.card_number],
back_populates='member_info',
remote_side=CustData.card_number,
remote_side=CustomerClassic.card_number,
doc="""
List of :class:`CustData` instances which are associated with this member info.
List of :class:`CustomerClassic` instances which are associated with this member info.
""")
dates = orm.relationship(
@ -1145,6 +1206,7 @@ class MemberInfo(Base):
primaryjoin='MemberDate.card_number == MemberInfo.card_number',
foreign_keys='MemberDate.card_number',
cascade='all, delete-orphan',
uselist=False,
doc="""
List of date records for the member.
""",
@ -1154,6 +1216,21 @@ class MemberInfo(Base):
Reference to the member to whom the date record applies.
"""))
barcodes = orm.relationship(
'MemberBarcode',
primaryjoin='MemberBarcode.card_number == MemberInfo.card_number',
foreign_keys='MemberBarcode.card_number',
order_by='MemberBarcode.upc',
# cascade='all, delete-orphan',
doc="""
List of extra barcode records for the member.
""",
backref=orm.backref(
'member_info',
doc="""
Reference to the :class:`MemberInfo` record to which the barcode applies.
"""))
notes = orm.relationship(
'MemberNote',
primaryjoin='MemberNote.card_number == MemberInfo.card_number',
@ -1232,7 +1309,7 @@ class MemberDate(Base):
class MemberContact(Base):
"""
Contact preferences for members
Member contacts
"""
__tablename__ = 'memContact'
@ -1259,6 +1336,34 @@ class MemberContact(Base):
return str(self.preference)
class MemberContactPreference(Base):
"""
Member contact preferences
"""
__tablename__ = 'memContactPrefs'
id = sa.Column('pref_id', sa.Integer(), primary_key=True, autoincrement=False, nullable=False)
description = sa.Column('pref_description', sa.String(length=50), nullable=True)
def __str__(self):
return self.description or ""
class MemberBarcode(Base):
"""
Additional barcode for a member.
"""
__tablename__ = 'memberCards'
card_number = sa.Column('card_no', sa.Integer(), nullable=False,
primary_key=True, autoincrement=False)
upc = sa.Column(sa.String(length=13), nullable=False, primary_key=True)
def __str__(self):
return self.upc or ""
class MemberNote(Base):
"""
Additional notes for a member.
@ -1279,6 +1384,26 @@ class MemberNote(Base):
return self.note or ""
class CustomerNotification(Base):
"""
Represents a customer notification for display at the lane.
https://github.com/CORE-POS/IS4C/blob/master/fannie/classlib2.0/data/models/op/CustomerNotificationsModel.php
"""
__tablename__ = 'CustomerNotifications'
id = sa.Column('customerNotificationID', sa.Integer(), primary_key=True, autoincrement=True, nullable=False)
card_number = sa.Column('cardNo', sa.Integer(), nullable=True)
customer_id = sa.Column('customerID', sa.Integer(), nullable=True)
source = sa.Column(sa.String(length=50), nullable=True)
type = sa.Column(sa.String(length=50), nullable=True)
message = sa.Column(sa.String(length=255), nullable=True)
modifier_module = sa.Column('modifierModule', sa.String(length=50), nullable=True)
def __str__(self):
return self.message or ''
class ReasonCode(Base):
"""
Reason codes for legacy account suspensions.
@ -1323,6 +1448,10 @@ class Suspension(Base):
reason_code = sa.Column('reasoncode', sa.Integer(), nullable=True)
reason_object = orm.relationship(ReasonCode)
def __str__(self):
return "#{} on {}".format(self.card_number,
self.suspension_date)
class HouseCoupon(Base):
"""
@ -1365,13 +1494,54 @@ 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'
id = sa.Column('batchTypeID', sa.Integer(), primary_key=True, autoincrement=False, nullable=False)
# nb. this is *not* autoincrement for some reason; must
# calculate new ID manually based on max existing
id = sa.Column('batchTypeID', sa.Integer(), nullable=False, primary_key=True, autoincrement=False)
description = sa.Column('typeDesc', sa.String(length=50), nullable=True)
@ -1381,7 +1551,7 @@ class BatchType(Base):
special_order_eligible = sa.Column('specialOrderEligible', sa.Boolean(), nullable=True, default=True)
editor_ui = sa.Column('editorUI', sa.Boolean(), nullable=True, default=True)
editor_ui = sa.Column('editorUI', sa.SmallInteger(), nullable=True, default=True)
allow_single_store = sa.Column('allowSingleStore', sa.Boolean(), nullable=True, default=False)
@ -1396,9 +1566,6 @@ class Batch(Base):
Represents a batch.
"""
__tablename__ = 'batches'
__table_args__ = (
sa.ForeignKeyConstraint(['batchType'], ['batchType.batchTypeID']),
)
id = sa.Column('batchID', sa.Integer(), primary_key=True, autoincrement=True, nullable=False)
@ -1408,7 +1575,8 @@ class Batch(Base):
name = sa.Column('batchName', sa.String(length=80), nullable=True)
batch_type_id = sa.Column('batchType', sa.Integer(), nullable=True)
batch_type_id = sa.Column('batchType', sa.Integer(),
sa.ForeignKey('batchType.batchTypeID'), nullable=True)
batch_type = orm.relationship(BatchType)
discount_type = sa.Column('discountType', sa.Integer(), nullable=True)
@ -1430,16 +1598,18 @@ class BatchItem(Base):
Represents a batch "list" item.
"""
__tablename__ = 'batchList'
__table_args__ = (
sa.ForeignKeyConstraint(['batchID'], ['batches.batchID']),
)
id = sa.Column('listID', sa.Integer(), primary_key=True, autoincrement=True, nullable=False)
batch_id = sa.Column('batchID', sa.Integer(), nullable=True)
batch_id = sa.Column('batchID', sa.Integer(),
sa.ForeignKey('batches.batchID'), nullable=True)
batch = orm.relationship(Batch, backref=orm.backref('items'))
upc = sa.Column(sa.String(length=13), nullable=True)
product = orm.relationship(
Product,
primaryjoin=Product.upc == upc,
foreign_keys=[upc])
sale_price = sa.Column('salePrice', sa.Numeric(precision=9, scale=3), nullable=True)
@ -1457,3 +1627,140 @@ class BatchItem(Base):
def __str__(self):
return self.upc or ""
class PurchaseOrder(Base):
"""
Represents a purchase order.
"""
__tablename__ = 'PurchaseOrder'
# TODO: would be simpler to declare these, but is it safe?
# __table_args__ = (
# sa.ForeignKeyConstraint(['vendorID'], ['vendors.vendorID']),
# sa.ForeignKeyConstraint(['storeID'], ['Stores.storeID']),
# )
orderID = sa.Column(sa.Integer(), nullable=False, primary_key=True, autoincrement=True)
id = orm.synonym('orderID')
vendor_id = sa.Column('vendorID', sa.Integer(), nullable=True)
vendor = orm.relationship(
Vendor,
primaryjoin=Vendor.id == vendor_id,
foreign_keys=[vendor_id])
store_id = sa.Column('storeID', sa.Integer(), nullable=True)
store = orm.relationship(
Store,
primaryjoin=Store.id == store_id,
foreign_keys=[store_id])
creation_date = sa.Column('creationDate', sa.DateTime(), nullable=True)
placed = sa.Column(sa.Boolean(), nullable=True, default=False)
placed_date = sa.Column('placedDate', sa.DateTime(), nullable=True)
user_id = sa.Column('userID', sa.Integer(), nullable=True)
vendor_order_id = sa.Column('vendorOrderID', sa.String(length=25), nullable=True)
vendor_invoice_id = sa.Column('vendorInvoiceID', sa.String(length=25), nullable=True)
standing_id = sa.Column('standingID', sa.Integer(), nullable=True)
inventory_ignore = sa.Column('inventoryIgnore', sa.Boolean(), nullable=True, default=False)
transfer_id = sa.Column('transferID', sa.Integer(), nullable=True)
notes = association_proxy('notes', 'notes')
def __str__(self):
return "#{} for {}".format(self.id, self.vendor or "??")
class PurchaseOrderItem(Base):
"""
Represents a line item in a purchase order.
"""
__tablename__ = 'PurchaseOrderItems'
__table_args__ = (
sa.ForeignKeyConstraint(['orderID'], ['PurchaseOrder.orderID']),
)
order_id = sa.Column('orderID', sa.Integer(), nullable=False,
primary_key=True, autoincrement=False)
order = orm.relationship(PurchaseOrder, backref=orm.backref('items'))
sku = sa.Column(sa.String(length=13), nullable=False,
primary_key=True)
quantity = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True)
unit_cost = sa.Column('unitCost', sa.Numeric(precision=10, scale=4), nullable=True)
case_size = sa.Column('caseSize', sa.Float(), nullable=True)
received_date = sa.Column('receivedDate', sa.DateTime(), nullable=True)
received_quantity = sa.Column('receivedQty', sa.Float(), nullable=True)
received_total_cost = sa.Column('receivedTotalCost', sa.Numeric(precision=10, scale=4), nullable=True)
unit_size = sa.Column('unitSize', sa.String(length=25), nullable=True)
brand = sa.Column(sa.String(length=50), nullable=True)
description = sa.Column(sa.String(length=50), nullable=True)
internal_upc = sa.Column('internalUPC', sa.String(length=13), nullable=True)
sales_code = sa.Column('salesCode', sa.Integer(), nullable=True)
is_special_order = sa.Column('isSpecialOrder', sa.Boolean(), nullable=True,
default=False)
# TODO: this probably is FK to e.g. User.id ?
received_by = sa.Column('receivedBy', sa.Integer(), nullable=True,
default=0)
def __str__(self):
return self.description or ""
class PurchaseOrderNote(Base):
"""
Represents a note attached to a purchase order.
"""
__tablename__ = 'PurchaseOrderNotes'
__table_args__ = (
sa.ForeignKeyConstraint(['orderID'], ['PurchaseOrder.orderID']),
)
order_id = sa.Column('orderID', sa.Integer(), nullable=False,
primary_key=True, autoincrement=False)
order = orm.relationship(PurchaseOrder, backref=orm.backref('notes'))
notes = sa.Column(sa.Text(), nullable=True)
def __str__(self):
return self.notes or ""
# 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

View file

@ -2,7 +2,7 @@
################################################################################
#
# pyCOREPOS -- Python Interface to CORE POS
# Copyright © 2018-2020 Lance Edgar
# Copyright © 2018-2025 Lance Edgar
#
# This file is part of pyCOREPOS.
#
@ -24,102 +24,68 @@
CORE POS Transaction Data Model
"""
from __future__ import unicode_literals, absolute_import
import six
import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import orm
from corepos.db.common import trans as common
Base = declarative_base()
Base = orm.declarative_base()
@six.python_2_unicode_compatible
class TransactionDetail(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):
"""
Represents a POS transaction detail record.
Represents a member equity payment.
"""
__tablename__ = 'stockpurchases'
card_number = sa.Column('card_no', sa.Integer(), nullable=False, primary_key=True, autoincrement=False)
amount = sa.Column('stockPurchase', sa.Numeric(precision=10, scale=2), nullable=True)
datetime = sa.Column('tdate', sa.DateTime(), nullable=True, primary_key=True, autoincrement=False)
transaction_number = sa.Column('trans_num', sa.String(length=50), nullable=True, primary_key=True)
transaction_id = sa.Column('trans_id', sa.Integer(), nullable=True)
department_number = sa.Column('dept', sa.Integer(), nullable=True, primary_key=True, autoincrement=False)
def __str__(self):
return f"#{self.card_number} for ${self.amount}"
class EquityLiveBalance(Base):
__tablename__ = 'equity_live_balance'
member_number = sa.Column('memnum', sa.Integer(), nullable=False, primary_key=True, autoincrement=False)
payments = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True)
start_date = sa.Column('startdate', sa.DateTime(), nullable=True)
class DTransactionBase(common.TransactionDetailBase):
"""
Base class for ``dtransactions`` and similar models.
"""
store_row_id = sa.Column(sa.Integer(), primary_key=True, nullable=False)
pos_row_id = sa.Column(sa.Integer(), nullable=True)
store_id = sa.Column(sa.Integer(), nullable=True, default=0)
date_time = sa.Column('datetime', sa.DateTime(), nullable=True)
class DTransaction(DTransactionBase, Base):
"""
Data model for ``dtransactions`` table.
"""
__tablename__ = 'dtransactions'
# store
store_row_id = sa.Column(sa.Integer(), primary_key=True, nullable=False)
store_id = sa.Column(sa.Integer(), nullable=True, default=0)
# register
register_number = sa.Column('register_no', sa.Integer(), nullable=True)
pos_row_id = sa.Column(sa.Integer(), nullable=True)
# txn
transaction_id = sa.Column('trans_id', sa.Integer(), nullable=True)
transaction_number = sa.Column('trans_no', sa.Integer(), nullable=True)
transaction_type = sa.Column('trans_type', sa.String(length=1), nullable=True)
transaction_subtype = sa.Column('trans_subtype', sa.String(length=2), nullable=True)
transaction_status = sa.Column('trans_status', sa.String(length=1), nullable=True)
# timestamps
date_time = sa.Column('datetime', sa.DateTime(), nullable=True)
# cashier
employee_number = sa.Column('emp_no', sa.Integer(), nullable=True)
# customer
card_number = sa.Column('card_no', sa.Integer(), nullable=True)
member_type = sa.Column('memType', sa.Integer(), nullable=True)
staff = sa.Column(sa.Boolean(), nullable=True)
##############################
# remainder is "line item" ...
##############################
upc = sa.Column(sa.String(length=13), nullable=True)
department_number = sa.Column('department', sa.Integer(), nullable=True)
description = sa.Column(sa.String(length=30), nullable=True)
quantity = sa.Column(sa.Float(), nullable=True)
scale = sa.Column(sa.Boolean(), nullable=True, default=False)
cost = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True)
unit_price = sa.Column('unitPrice', sa.Numeric(precision=10, scale=2), nullable=True)
total = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True)
reg_price = sa.Column('regPrice', sa.Numeric(precision=10, scale=2), nullable=True)
tax = sa.Column(sa.Boolean(), nullable=True)
food_stamp = sa.Column('foodstamp', sa.Boolean(), nullable=True)
discount = sa.Column(sa.Numeric(precision=10, scale=2), nullable=True)
member_discount = sa.Column('memDiscount', sa.Numeric(precision=10, scale=2), nullable=True)
discountable = sa.Column(sa.Boolean(), nullable=True)
discount_type = sa.Column('discounttype', sa.Integer(), nullable=True)
voided = sa.Column(sa.Boolean(), nullable=True)
percent_discount = sa.Column('percentDiscount', sa.Integer(), nullable=True)
item_quantity = sa.Column('ItemQtty', sa.Float(), nullable=True)
volume_discount_type = sa.Column('volDiscType', sa.Integer(), nullable=True)
volume = sa.Column(sa.Integer(), nullable=True)
volume_special = sa.Column('VolSpecial', sa.Numeric(precision=10, scale=2), nullable=True)
mix_match = sa.Column('mixMatch', sa.String(length=13), nullable=True)
matched = sa.Column(sa.Boolean(), nullable=True)
num_flag = sa.Column('numflag', sa.Integer(), nullable=True, default=0)
char_flag = sa.Column('charflag', sa.String(length=2), nullable=True)
def __str__(self):
return self.description or ''
# TODO: deprecate / remove this
TransactionDetail = DTransaction

View file

@ -0,0 +1,32 @@
# -*- coding: utf-8; -*-
################################################################################
#
# pyCOREPOS -- Python Interface to CORE POS
# Copyright © 2018-2023 Lance Edgar
#
# This file is part of pyCOREPOS.
#
# pyCOREPOS is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# pyCOREPOS is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# pyCOREPOS. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
"Archive" Transaction Database Interface
"""
import warnings
warnings.warn("The `corepos.db.office_trans_archive` module is deprecated! "
"Please use `corepos.db.office_arch` instead.",
DeprecationWarning, stacklevel=2)
from corepos.db.office_arch import *

View file

@ -0,0 +1,32 @@
# -*- coding: utf-8; -*-
################################################################################
#
# pyCOREPOS -- Python Interface to CORE POS
# Copyright © 2018-2023 Lance Edgar
#
# This file is part of pyCOREPOS.
#
# pyCOREPOS is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# pyCOREPOS is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# pyCOREPOS. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
CORE POS Transaction Data Model
"""
import warnings
warnings.warn("The `corepos.db.office_trans_archive.model` module is deprecated! "
"Please use `corepos.db.office_arch.model` instead.",
DeprecationWarning, stacklevel=2)
from corepos.db.office_arch.model import *

View file

@ -2,7 +2,7 @@
################################################################################
#
# pyCOREPOS -- Python Interface to CORE POS
# Copyright © 2018-2019 Lance Edgar
# Copyright © 2018-2024 Lance Edgar
#
# This file is part of pyCOREPOS.
#
@ -24,22 +24,43 @@
CORE POS enumeration constants
"""
from __future__ import unicode_literals, absolute_import
try:
from collections import OrderedDict
except ImportError:
from ordereddict import OrderedDict
from collections import OrderedDict
from enum import Enum
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, "Price Change"),
(BATCH_DISCOUNT_TYPE_PRICE_CHANGE, "None (Change regular price)"),
(BATCH_DISCOUNT_TYPE_SALE_EVERYONE, "Sale for everyone"),
(BATCH_DISCOUNT_TYPE_SALE_RESTRICTED, "Member/Owner only sale"),
(BATCH_DISCOUNT_TYPE_SALE_RESTRICTED, "Sale for Members"),
(BATCH_DISCOUNT_TYPE_SLIDING_PERCENT, "Sliding % Off for Members"),
(BATCH_DISCOUNT_TYPE_SLIDING_AMOUNT, "Sliding $ Off for Members"),
(BATCH_DISCOUNT_TYPE_TRACKING, "Tracking (does not change any prices)"),
])
BATCH_EDITOR_UI_STANDARD = 1
BATCH_EDITOR_UI_PAIRED_SALE = 2
BATCH_EDITOR_UI_PARTIAL = 3
BATCH_EDITOR_UI_TRACKING = 4
BATCH_EDITOR_UI = OrderedDict([
(BATCH_EDITOR_UI_STANDARD, "Standard"),
(BATCH_EDITOR_UI_PAIRED_SALE, "Paired Sale"),
(BATCH_EDITOR_UI_PARTIAL, "Partial"),
(BATCH_EDITOR_UI_TRACKING, "Tracking"),
])

49
pyproject.toml Normal file
View file

@ -0,0 +1,49 @@
[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"]

View file

@ -1,97 +0,0 @@
# -*- coding: utf-8; -*-
################################################################################
#
# pyCOREPOS -- Python Interface to CORE POS
# Copyright © 2018-2020 Lance Edgar
#
# This file is part of pyCOREPOS.
#
# pyCOREPOS is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# pyCOREPOS is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# pyCOREPOS. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
import os
import sys
from setuptools import setup, find_packages
here = os.path.abspath(os.path.dirname(__file__))
exec(open(os.path.join(here, 'corepos', '_version.py')).read())
README = open(os.path.join(here, 'README.rst')).read()
requires = [
#
# Version numbers within comments below have specific meanings.
# Basically the 'low' value is a "soft low," and 'high' a "soft high."
# In other words:
#
# If either a 'low' or 'high' value exists, the primary point to be
# made about the value is that it represents the most current (stable)
# version available for the package (assuming typical public access
# methods) whenever this project was started and/or documented.
# Therefore:
#
# If a 'low' version is present, you should know that attempts to use
# versions of the package significantly older than the 'low' version
# may not yield happy results. (A "hard" high limit may or may not be
# indicated by a true version requirement.)
#
# Similarly, if a 'high' version is present, and especially if this
# project has laid dormant for a while, you may need to refactor a bit
# when attempting to support a more recent version of the package. (A
# "hard" low limit should be indicated by a true version requirement
# when a 'high' version is present.)
#
# In any case, developers and other users are encouraged to play
# outside the lines with regard to these soft limits. If bugs are
# encountered then they should be filed as such.
#
# package # low high
'mysql-connector-python', # 8.0.6
'requests', # 2.23.0
'six', # 1.12.0
'SQLAlchemy', # 0.9.8
]
setup(
name = "pyCOREPOS",
version = __version__,
author = "Lance Edgar",
author_email = "lance@edbob.org",
url = "https://rattailproject.org/",
license = "GNU GPL v3",
description = "Python Interface to CORE POS",
long_description = README,
classifiers = [
'Development Status :: 3 - Alpha',
'Environment :: Console',
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
'Natural Language :: English',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Topic :: Office/Business',
'Topic :: Software Development :: Libraries :: Python Modules',
],
install_requires = requires,
packages = find_packages(),
)

View file

@ -2,7 +2,7 @@
################################################################################
#
# pyCOREPOS -- Python Interface to CORE POS
# Copyright © 2018-2020 Lance Edgar
# Copyright © 2018-2024 Lance Edgar
#
# This file is part of pyCOREPOS.
#
@ -25,20 +25,32 @@ Tasks for 'pyCOREPOS' package
"""
import os
import re
import shutil
from invoke import task
here = os.path.abspath(os.path.dirname(__file__))
exec(open(os.path.join(here, 'corepos', '_version.py')).read())
__version__ = None
pattern = re.compile(r'^version = "(\d+\.\d+\.\d+)"$')
with open(os.path.join(here, 'pyproject.toml'), 'rt') as f:
for line in f:
line = line.rstrip('\n')
match = pattern.match(line)
if match:
__version__ = match.group(1)
break
if not __version__:
raise RuntimeError("could not parse version!")
@task
def release(ctx):
def release(c):
"""
Release a new version of 'pyCOREPOS'.
"""
if os.path.exists('pyCOREPOS.egg-info'):
shutil.rmtree('pyCOREPOS.egg-info')
ctx.run('python setup.py sdist --formats=gztar')
ctx.run('twine upload dist/pyCOREPOS-{}.tar.gz'.format(__version__))
c.run('python -m build --sdist')
c.run('twine upload dist/pycorepos-{}.tar.gz'.format(__version__))