Add employee importer for CORE -> Rattail, and CORE cashier auth handler

This commit is contained in:
Lance Edgar 2023-10-01 19:40:46 -05:00
parent fd5d3142ed
commit 117442f8db
9 changed files with 255 additions and 1 deletions

View file

@ -92,6 +92,21 @@ class CoreHandler(GenericHandler):
# TODO: deprecate / remove this
get_office_customer_account_url = get_office_member_url
def get_office_employee_url(
self,
employee_number,
office_url=None,
require=False,
**kwargs):
"""
Returns the CORE Office URL for the employee with the given
number.
"""
if not office_url:
office_url = self.get_office_url(require=require)
if office_url:
return f'{office_url}/admin/Cashiers/CashierEditor.php?emp_no={employee_number}'
def get_office_product_url(
self,
upc,

80
rattail_corepos/auth.py Normal file
View file

@ -0,0 +1,80 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2023 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail 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.
#
# Rattail 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
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Auth Handler for use with CORE-POS
"""
from sqlalchemy import orm
from rattail import auth as base
class CoreAuthHandler(base.AuthHandler):
"""
Custom auth handler for use with CORE-POS.
"""
def authenticate_user(self, session, username, password):
model = self.model
# first try default logic, if it works then great
user = super().authenticate_user(session, username, password)
if user:
return user
# only if configured, we also check CORE-POS credentials
if self.config.getbool('rattail.auth', 'corepos.check_cashier_credentials',
default=False):
user = None
core_session = self.app.get_corepos_handler().make_session_office_op()
core_employee = self.check_corepos_cashier_credentials(core_session, password)
if core_employee:
user = self.get_user_from_corepos_employee(session, core_employee)
core_session.close()
if user and user.active:
return user
def check_corepos_cashier_credentials(self, core_session, password):
core_op = self.app.get_corepos_handler().get_model_office_op()
try:
core_employee = core_session.query(core_op.Employee)\
.filter(core_op.Employee.cashier_password == password)\
.one()
except orm.exc.NoResultFound:
pass
else:
if core_employee.active:
return core_employee
def get_user_from_corepos_employee(self, session, core_employee):
model = self.model
try:
employee = session.query(model.Employee)\
.join(model.CoreEmployee)\
.filter(model.CoreEmployee.corepos_number == core_employee.number)\
.one()
except orm.exc.NoResultFound:
pass
else:
return self.app.get_user(employee)

View file

@ -0,0 +1,51 @@
# -*- coding: utf-8; -*-
"""add core_employee
Revision ID: 1f2e2f57c90b
Revises: c40a6ec00428
Create Date: 2023-10-01 19:03:56.921897
"""
# revision identifiers, used by Alembic.
revision = '1f2e2f57c90b'
down_revision = 'c40a6ec00428'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
import rattail.db.types
def upgrade():
# corepos_employee
op.create_table('corepos_employee',
sa.Column('uuid', sa.String(length=32), nullable=False),
sa.Column('corepos_number', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['uuid'], ['employee.uuid'], name='corepos_employee_fk_employee'),
sa.PrimaryKeyConstraint('uuid')
)
op.create_table('corepos_employee_version',
sa.Column('uuid', sa.String(length=32), autoincrement=False, nullable=False),
sa.Column('corepos_number', sa.Integer(), autoincrement=False, nullable=True),
sa.Column('transaction_id', sa.BigInteger(), autoincrement=False, nullable=False),
sa.Column('end_transaction_id', sa.BigInteger(), nullable=True),
sa.Column('operation_type', sa.SmallInteger(), nullable=False),
sa.PrimaryKeyConstraint('uuid', 'transaction_id')
)
op.create_index(op.f('ix_corepos_employee_version_end_transaction_id'), 'corepos_employee_version', ['end_transaction_id'], unique=False)
op.create_index(op.f('ix_corepos_employee_version_operation_type'), 'corepos_employee_version', ['operation_type'], unique=False)
op.create_index(op.f('ix_corepos_employee_version_transaction_id'), 'corepos_employee_version', ['transaction_id'], unique=False)
def downgrade():
# corepos_employee
op.drop_index(op.f('ix_corepos_employee_version_transaction_id'), table_name='corepos_employee_version')
op.drop_index(op.f('ix_corepos_employee_version_operation_type'), table_name='corepos_employee_version')
op.drop_index(op.f('ix_corepos_employee_version_end_transaction_id'), table_name='corepos_employee_version')
op.drop_table('corepos_employee_version')
op.drop_table('corepos_employee')

View file

@ -25,7 +25,8 @@ Database schema extensions for CORE-POS integration
"""
from .stores import CoreStore, CoreTender
from .people import (CorePerson, CoreCustomer, CoreCustomerShopper,
from .people import (CorePerson, CoreEmployee,
CoreCustomer, CoreCustomerShopper,
CoreMember, CoreMemberEquityPayment)
from .products import (CoreDepartment, CoreSubdepartment,
CoreVendor, CoreProduct, CoreProductCost)

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2023 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail 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.
#
# Rattail 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
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
A "complete" data model including CORE-POS integration
"""
from rattail.db.model import *
from rattail_corepos.db.model import *

View file

@ -65,6 +65,41 @@ class CorePerson(model.Base):
CorePerson.make_proxy(model.Person, '_corepos', 'corepos_customer_id')
class CoreEmployee(model.Base):
"""
CORE-specific extensions to :class:`~rattail:rattail.db.model.Employee`
"""
__tablename__ = 'corepos_employee'
__table_args__ = (
sa.ForeignKeyConstraint(['uuid'], ['employee.uuid'],
name='corepos_employee_fk_employee'),
)
__versioned__ = {}
uuid = model.uuid_column(default=None)
employee = orm.relationship(
model.Employee,
doc="""
Reference to the actual employee record, which this one extends.
""",
backref=orm.backref(
'_corepos',
uselist=False,
cascade='all, delete-orphan',
doc="""
Reference to the CORE-POS extension record for this employee.
"""))
corepos_number = sa.Column(sa.Integer(), nullable=True, doc="""
``employees.emp_no`` value for this employee, within CORE-POS.
""")
def __str__(self):
return str(self.employee)
CoreEmployee.make_proxy(model.Employee, '_corepos', 'corepos_number')
class CoreCustomer(model.Base):
"""
CORE-specific extensions to :class:`rattail:rattail.db.model.Customer`.

View file

@ -35,6 +35,7 @@ from corepos.db.office_trans import model as coretrans, Session as CoreTransSess
from rattail import importing
from rattail.gpc import GPC
from rattail.db.util import normalize_full_name
from rattail_corepos import importing as corepos_importing
@ -65,6 +66,7 @@ class FromCOREPOSToRattail(importing.FromSQLAlchemyHandler, importing.ToRattailH
def get_importers(self):
importers = OrderedDict()
importers['Employee'] = EmployeeImporter
importers['Tender'] = TenderImporter
importers['Vendor'] = VendorImporter
importers['Department'] = DepartmentImporter
@ -88,6 +90,30 @@ class FromCOREPOS(importing.FromSQLAlchemy):
"""
class EmployeeImporter(FromCOREPOS, corepos_importing.model.EmployeeImporter):
"""
Importer for employee data from CORE POS.
"""
host_model_class = corepos.Employee
key = 'corepos_number'
supported_fields = [
'corepos_number',
'first_name',
'last_name',
'full_name',
'status',
]
def normalize_host_object(self, employee):
return {
'corepos_number': employee.number,
'first_name': employee.first_name,
'last_name': employee.last_name,
'full_name': normalize_full_name(employee.first_name, employee.last_name),
'status': self.enum.EMPLOYEE_STATUS_CURRENT if employee.active else self.enum.EMPLOYEE_STATUS_FORMER,
}
class TenderImporter(FromCOREPOS, corepos_importing.model.TenderImporter):
"""
Importer for tender data from CORE POS.

View file

@ -54,6 +54,15 @@ class PersonImporter(importing.model.PersonImporter):
return query
class EmployeeImporter(importing.model.EmployeeImporter):
extensions = {
'_corepos': [
'corepos_number',
],
}
class CustomerImporter(importing.model.CustomerImporter):
extensions = {

View file

@ -34,6 +34,7 @@ class CoreposVersionMixin(object):
def add_corepos_importers(self, importers):
importers['CorePerson'] = CorePersonImporter
importers['CoreEmployee'] = CoreEmployeeImporter
importers['CoreCustomer'] = CoreCustomerImporter
importers['CoreCustomerShopper'] = CoreCustomerShopperImporter
importers['CoreMember'] = CoreMemberImporter
@ -54,6 +55,14 @@ class CorePersonImporter(base.VersionImporter):
return model.CorePerson
class CoreEmployeeImporter(base.VersionImporter):
@property
def host_model_class(self):
model = self.config.get_model()
return model.CoreEmployee
class CoreCustomerImporter(base.VersionImporter):
@property