Compare commits

...

12 commits

Author SHA1 Message Date
Lance Edgar 49d897ab86 docs: update project links, kallithea -> forgejo 2024-09-14 12:11:06 -05:00
Lance Edgar 7cc5c7abad docs: use markdown for readme file 2024-09-13 18:46:06 -05:00
Lance Edgar c954c4304b bump: version 0.3.8 → 0.3.9 2024-08-19 12:02:28 -05:00
Lance Edgar 57d3a21e43 fix: improve logic for matching CORE stock purchase to Rattail payment
we were already "trying" to match on date only, but only as a sort of
fallback.  now we still try "exact" date/time match first but then
also an explicit date match, before other fallback logic
2024-08-19 11:30:44 -05:00
Lance Edgar 666fb747bb bump: version 0.3.7 → 0.3.8 2024-08-18 20:08:25 -05:00
Lance Edgar 6072a359fd fix: avoid deprecated base class for config extension 2024-08-16 10:14:07 -05:00
Lance Edgar 802c8ab87b fix: work around, log error when datasync can't locate member 2024-08-14 09:12:32 -05:00
Lance Edgar b4f8bb9c93 bump: version 0.3.6 → 0.3.7 2024-08-13 11:24:36 -05:00
Lance Edgar c3441f700d fix: improve core-office anonymize command logic
- prefer setting `custdata` names over `meminfo`
- use custdata name for basis of `meminfo.email`
- use "real" random zipcode
- fix attr assignment for `meminfo.zip`
2024-08-10 12:00:48 -05:00
Lance Edgar 9cc137d29a bump: version 0.3.5 → 0.3.6 2024-08-06 23:22:14 -05:00
Lance Edgar 339c718b32 fix: fix DELETE triggers for meminfo, employees
whoops not sure how those got missed
2024-08-06 11:41:02 -05:00
Lance Edgar 6cece2c41e fix: avoid deprecated AppProvider.load_object() method 2024-07-18 08:32:45 -05:00
11 changed files with 127 additions and 61 deletions

View file

@ -5,6 +5,32 @@ 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/)
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)
### Fix

11
README.md Normal file
View file

@ -0,0 +1,11 @@
# 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.

View file

