Compare commits
No commits in common. "master" and "v0.3.5" have entirely different histories.
26
CHANGELOG.md
26
CHANGELOG.md
|
@ -5,32 +5,6 @@ All notable changes to rattail-corepos will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
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).
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## v0.3.9 (2024-08-19)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- improve logic for matching CORE stock purchase to Rattail payment
|
|
||||||
|
|
||||||
## v0.3.8 (2024-08-18)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- avoid deprecated base class for config extension
|
|
||||||
- work around, log error when datasync can't locate member
|
|
||||||
|
|
||||||
## v0.3.7 (2024-08-13)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- improve `core-office anonymize` command logic
|
|
||||||
|
|
||||||
## v0.3.6 (2024-08-06)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- fix DELETE triggers for `meminfo`, `employees`
|
|
||||||
- avoid deprecated `AppProvider.load_object()` method
|
|
||||||
|
|
||||||
## v0.3.5 (2024-07-14)
|
## v0.3.5 (2024-07-14)
|
||||||
|
|
||||||
### Fix
|
### Fix
|
||||||
|
|
11
README.md
11
README.md
|
@ -1,11 +0,0 @@
|
||||||
|
|
||||||
# rattail-corepos
|
|
||||||
|
|
||||||
Rattail is a retail software framework, released under the GNU General Public
|
|
||||||
License.
|
|
||||||
|
|
||||||
This package contains software interfaces for the [CORE
|
|
||||||
POS](https://github.com/CORE-POS/IS4C) system.
|
|
||||||
|
|
||||||
Please see Rattail's [home page](https://rattailproject.org/) for more
|
|
||||||
information.
|
|
14
README.rst
Normal file
14
README.rst
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
rattail_corepos
|
||||||
|
===============
|
||||||
|
|
||||||
|
Rattail is a retail software framework, released under the GNU General Public
|
||||||
|
License.
|
||||||
|
|
||||||
|
This package contains software interfaces for the `CORE POS`_ system.
|
||||||
|
|
||||||
|
.. _`CORE POS`: https://github.com/CORE-POS/IS4C
|
||||||
|
|
||||||
|
Please see Rattail's `home page`_ for more information.
|
||||||
|
|
||||||
|
.. _`home page`: https://rattailproject.org/
|
|
@ -6,9 +6,9 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "rattail_corepos"
|
name = "rattail_corepos"
|
||||||
version = "0.3.9"
|
version = "0.3.5"
|
||||||
description = "Rattail Software Interfaces for CORE POS"
|
description = "Rattail Software Interfaces for CORE POS"
|
||||||
readme = "README.md"
|
readme = "README.rst"
|
||||||
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
|
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
|
||||||
license = {text = "GNU GPL v3+"}
|
license = {text = "GNU GPL v3+"}
|
||||||
classifiers = [
|
classifiers = [
|
||||||
|
@ -33,10 +33,10 @@ dependencies = [
|
||||||
|
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://rattailproject.org"
|
Homepage = "https://redmine.rattailproject.org/projects/corepos-integration"
|
||||||
Repository = "https://forgejo.wuttaproject.org/rattail/rattail-corepos"
|
Repository = "https://kallithea.rattailproject.org/rattail-project/rattail-corepos"
|
||||||
Issues = "https://forgejo.wuttaproject.org/rattail/rattail-corepos/issues"
|
Issues = "https://redmine.rattailproject.org/projects/corepos-integration/issues"
|
||||||
Changelog = "https://forgejo.wuttaproject.org/rattail/rattail-corepos/src/branch/master/CHANGELOG.md"
|
Changelog = "https://kallithea.rattailproject.org/rattail-project/rattail-corepos/files/master/CHANGELOG.md"
|
||||||
|
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|
|
@ -34,9 +34,9 @@ class CoreProvider(RattailProvider):
|
||||||
|
|
||||||
def get_corepos_handler(self, **kwargs):
|
def get_corepos_handler(self, **kwargs):
|
||||||
if not hasattr(self, 'corepos_handler'):
|
if not hasattr(self, 'corepos_handler'):
|
||||||
spec = self.config.get('rattail.corepos.handler',
|
spec = self.config.get('rattail', 'corepos.handler',
|
||||||
default='rattail_corepos.app:CoreHandler')
|
default='rattail_corepos.app:CoreHandler')
|
||||||
factory = self.app.load_object(spec)
|
factory = self.load_object(spec)
|
||||||
self.corepos_handler = factory(self.config, **kwargs)
|
self.corepos_handler = factory(self.config, **kwargs)
|
||||||
return self.corepos_handler
|
return self.corepos_handler
|
||||||
|
|
||||||
|
|
|
@ -26,11 +26,12 @@ Rattail-COREPOS Config Extension
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from wuttjamaican.conf import WuttaConfigExtension
|
|
||||||
from wuttjamaican.db.conf import get_engines
|
from wuttjamaican.db.conf import get_engines
|
||||||
|
|
||||||
|
from rattail.config import ConfigExtension
|
||||||
|
|
||||||
class RattailCOREPOSExtension(WuttaConfigExtension):
|
|
||||||
|
class RattailCOREPOSExtension(ConfigExtension):
|
||||||
"""
|
"""
|
||||||
Config extension for Rattail-COREPOS
|
Config extension for Rattail-COREPOS
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -37,13 +37,8 @@ class Anonymizer(GenericHandler):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def anonymize_all(self, dbkey=None, dry_run=False, progress=None):
|
def anonymize_all(self, dbkey=None, dry_run=False, progress=None):
|
||||||
# nb. these must be installed with:
|
|
||||||
# pip install names us zipcodes
|
|
||||||
import names
|
import names
|
||||||
import us
|
import us
|
||||||
import zipcodes
|
|
||||||
|
|
||||||
self.all_zipcodes = zipcodes.list_all()
|
|
||||||
|
|
||||||
core_handler = self.app.get_corepos_handler()
|
core_handler = self.app.get_corepos_handler()
|
||||||
op_session = core_handler.make_session_office_op(dbkey=dbkey)
|
op_session = core_handler.make_session_office_op(dbkey=dbkey)
|
||||||
|
@ -51,56 +46,45 @@ class Anonymizer(GenericHandler):
|
||||||
|
|
||||||
states = [state.abbr for state in us.states.STATES]
|
states = [state.abbr for state in us.states.STATES]
|
||||||
|
|
||||||
# custdata
|
|
||||||
customers = op_session.query(op_model.CustomerClassic)\
|
|
||||||
.order_by(op_model.CustomerClassic.card_number,
|
|
||||||
op_model.CustomerClassic.person_number)\
|
|
||||||
.all()
|
|
||||||
blueline_template = get_blueline_template(self.config)
|
|
||||||
customers_by_card_number = {}
|
|
||||||
|
|
||||||
def anon_custdata(customer, i):
|
|
||||||
customer.first_name = names.get_first_name()
|
|
||||||
customer.last_name = names.get_last_name()
|
|
||||||
customer.blue_line = make_blueline(self.config, customer,
|
|
||||||
template=blueline_template)
|
|
||||||
customers_by_card_number.setdefault(customer.card_number, []).append(customer)
|
|
||||||
|
|
||||||
self.app.progress_loop(anon_custdata, customers, progress,
|
|
||||||
message="Anonymizing custdata")
|
|
||||||
|
|
||||||
# meminfo
|
# meminfo
|
||||||
members = op_session.query(op_model.MemberInfo).all()
|
members = op_session.query(op_model.MemberInfo).all()
|
||||||
|
members_by_card_number = {}
|
||||||
|
|
||||||
def anon_meminfo(member, i):
|
def anon_meminfo(member, i):
|
||||||
if member.first_name:
|
member.first_name = names.get_first_name()
|
||||||
member.first_name = names.get_first_name()
|
member.last_name = names.get_last_name()
|
||||||
if member.last_name:
|
member.other_first_name = names.get_first_name()
|
||||||
member.last_name = names.get_last_name()
|
member.other_last_name = names.get_last_name()
|
||||||
if member.other_first_name:
|
|
||||||
member.other_first_name = names.get_first_name()
|
|
||||||
if member.other_last_name:
|
|
||||||
member.other_last_name = names.get_last_name()
|
|
||||||
|
|
||||||
member.street = '123 Main St.'
|
member.street = '123 Main St.'
|
||||||
member.city = 'Anytown'
|
member.city = 'Anytown'
|
||||||
member.state = random.choice(states)
|
member.state = random.choice(states)
|
||||||
member.zip = self.random_zipcode()
|
member.zipcode = self.random_zipcode()
|
||||||
member.phone = self.random_phone()
|
member.phone = self.random_phone()
|
||||||
|
member.email = self.random_email()
|
||||||
customers = customers_by_card_number.get(member.card_number)
|
|
||||||
if customers:
|
|
||||||
customer = customers[0]
|
|
||||||
member.email = f'{customer.first_name}_{customer.last_name}@mailinator.com'\
|
|
||||||
.lower()
|
|
||||||
else:
|
|
||||||
member.email = self.random_email()
|
|
||||||
|
|
||||||
member.notes.clear()
|
member.notes.clear()
|
||||||
|
members_by_card_number[member.card_number] = member
|
||||||
|
|
||||||
self.app.progress_loop(anon_meminfo, members, progress,
|
self.app.progress_loop(anon_meminfo, members, progress,
|
||||||
message="Anonymizing meminfo")
|
message="Anonymizing meminfo")
|
||||||
|
|
||||||
|
# custdata
|
||||||
|
customers = op_session.query(op_model.CustomerClassic).all()
|
||||||
|
blueline_template = get_blueline_template(self.config)
|
||||||
|
|
||||||
|
def anon_custdata(customer, i):
|
||||||
|
member = members_by_card_number.get(customer.card_number)
|
||||||
|
if member:
|
||||||
|
customer.first_name = member.first_name
|
||||||
|
customer.last_name = member.last_name
|
||||||
|
else:
|
||||||
|
customer.first_name = names.get_first_name()
|
||||||
|
customer.last_name = names.get_last_name()
|
||||||
|
customer.blue_line = make_blueline(self.config, customer,
|
||||||
|
template=blueline_template)
|
||||||
|
|
||||||
|
self.app.progress_loop(anon_custdata, customers, progress,
|
||||||
|
message="Anonymizing custdata")
|
||||||
|
|
||||||
# Customers
|
# Customers
|
||||||
customers = op_session.query(op_model.Customer).all()
|
customers = op_session.query(op_model.Customer).all()
|
||||||
|
|
||||||
|
@ -149,7 +133,9 @@ class Anonymizer(GenericHandler):
|
||||||
import names
|
import names
|
||||||
name = names.get_full_name()
|
name = names.get_full_name()
|
||||||
name = name.replace(' ', '_')
|
name = name.replace(' ', '_')
|
||||||
return f'{name}@mailinator.com'.lower()
|
return f'{name}@mailinator.com'
|
||||||
|
|
||||||
def random_zipcode(self):
|
def random_zipcode(self):
|
||||||
return random.choice(self.all_zipcodes)['zip_code']
|
digits = [random.choice('0123456789')
|
||||||
|
for i in range(5)]
|
||||||
|
return ''.join(digits)
|
||||||
|
|
|
@ -95,13 +95,6 @@ def anonymize(
|
||||||
"\tpip install us\n")
|
"\tpip install us\n")
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
try:
|
|
||||||
import zipcodes
|
|
||||||
except ImportError:
|
|
||||||
sys.stderr.write("must install the `zipcodes` package first!\n\n"
|
|
||||||
"\tpip install zipcodes\n")
|
|
||||||
sys.exit(2)
|
|
||||||
|
|
||||||
anonymizer = Anonymizer(config)
|
anonymizer = Anonymizer(config)
|
||||||
anonymizer.anonymize_all(dbkey=dbkey, dry_run=dry_run,
|
anonymizer.anonymize_all(dbkey=dbkey, dry_run=dry_run,
|
||||||
progress=progress)
|
progress=progress)
|
||||||
|
|
|
@ -222,7 +222,7 @@ class CoreTriggerHandler(GenericHandler):
|
||||||
op_session.execute(sa.text(f"""
|
op_session.execute(sa.text(f"""
|
||||||
CREATE TRIGGER record_meminfo_delete
|
CREATE TRIGGER record_meminfo_delete
|
||||||
AFTER DELETE ON meminfo
|
AFTER DELETE ON meminfo
|
||||||
FOR EACH ROW INSERT INTO {changes_table} (object_type, object_key, deleted) VALUES ('Member', CONVERT(OLD.card_no, CHAR), 1);
|
FOR EACH ROW INSERT INTO {changes_table} (object_type, object_key, deleted) VALUES ('Member', CONVERT(OLD.card_no, CHAR));
|
||||||
"""))
|
"""))
|
||||||
|
|
||||||
def create_triggers_employees(self, op_session, changes_table):
|
def create_triggers_employees(self, op_session, changes_table):
|
||||||
|
@ -242,7 +242,7 @@ class CoreTriggerHandler(GenericHandler):
|
||||||
op_session.execute(sa.text(f"""
|
op_session.execute(sa.text(f"""
|
||||||
CREATE TRIGGER record_employees_delete
|
CREATE TRIGGER record_employees_delete
|
||||||
AFTER DELETE ON employees
|
AFTER DELETE ON employees
|
||||||
FOR EACH ROW INSERT INTO {changes_table} (object_type, object_key, deleted) VALUES ('Employee', CONVERT(OLD.emp_no, CHAR), 1);
|
FOR EACH ROW INSERT INTO {changes_table} (object_type, object_key, deleted) VALUES ('Employee', CONVERT(OLD.emp_no, CHAR));
|
||||||
"""))
|
"""))
|
||||||
|
|
||||||
def create_triggers_departments(self, op_session, changes_table):
|
def create_triggers_departments(self, op_session, changes_table):
|
||||||
|
|
|
@ -24,16 +24,11 @@
|
||||||
DataSync for Rattail DB
|
DataSync for Rattail DB
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
|
|
||||||
from rattail.datasync import DataSyncImportConsumer
|
from rattail.datasync import DataSyncImportConsumer
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class FromCOREAPIToRattail(DataSyncImportConsumer):
|
class FromCOREAPIToRattail(DataSyncImportConsumer):
|
||||||
"""
|
"""
|
||||||
Consumer for CORE POS (API) -> Rattail datasync
|
Consumer for CORE POS (API) -> Rattail datasync
|
||||||
|
@ -74,11 +69,6 @@ class FromCOREAPIToRattail(DataSyncImportConsumer):
|
||||||
else:
|
else:
|
||||||
# import member data from API, into various Rattail tables
|
# import member data from API, into various Rattail tables
|
||||||
member = self.get_host_object(session, change)
|
member = self.get_host_object(session, change)
|
||||||
if not member:
|
|
||||||
# TODO: should log.warning() instead but for now i
|
|
||||||
# need to see this in action and further troubleshoot
|
|
||||||
log.error("CORE member not found for change: %s", change)
|
|
||||||
continue
|
|
||||||
self.process_change(session, self.importers['Customer'],
|
self.process_change(session, self.importers['Customer'],
|
||||||
host_object=member)
|
host_object=member)
|
||||||
shoppers = self.importers['CustomerShopper'].get_shoppers_for_member(member)
|
shoppers = self.importers['CustomerShopper'].get_shoppers_for_member(member)
|
||||||
|
|
|
@ -953,17 +953,7 @@ class MemberEquityPaymentImporter(FromCOREPOS, corepos_importing.model.MemberEqu
|
||||||
if len(match) == 1:
|
if len(match) == 1:
|
||||||
return match[0]
|
return match[0]
|
||||||
|
|
||||||
# then try to match on date only, not time
|
# nb. avoid datetime for this one
|
||||||
match = [payment for payment in payments
|
|
||||||
if payment.corepos_transaction_number == stock_purchase.transaction_number
|
|
||||||
and payment.corepos_transaction_id == stock_purchase.transaction_id
|
|
||||||
and payment.amount == stock_purchase.amount
|
|
||||||
and payment.corepos_department_number == stock_purchase.department_number
|
|
||||||
and self.app.localtime(payment.corepos_datetime, from_utc=True).date() == dt.date()]
|
|
||||||
if len(match) == 1:
|
|
||||||
return match[0]
|
|
||||||
|
|
||||||
# nb. avoid date/time for this one
|
|
||||||
matches = [payment for payment in payments
|
matches = [payment for payment in payments
|
||||||
if payment.corepos_transaction_number == stock_purchase.transaction_number
|
if payment.corepos_transaction_number == stock_purchase.transaction_number
|
||||||
and payment.corepos_transaction_id == stock_purchase.transaction_id
|
and payment.corepos_transaction_id == stock_purchase.transaction_id
|
||||||
|
@ -976,9 +966,6 @@ class MemberEquityPaymentImporter(FromCOREPOS, corepos_importing.model.MemberEqu
|
||||||
stock_purchase.amount,
|
stock_purchase.amount,
|
||||||
stock_purchase.datetime)
|
stock_purchase.datetime)
|
||||||
|
|
||||||
# TODO: now that we try to match on date above, this logic
|
|
||||||
# may no longer be necssary/useful?
|
|
||||||
|
|
||||||
# so there is one match, but its timestamp may be way off,
|
# so there is one match, but its timestamp may be way off,
|
||||||
# so let's also make sure at least date matches
|
# so let's also make sure at least date matches
|
||||||
payment = matches[0]
|
payment = matches[0]
|
||||||
|
|
Loading…
Reference in a new issue