@ -1,14 +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`_ system.
.. _`CORE POS`: https://github.com/CORE-POS/IS4C
Please see Rattail's `home page`_ for more information.
.. _`home page`: https://rattailproject.org/

View file

@ -6,9 +6,9 @@ build-backend = "hatchling.build"
[project]
name = "rattail_corepos"
version = "0.3.5"
version = "0.3.9"
description = "Rattail Software Interfaces for CORE POS"
readme = "README.rst"
readme = "README.md"
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
license = {text = "GNU GPL v3+"}
classifiers = [
@ -33,10 +33,10 @@ dependencies = [
[project.urls]
Homepage = "https://redmine.rattailproject.org/projects/corepos-integration"
Repository = "https://kallithea.rattailproject.org/rattail-project/rattail-corepos"
Issues = "https://redmine.rattailproject.org/projects/corepos-integration/issues"
Changelog = "https://kallithea.rattailproject.org/rattail-project/rattail-corepos/files/master/CHANGELOG.md"
Homepage = "https://rattailproject.org"
Repository = "https://forgejo.wuttaproject.org/rattail/rattail-corepos"
Issues = "https://forgejo.wuttaproject.org/rattail/rattail-corepos/issues"
Changelog = "https://forgejo.wuttaproject.org/rattail/rattail-corepos/src/branch/master/CHANGELOG.md"
[project.scripts]

View file

@ -34,9 +34,9 @@ class CoreProvider(RattailProvider):
def get_corepos_handler(self, **kwargs):
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')
factory = self.load_object(spec)
factory = self.app.load_object(spec)
self.corepos_handler = factory(self.config, **kwargs)
return self.corepos_handler

View file

@ -26,12 +26,11 @@ Rattail-COREPOS Config Extension
import warnings
from wuttjamaican.conf import WuttaConfigExtension
from wuttjamaican.db.conf import get_engines
from rattail.config import ConfigExtension
class RattailCOREPOSExtension(ConfigExtension):
class RattailCOREPOSExtension(WuttaConfigExtension):
"""
Config extension for Rattail-COREPOS
"""

View file

@ -37,8 +37,13 @@ class Anonymizer(GenericHandler):
"""
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 us
import zipcodes
self.all_zipcodes = zipcodes.list_all()
core_handler = self.app.get_corepos_handler()
op_session = core_handler.make_session_office_op(dbkey=dbkey)
@ -46,45 +51,56 @@ class Anonymizer(GenericHandler):
states = [state.abbr for state in us.states.STATES]
# meminfo
members = op_session.query(op_model.MemberInfo).all()
members_by_card_number = {}
def anon_meminfo(member, i):
member.first_name = names.get_first_name()
member.last_name = names.get_last_name()
member.other_first_name = names.get_first_name()
member.other_last_name = names.get_last_name()
member.street = '123 Main St.'
member.city = 'Anytown'
member.state = random.choice(states)
member.zipcode = self.random_zipcode()
member.phone = self.random_phone()
member.email = self.random_email()
member.notes.clear()
members_by_card_number[member.card_number] = member
self.app.progress_loop(anon_meminfo, members, progress,
message="Anonymizing meminfo")
# custdata
customers = op_session.query(op_model.CustomerClassic).all()
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):
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.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
members = op_session.query(op_model.MemberInfo).all()
def anon_meminfo(member, i):
if member.first_name:
member.first_name = names.get_first_name()
if member.last_name:
member.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.city = 'Anytown'
member.state = random.choice(states)
member.zip = self.random_zipcode()
member.phone = self.random_phone()
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()
self.app.progress_loop(anon_meminfo, members, progress,
message="Anonymizing meminfo")
# Customers
customers = op_session.query(op_model.Customer).all()
@ -133,9 +149,7 @@ class Anonymizer(GenericHandler):
import names
name = names.get_full_name()
name = name.replace(' ', '_')
return f'{name}@mailinator.com'
return f'{name}@mailinator.com'.lower()
def random_zipcode(self):
digits = [random.choice('0123456789')
for i in range(5)]
return ''.join(digits)
return random.choice(self.all_zipcodes)['zip_code']

View file

@ -95,6 +95,13 @@ def anonymize(
"\tpip install us\n")
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.anonymize_all(dbkey=dbkey, dry_run=dry_run,
progress=progress)

View file

@ -222,7 +222,7 @@ class CoreTriggerHandler(GenericHandler):
op_session.execute(sa.text(f"""
CREATE TRIGGER record_meminfo_delete
AFTER DELETE ON meminfo
FOR EACH ROW INSERT INTO {changes_table} (object_type, object_key, deleted) VALUES ('Member', CONVERT(OLD.card_no, CHAR));
FOR EACH ROW INSERT INTO {changes_table} (object_type, object_key, deleted) VALUES ('Member', CONVERT(OLD.card_no, CHAR), 1);
"""))
def create_triggers_employees(self, op_session, changes_table):
@ -242,7 +242,7 @@ class CoreTriggerHandler(GenericHandler):
op_session.execute(sa.text(f"""
CREATE TRIGGER record_employees_delete
AFTER DELETE ON employees
FOR EACH ROW INSERT INTO {changes_table} (object_type, object_key, deleted) VALUES ('Employee', CONVERT(OLD.emp_no, CHAR));
FOR EACH ROW INSERT INTO {changes_table} (object_type, object_key, deleted) VALUES ('Employee', CONVERT(OLD.emp_no, CHAR), 1);
"""))
def create_triggers_departments(self, op_session, changes_table):

View file

@ -24,11 +24,16 @@
DataSync for Rattail DB
"""
import logging
from sqlalchemy import orm
from rattail.datasync import DataSyncImportConsumer
log = logging.getLogger(__name__)
class FromCOREAPIToRattail(DataSyncImportConsumer):
"""
Consumer for CORE POS (API) -> Rattail datasync
@ -69,6 +74,11 @@ class FromCOREAPIToRattail(DataSyncImportConsumer):
else:
# import member data from API, into various Rattail tables
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'],
host_object=member)
shoppers = self.importers['CustomerShopper'].get_shoppers_for_member(member)

View file

@ -953,7 +953,17 @@ class MemberEquityPaymentImporter(FromCOREPOS, corepos_importing.model.MemberEqu
if len(match) == 1:
return match[0]
# nb. avoid datetime for this one
# then try to match on date only, not time
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
if payment.corepos_transaction_number == stock_purchase.transaction_number
and payment.corepos_transaction_id == stock_purchase.transaction_id
@ -966,6 +976,9 @@ class MemberEquityPaymentImporter(FromCOREPOS, corepos_importing.model.MemberEqu
stock_purchase.amount,
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 let's also make sure at least date matches
payment = matches[0]