diff --git a/rattail/__init__.py b/rattail/__init__.py index 20f9104cc76c936fd1a40d8e2a9566b9c9b1ddc1..2fb515c7313aaf8340e6572a1659bcb51bc91ff3 100644 --- a/rattail/__init__.py +++ b/rattail/__init__.py @@ -31,3 +31,5 @@ __path__ = extend_path(__path__, __name__) from rattail._version import __version__ +from rattail.enum import * +from rattail.gpc import GPC diff --git a/rattail/batches.py b/rattail/batches.py deleted file mode 100644 index 6dc0a92d02d72f1956f77491013da5f221870608..0000000000000000000000000000000000000000 --- a/rattail/batches.py +++ /dev/null @@ -1,466 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -################################################################################ -# -# Rattail -- Retail Software Framework -# Copyright © 2010-2012 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 Affero 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 Affero General Public License for -# more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with Rattail. If not, see . -# -################################################################################ - -""" -``rattail.batches`` -- Batch Interface -""" - -# import logging - -from sqlalchemy import and_ -from sqlalchemy.orm import object_session - -import edbob -from edbob.db import needs_session -from edbob.util import requires_impl - -import rattail -# from rattail.db import needs_session -# from rattail.util import get_entry_points, requires_impl - - -# log = logging.getLogger(__name__) -# # _registered_sources = None - - -# # def get_registered_sources(): -# # """ -# # Returns the entry point map for registered batch sources. -# # """ - -# # global _registered_sources -# # if _registered_sources is None: -# # _registered_sources = get_entry_points('rattail.batch_sources') -# # return _registered_sources - - -# # @needs_session -# # def get_sources(session): -# # """ -# # Returns a dictionary of registered :class:`rattail.BatchSource` classes, -# # keyed by :attr:`BatchSource.name`. -# # """ - -# # sources = {} -# # for src in session.query(rattail.BatchSource): -# # sources[src.name] = src -# # return sources - - -class BatchTerminal(edbob.Object): - """ - Defines the interface for data batch terminals. Subclass this when - implementing new data awareness and/or integration with external systems. - """ - - @property - @requires_impl() - def name(self): - pass - - @property - @requires_impl() - def display(self): - pass - - @property - @requires_impl() - def fieldmap_internal(self): - pass - - @property - @requires_impl() - def fieldmap_user(self): - pass - - @requires_impl() - def provide_rows(self, session, rowclass, dictionary, query=None, **kwargs): - """ - Generator which yields (new) batch row instances. This is used to - populate batches. - """ - - raise NotImplementedError - - # def get_elements(self, elements, fieldmap, *required): - # """ - # Returns the proper element list according to current context. - # """ - - # if elements is None: - # elements = sorted(fieldmap) - # self.require_elements(elements, *required) - # return elements - - # def require_elements(self, using, *required): - # """ - # Officially require one or more elements when processing a batch. - # """ - - # for elements in required: - # elements = elements.split(',') - # for element in elements: - # if element not in using: - # raise sil.ElementRequiredError(element, using) - - -class RattailBatchTerminal(BatchTerminal): - """ - Defines the core batch terminal for Rattail. - """ - - name = 'rattail' - description = "Rattail (local)" - - source_columns = { - 'ITEM_DCT': [ - 'F01', - 'F02', - ], - } - - target_columns = { - 'DEPT_DCT': [ - 'F03', - 'F238', - ], - 'ITEM_DCT': [ - 'F01', - 'F02', - 'F03', - 'F22', - 'F155', - ], - } - - def provide_rows(self, session, rowclass, dictionary, query=None, **kwargs): - - if dictionary.name == 'DEPT_DCT': - if not query: - query = session.query(rattail.Department) - for dept in query: - yield rowclass( - F03=dept.number, - F238=dept.name, - ) - return - - elif dictionary.name == 'ITEM_DCT': - if not query: - query = session.query(rattail.Product) - for product in query: - yield rowclass( - F01=int(product.upc), - F02=product.description[:20], - F155=product.brand.name if product.brand else None, - ) - return - - assert False, "FIXME" - - # def import_main_item(self, session, elements=None, progress_factory=None): - # """ - # Create a main item (ITEM_DCT) batch from current Rattail data. - # """ - - # elements = self.get_elements(elements, self.fieldname_map_main_item, 'F01') - # batch = make_batch(self.name, elements, session, - # description="Main item data from Rattail") - - # products = session.query(rattail.Product) - # prog = None - # if progress_factory: - # prog = progress_factory("Creating batch", products.count()) - # for i, prod in enumerate(products, 1): - # fields = {} - # for name in elements: - # fields[name] = row[self.fieldname_map_main_item[name]] - # batch.append(**fields) - # if prog: - # prog.update(i) - # if prog: - # prog.destroy() - - # return batch - - def add_departments(self, session, batch): - for row in batch.provide_rows(): - dept = rattail.Department() - dept.number = row.F03 - dept.name = row.F238 - session.add(dept) - session.flush() - - def add_replace_departments(self, session, batch): - for row in batch.provide_rows(): - q = session.query(rattail.Department) - q = q.filter_by(number=row.F03) - if q.count(): - prods = session.query(rattail.Product) - prods = prods.filter_by(department_uuid=q.first().uuid) - if prods.count(): - prods.update(dict(department_uuid=None), synchronize_session='fetch') - q.delete(synchronize_session=False) - - dept = rattail.Department() - dept.number = row.F03 - dept.name = row.F238 - session.add(dept) - session.flush() - - def add_products(self, session, batch): - q = session.query(rattail.Department) - depts = {} - for dept in q: - depts[dept.number] = dept - - q = session.query(rattail.Brand) - brands = {} - for brand in q: - brands[brand.name] = brand - - for row in batch.provide_rows(): - dept = None - if row.F03: - if row.F03 in depts: - dept = depts[row.F03] - else: - dept = rattail.Department(number=row.F03) - session.add(dept) - session.flush() - depts[dept.number] = dept - - brand = None - if row.F155: - if row.F155 in brands: - brand = brands[row.F155] - else: - brand = rattail.Brand(name=row.F155) - session.add(brand) - session.flush() - brands[brand.name] = brand - - prod = rattail.Product() - prod.upc = row.F01 - prod.description = row.F02 - prod.size = row.F22 - prod.department = dept - prod.brand = brand - session.add(prod) - session.flush() - - def add_replace_products(self, session, batch): - q = session.query(rattail.Department) - depts = {} - for dept in q: - depts[dept.number] = dept - - q = session.query(rattail.Brand) - brands = {} - for brand in q: - brands[brand.name] = brand - - products = session.query(rattail.Product) - for row in batch.provide_rows(): - dept = None - if row.F03: - if row.F03 in depts: - dept = depts[row.F03] - else: - dept = rattail.Department(number=row.F03) - session.add(dept) - session.flush() - depts[dept.number] = dept - - brand = None - if row.F155: - if row.F155 in brands: - brand = brands[row.F155] - else: - brand = rattail.Brand(name=row.F155) - session.add(brand) - session.flush() - brands[brand.name] = brand - - q = products.filter_by(upc=row.F01) - if q.count(): - q.delete(synchronize_session=False) - - prod = rattail.Product() - prod.upc = row.F01 - prod.description = row.F02 - prod.size = row.F22 - prod.department = dept - prod.brand = brand - session.add(prod) - session.flush() - - def change_departments(self, session, batch): - depts = session.query(rattail.Department) - for row in batch.provide_rows(): - dept = depts.filter_by(number=row.F03).first() - if dept: - dept.name = row.F238 - session.flush() - - def change_products(self, session, batch): - q = session.query(rattail.Department) - depts = {} - for dept in q: - depts[dept.number] = dept - - products = session.query(rattail.Product) - for row in batch.provide_rows(): - prod = products.filter_by(upc=row.F01).first() - if prod: - dept = None - if row.F03: - if row.F03 in depts: - dept = depts[row.F03] - else: - dept = rattail.Department(number=row.F03) - session.add(dept) - session.flush() - depts[dept.number] = dept - prod.dept = dept - prod.size = row.F22 - session.flush() - - def execute_batch(self, batch): - """ - Executes ``batch``, which should be a :class:`rattail.Batch` instance. - """ - - session = object_session(batch) - - if batch.action_type == rattail.BATCH_ADD: - - if batch.dictionary.name == 'DEPT_DCT': - self.add_departments(session, batch) - return - if batch.dictionary.name == 'ITEM_DCT': - self.add_products(session, batch) - return - - if batch.action_type == rattail.BATCH_ADD_REPLACE: - if batch.dictionary.name == 'DEPT_DCT': - return self.add_replace_departments(session, batch) - if batch.dictionary.name == 'ITEM_DCT': - return self.add_replace_products(session, batch) - - if batch.action_type == rattail.BATCH_CHANGE: - - if batch.dictionary.name == 'DEPT_DCT': - self.change_departments(session, batch) - return - - if batch.dictionary.name == 'ITEM_DCT': - return self.change_products(session, batch) - - if batch.action_type == rattail.BATCH_REMOVE: - if batch.dictionary.name == 'DEPT_DCT': - return self.remove_departments(session, batch) - if batch.dictionary.name == 'ITEM_DCT': - return self.remove_products(session, batch) - - assert False, "FIXME" - - def remove_departments(self, session, batch): - depts = session.query(rattail.Department) - products = session.query(rattail.Product) - for row in batch.provide_rows(): - dept = depts.filter_by(number=row.F03).first() - if dept: - q = products.filter_by(department_uuid=dept.uuid) - if q.count(): - q.update({'department_uuid': None}, synchronize_session=False) - session.delete(dept) - session.flush() - - def remove_products(self, session, batch): - products = session.query(rattail.Product) - for row in batch.provide_rows(): - prod = products.filter_by(upc=row.F01).first() - if prod: - session.delete(prod) - session.flush() - - -# def make_batch(source, elements, session, batch_id=None, **kwargs): -# """ -# Create and return a new SIL-based :class:`rattail.Batch` instance. -# """ - -# if not batch_id: -# batch_id = next_batch_id(source, consume=True) - -# kwargs['source'] = source -# kwargs['batch_id'] = batch_id -# kwargs['target'] = target -# kwargs['elements'] = elements -# kwargs.setdefault('sil_type', 'HM') -# kwargs.setdefault('action_type', rattail.BATCH_ADD_REPLACE) -# kwargs.setdefault('dictionary', rattail.BATCH_MAIN_ITEM) -# batch = rattail.Batch(**kwargs) - -# session.add(batch) -# session.flush() -# batch.table.create() -# log.info("Created batch table: %s" % batch.table.name) -# return batch - - -@needs_session -def next_batch_id(session, source, consume=False): - """ - Returns the next available batch ID (as an integer) for the given - ``source`` SIL ID. - - If ``consume`` is ``True``, the "running" ID will be incremented so that - the next caller will receive a different ID. - """ - - batch_id = edbob.get_setting('batch.next_id.%s' % source, - session=session) - if batch_id is None or not batch_id.isdigit(): - batch_id = 1 - else: - batch_id = int(batch_id) - - while True: - q = session.query(rattail.Batch) - q = q.join((rattail.BatchTerminal, - rattail.BatchTerminal.uuid == rattail.Batch.source_uuid)) - q = q.filter(and_( - rattail.BatchTerminal.sil_id == source, - rattail.Batch.batch_id == '%08u' % batch_id, - )) - if not q.count(): - break - batch_id += 1 - - if consume: - edbob.save_setting('batch.next_id.%s' % source, str(batch_id + 1), - session=session) - return batch_id diff --git a/rattail/batches/__init__.py b/rattail/batches/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3b66140b15d696e98f41beec201d5912f1ade2ab --- /dev/null +++ b/rattail/batches/__init__.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2012 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 Affero 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 Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Rattail. If not, see . +# +################################################################################ + +""" +``rattail.batches`` -- Batch System +""" + +from edbob.util import entry_point_map + +from rattail.batches.exceptions import * +from rattail.batches.providers import * + + +registered_providers = {} + + +def get_provider(name): + provider = registered_providers.get(name) + if not provider: + raise BatchProviderNotFound(name) + return provider() + + +def iter_providers(): + return sorted(registered_providers.itervalues(), + key=lambda x: x.description) + + +def init(config): + global registered_providers + registered_providers = entry_point_map('rattail.batches.providers') diff --git a/rattail/batches/exceptions.py b/rattail/batches/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..d30c29a333442095dc3133a0bc7f0a7a7a811a1f --- /dev/null +++ b/rattail/batches/exceptions.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2012 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 Affero 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 Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Rattail. If not, see . +# +################################################################################ + +""" +``rattail.batches.exceptions`` -- Batch Exceptions +""" + + +class BatchError(Exception): + + pass + + +class BatchProviderNotFound(BatchError): + + def __init__(self, name): + self.name = name + + def __str__(self): + return "Batch provider not found: %s" % self.name + + +class BatchDestinationNotSupported(BatchError): + + def __init__(self, batch): + self.batch = batch + + def __str__(self): + return "Destination '%s' not supported for batch: %s" % ( + self.batch.destination, self.batch.description) diff --git a/rattail/batches/providers/__init__.py b/rattail/batches/providers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..729629dcff42b5c6561de9111b98ed39fc566d8d --- /dev/null +++ b/rattail/batches/providers/__init__.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2012 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 Affero 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 Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Rattail. If not, see . +# +################################################################################ + +""" +``rattail.batches.providers`` -- Batch Providers +""" + +import datetime + +import edbob + +import rattail +from rattail import sil + + +__all__ = ['BatchProvider'] + + +class BatchProvider(edbob.Object): + + name = None + description = None + source = 'RATAIL' + destination = None + action_type = None + purge_date_offset = 90 + + def add_columns(self, batch): + pass + + def add_rows_begin(self, batch): + pass + + def add_rows(self, batch, query, progress=None): + self.add_rows_begin(batch) + prog = None + if progress: + prog = progress("Adding rows to batch \"%s\"" % batch.description, + query.count()) + cancel = False + for i, instance in enumerate(query, 1): + self.add_row(batch, instance) + if prog and not prog.update(i): + cancel = True + break + if prog: + prog.destroy() + return not cancel + + def execute(self, batch, progress=None): + raise NotImplementedError + + def make_batch(self, session, data, progress=None): + batch = rattail.Batch() + batch.provider = self.name + batch.source = self.source + batch.id = sil.consume_batch_id(batch.source) + batch.destination = self.destination + batch.description = self.description + batch.action_type = self.action_type + self.set_purge_date(batch) + session.add(batch) + session.flush() + + self.add_columns(batch) + batch.create_table() + if not self.add_rows(batch, data, progress=progress): + batch.drop_table() + return None + return batch + + def set_purge_date(self, batch): + today = edbob.utc_time(naive=True).date() + purge_offset = datetime.timedelta(days=self.purge_date_offset) + batch.purge = today + purge_offset + + def set_params(self, session, **params): + pass diff --git a/rattail/batches/providers/labels.py b/rattail/batches/providers/labels.py new file mode 100644 index 0000000000000000000000000000000000000000..0ef8ec4267d04386cc1fee044a4ce4fdfa7e08dc --- /dev/null +++ b/rattail/batches/providers/labels.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python + +""" +``dtail.batches.products.labels`` -- Print Labels Batch +""" + +from sqlalchemy.orm import object_session + +import rattail +from rattail.batches.providers import BatchProvider + + +class PrintLabels(BatchProvider): + + name = 'print_labels' + description = "Print Labels" + + default_profile = None + default_quantity = 1 + + def add_columns(self, batch): + batch.add_column('F01') + batch.add_column('F155') + batch.add_column('F02') + batch.add_column('F22', display_name="Size") + batch.add_column('F95', display_name="Label") + batch.add_column('F94', display_name="Quantity") + + def add_rows_begin(self, batch): + session = object_session(batch) + if not self.default_profile: + q = session.query(rattail.LabelProfile) + q = q.order_by(rattail.LabelProfile.ordinal) + self.default_profile = q.first() + assert self.default_profile + else: + self.default_profile = session.merge(self.default_profile) + + def add_row(self, batch, product): + row = batch.rowclass() + row.F01 = product.upc + if product.brand: + row.F155 = product.brand.name + row.F02 = product.description[:20] + row.F22 = product.size + row.F95 = self.default_profile.code + row.F94 = self.default_quantity + batch.add_row(row) + + def execute(self, batch, progress=None): + prog = None + if progress: + prog = progress("Loading product data", batch.rowcount) + + session = object_session(batch) + profiles = {} + + cancel = False + for i, row in enumerate(batch.iter_rows(), 1): + + profile = profiles.get(row.F95) + if not profile: + q = session.query(rattail.LabelProfile) + q = q.filter(rattail.LabelProfile.code == row.F95) + profile = q.one() + profile.labels = [] + profiles[row.F95] = profile + + q = session.query(rattail.Product) + q = q.filter(rattail.Product.upc == row.F01) + product = q.one() + + profile.labels.append((product, row.F94)) + + if prog and not prog.update(i): + cancel = True + break + + if not cancel: + for profile in profiles.itervalues(): + printer = profile.get_printer() + prog2 = None + if prog: + prog2 = prog.secondary_progress() + if not printer.print_labels(profile.labels, progress=prog2): + cancel = True + break + + if prog: + prog.destroy() + return not cancel + + def set_params(self, session, **params): + profile = params.get('profile') + if profile: + q = session.query(rattail.LabelProfile) + q = q.filter(rattail.LabelProfile.code == profile) + self.default_profile = q.one() + + quantity = params.get('quantity') + if quantity and quantity.isdigit(): + self.default_quantity = int(quantity) diff --git a/rattail/db/__init__.py b/rattail/db/__init__.py index db384279f6d7088e33731ae93f15a27e1d7c2bcd..f02eee35a26f63754b4543ca039360250e6990df 100644 --- a/rattail/db/__init__.py +++ b/rattail/db/__init__.py @@ -27,7 +27,6 @@ """ import edbob -# from edbob.db.extensions import activate_extension import rattail @@ -42,60 +41,3 @@ def init(config): from rattail.db.extension import enum edbob.graft(rattail, enum) - - -# def init_database(engine, session): -# """ -# Initialize an ``edbob`` database for use with Rattail. -# """ - -# activate_extension('rattail', engine) - -# columns = [ -# ('F01', 'UPC', 'GPC(14)'), -# ('F02', 'Description', 'CHAR(20)'), -# ('F03', 'Department Number', 'NUMBER(4,0)'), -# ('F22', 'Size', 'CHAR(30)'), -# ('F155', 'Brand', 'CHAR(30)'), -# ('F238', 'Department Name', 'CHAR(30)'), -# ] - -# for name, disp, dtype in columns: -# session.add(rattail.SilColumn( -# sil_name=name, display=disp, data_type=dtype)) -# session.flush() - -# dictionaries = [ -# ('DEPT_DCT', 'Department', [ -# ('F03', True), -# 'F238', -# ]), -# ('ITEM_DCT', 'Product', [ -# ('F01', True), -# 'F02', -# 'F03', -# 'F22', -# 'F155', -# ]), -# # ('PRICE_DCT', 'Price', []), -# # ('FCOST_DCT', 'Future Cost', []), -# # ('FSPRICE_DCT', 'Future Sale Price', []), -# # ('CLASS_GROUP', 'Scale Class / Group', []), -# # ('NUTRITION', 'Scale Nutrition', []), -# # ('SCALE_TEXT', 'Scale Text', []), -# # ('VENDOR_DCT', 'Vendor', []), -# ] - -# for name, desc, cols in dictionaries: -# bd = rattail.BatchDictionary(name=name, description=desc) -# for col in cols: -# key = False -# if not isinstance(col, basestring): -# col, key = col -# q = session.query(rattail.SilColumn) -# q = q.filter(rattail.SilColumn.sil_name == col) -# col = q.one() -# bd.columns.append( -# rattail.BatchDictionaryColumn(sil_column=col, key=key)) -# session.add(bd) -# session.flush() diff --git a/rattail/db/extension/enum.py b/rattail/db/extension/enum.py index f4ffd932914185c48c6ac53941163c8c48032d9f..9fb0c485707236ff398aa70dba93aa61613304a0 100644 --- a/rattail/db/extension/enum.py +++ b/rattail/db/extension/enum.py @@ -27,28 +27,6 @@ """ -BATCH_ADD = 'ADD' -BATCH_ADD_REPLACE = 'ADDRPL' -BATCH_CHANGE = 'CHANGE' -BATCH_LOAD = 'LOAD' -BATCH_REMOVE = 'REMOVE' - -BATCH_ACTION_TYPE = { - BATCH_ADD : "Add", - BATCH_ADD_REPLACE : "Add/Replace", - BATCH_CHANGE : "Change", - BATCH_LOAD : "Load", - BATCH_REMOVE : "Remove", - } - - -# BATCH_MAIN_ITEM = 'ITEM_DCT' - -# BATCH_DICTIONARY = { -# BATCH_MAIN_ITEM : "Main Item", -# } - - EMPLOYEE_STATUS_CURRENT = 1 EMPLOYEE_STATUS_FORMER = 2 @@ -58,34 +36,6 @@ EMPLOYEE_STATUS = { } -PRICE_TYPE_REGULAR = 0 -PRICE_TYPE_TPR = 1 -PRICE_TYPE_SALE = 2 -PRICE_TYPE_MANAGER_SPECIAL = 3 -PRICE_TYPE_ALTERNATE = 4 -PRICE_TYPE_FREQUENT_SHOPPER = 5 -PRICE_TYPE_MFR_SUGGESTED = 901 - -PRICE_TYPE = { - PRICE_TYPE_REGULAR : "Regular Price", - PRICE_TYPE_TPR : "TPR", - PRICE_TYPE_SALE : "Sale", - PRICE_TYPE_MANAGER_SPECIAL : "Manager Special", - PRICE_TYPE_ALTERNATE : "Alternate Price", - PRICE_TYPE_FREQUENT_SHOPPER : "Frequent Shopper", - PRICE_TYPE_MFR_SUGGESTED : "Manufacturer's Suggested", - } - - -UNIT_OF_MEASURE_EACH = '01' -UNIT_OF_MEASURE_POUND = '49' - -UNIT_OF_MEASURE = { - UNIT_OF_MEASURE_EACH : "Each", - UNIT_OF_MEASURE_POUND : "Pound", - } - - # VENDOR_CATALOG_NOT_PARSED = 1 # VENDOR_CATALOG_PARSED = 2 # VENDOR_CATALOG_COGNIZED = 3 diff --git a/rattail/db/extension/model.py b/rattail/db/extension/model.py index 110f3736ff094bce267e6c1e459af600693e3e7c..7f43e2f0c28566aa15188819e19d384fc1b5020c 100644 --- a/rattail/db/extension/model.py +++ b/rattail/db/extension/model.py @@ -26,12 +26,9 @@ ``rattail.db.extension.model`` -- Schema Definition """ -from __future__ import absolute_import - -import re - -from sqlalchemy import (Column, String, Integer, Date, DateTime, - Boolean, Text, ForeignKey, BigInteger, Numeric) +from sqlalchemy import Column, ForeignKey +from sqlalchemy import String, Integer, DateTime, Date, Boolean, Numeric, Text +from sqlalchemy import types from sqlalchemy import and_ from sqlalchemy.orm import relationship, object_session from sqlalchemy.ext.associationproxy import association_proxy @@ -39,475 +36,175 @@ from sqlalchemy.ext.orderinglist import ordering_list import edbob from edbob.db.model import Base, uuid_column -from edbob.db.extensions.contact import (Person, EmailAddress, PhoneNumber, - PersonPhoneNumber) +from edbob.db.extensions.contact import Person, EmailAddress, PhoneNumber +from edbob.exceptions import LoadSpecError from edbob.sqlalchemy import getset_factory +from rattail import sil +from rattail import batches +from rattail.gpc import GPCType -# __all__ = ['SilColumn', 'BatchDictionaryColumn', 'BatchDictionary', -# 'BatchTerminalColumn', 'BatchTerminal', 'BatchColumn', -# 'Batch', 'Brand', 'Department', 'Subdepartment', 'Category', -# 'Product', 'Employee', 'Vendor', 'VendorContact', 'VendorPhoneNumber', -# 'ProductCost', 'ProductPrice', 'Customer', 'CustomerGroup', -# 'CustomerGroupAssignment', 'CustomerPerson'] __all__ = ['Department', 'Subdepartment', 'Brand', 'Category', 'Vendor', 'VendorContact', 'VendorPhoneNumber', 'Product', 'ProductCost', 'ProductPrice', 'Customer', 'CustomerEmailAddress', 'CustomerPhoneNumber', 'CustomerGroup', 'CustomerGroupAssignment', 'CustomerPerson', 'Employee', 'EmployeeEmailAddress', - 'EmployeePhoneNumber'] - -# sil_type_pattern = re.compile(r'^(CHAR|NUMBER)\((\d+(?:\,\d+)?)\)$') - - -# class SilColumn(Base): -# """ -# Represents a SIL-compatible column available to the batch system. -# """ + 'EmployeePhoneNumber', 'BatchColumn', 'Batch', 'LabelProfile'] -# __tablename__ = 'sil_columns' - -# uuid = uuid_column() -# sil_name = Column(String(10)) -# display = Column(String(20)) -# data_type = Column(String(15)) - -# def __repr__(self): -# return "" % self.sil_name - -# def __str__(self): -# return str(self.sil_name or '') +class BatchColumn(Base): + """ + Represents a :class:`SilColumn` associated with a :class:`Batch`. + """ -# class BatchDictionaryColumn(Base): -# """ -# Represents a column within a :class:`BatchDictionary`. -# """ + __tablename__ = 'batch_columns' -# __tablename__ = 'batch_dictionary_columns' + uuid = uuid_column() + batch_uuid = Column(String(32), ForeignKey('batches.uuid')) + ordinal = Column(Integer, nullable=False) + name = Column(String(20)) + display_name = Column(String(50)) + sil_name = Column(String(10)) + data_type = Column(String(15)) + description = Column(String(50)) + visible = Column(Boolean, default=True) + + def __init__(self, sil_name=None, **kwargs): + if sil_name: + kwargs['sil_name'] = sil_name + sil_column = sil.get_column(sil_name) + kwargs.setdefault('name', sil_name) + kwargs.setdefault('data_type', sil_column.data_type) + kwargs.setdefault('description', sil_column.description) + kwargs.setdefault('display_name', sil_column.display_name) + super(BatchColumn, self).__init__(**kwargs) -# uuid = uuid_column() -# dictionary_uuid = Column(String(32), ForeignKey('batch_dictionaries.uuid')) -# sil_column_uuid = Column(String(32), ForeignKey('sil_columns.uuid')) -# key = Column(Boolean) + def __repr__(self): + return "" % self.name -# sil_column = relationship(SilColumn) + def __unicode__(self): + return unicode(self.display_name or '') -# def __repr__(self): -# return "" % self.sil_column -# def __str__(self): -# return str(self.sil_column or '') +class BatchRow(edbob.Object): + """ + Superclass of batch row objects. + """ + def __unicode__(self): + return u"Row %d" % self.ordinal -# class BatchDictionary(Base): -# """ -# Represents a SIL-based dictionary supported by one or more -# :class:`BatchTerminal` classes. -# """ -# __tablename__ = 'batch_dictionaries' +class Batch(Base): + """ + Represents a SIL-compliant batch of data. + """ -# uuid = uuid_column() -# name = Column(String(20)) -# description = Column(String(255)) + __tablename__ = 'batches' -# columns = relationship( -# BatchDictionaryColumn, -# backref='dictionary') + uuid = uuid_column() + provider = Column(String(50)) + id = Column(String(8)) + source = Column(String(6)) + destination = Column(String(6)) + action_type = Column(String(6)) + description = Column(String(50)) + rowcount = Column(Integer, default=0) + executed = Column(DateTime) + purge = Column(Date) + + columns = relationship(BatchColumn, backref='batch', + collection_class=ordering_list('ordinal'), + order_by=BatchColumn.ordinal, + cascade='save-update, merge, delete, delete-orphan') + + _rowclasses = {} -# def __repr__(self): -# return "" % self.name + def __repr__(self): + return "" % self.description -# def __str__(self): -# return str(self.description or '') + def __unicode__(self): + return unicode(self.description or '') + @property + def rowclass(self): + """ + Returns the mapped class for the underlying row (data) table. + """ -# class BatchTerminalColumn(Base): -# """ -# Represents a column supported by a :class:`BatchTerminal`. -# """ + if not self.uuid: + object_session(self).flush() + assert self.uuid -# __tablename__ = 'batch_terminal_columns' + if self.uuid not in self._rowclasses: -# uuid = uuid_column() -# terminal_uuid = Column(String(32), ForeignKey('batch_terminals.uuid')) -# dictionary_uuid = Column(String(32), ForeignKey('batch_dictionaries.uuid')) -# sil_column_uuid = Column(String(32), ForeignKey('sil_columns.uuid')) -# ordinal = Column(Integer) -# source = Column(Boolean) -# target = Column(Boolean) + kwargs = { + '__tablename__': 'batch.%s' % self.uuid, + 'uuid': uuid_column(), + 'ordinal': Column(Integer, nullable=False), + } -# dictionary = relationship(BatchDictionary) -# sil_column = relationship(SilColumn) + for column in self.columns: + data_type = sil.get_sqlalchemy_type(column.data_type) + kwargs[column.name] = Column(data_type) + rowclass = type('BatchRow_%s' % str(self.uuid), (Base, BatchRow), kwargs) -# def __repr__(self): -# return "" % ( -# self.terminal, self.dictionary, self.sil_column) + batch_uuid = self.uuid + def batch(self): + return object_session(self).query(Batch).get(batch_uuid) + rowclass.batch = property(batch) -# def __str__(self): -# return str(self.sil_column or '') + self._rowclasses[self.uuid] = rowclass + return self._rowclasses[self.uuid] -# class BatchTerminal(Base): -# """ -# Represents a terminal, or "junction" for batch data. -# """ + def add_column(self, sil_name=None, **kwargs): + column = BatchColumn(sil_name, **kwargs) + self.columns.append(column) -# __tablename__ = 'batch_terminals' + def add_row(self, row, **kwargs): + """ + Adds a row to the batch data table. + """ -# uuid = uuid_column() -# sil_id = Column(String(20), unique=True) -# description = Column(String(50)) -# class_spec = Column(String(255)) -# functional = Column(Boolean, default=False) -# source = Column(Boolean) -# target = Column(Boolean) -# source_kwargs = Column(Text) -# target_kwargs = Column(Text) + session = object_session(self) + # FIXME: This probably needs to use a func.max() query. + row.ordinal = self.rowcount + 1 + session.add(row) + self.rowcount += 1 + session.flush() -# columns = relationship( -# BatchTerminalColumn, -# backref='terminal') + def create_table(self): + """ + Creates the batch's data table within the database. + """ -# _terminal = 'not_got_yet' + self.rowclass.__table__.create() -# def __repr__(self): -# return "" % self.sil_id + def drop_table(self): + """ + Drops the batch's data table from the database. + """ -# def __str__(self): -# return str(self.description or '') + self.rowclass.__table__.drop() -# def source_columns(self, dictionary): -# for col in self.columns: -# if col.dictionary is dictionary: -# yield col + def execute(self, progress=None): + provider = self.get_provider() + assert provider + provider.execute(self, progress) + self.executed = edbob.utc_time(naive=True) + object_session(self).flush() -# def get_terminal(self): -# """ -# Returns the :class:`rattail.batches.BatchTerminal` instance which is -# associated with the database record via its ``python_spec`` field. -# """ + def get_provider(self): + assert self.provider + return batches.get_provider(self.provider) -# if self._terminal == 'not_got_yet': -# self._terminal = None -# if self.class_spec: -# term = edbob.load_spec(self.class_spec) -# if term: -# self._terminal = term() -# return self._terminal - - -# class BatchColumn(Base): -# """ -# Represents a :class:`SilColumn` associated with a :class:`Batch`. -# """ - -# __tablename__ = 'batch_columns' - -# uuid = uuid_column() -# batch_uuid = Column(String(32), ForeignKey('batches.uuid')) -# ordinal = Column(Integer) -# sil_column_uuid = Column(String(32), ForeignKey('sil_columns.uuid')) -# source_uuid = Column(String(32), ForeignKey('batch_terminals.uuid')) -# targeted = Column(Boolean) - -# sil_column = relationship(SilColumn) - -# source = relationship( -# BatchTerminal, -# primaryjoin=BatchTerminal.uuid == source_uuid, -# order_by=[BatchTerminal.description], -# ) - -# def __repr__(self): -# return "" % (self.batch, self.sil_column) - - -# def get_sil_column(name): -# """ -# Returns a ``sqlalchemy.Column`` instance according to Rattail's notion of -# what each SIL field ought to look like. -# """ - -# type_map = { - -# # The first list of columns is a subset of Level 1 SIL. - -# 'F01': -# Integer, # upc -# 'F02': -# String(60), # short (receipt) description -# 'F478': -# Integer, # scale text type - -# # The remaining columns are custom to Rattail. - -# 'F4001': -# String(60), # short description, line 2 -# } - -# return Column(name, type_map[name]) - - -# def get_sil_type(data_type): -# """ -# Returns a SQLAlchemy-based data type according to the SIL-compliant type -# specifier found in ``data_type``. -# """ - -# if data_type == 'GPC(14)': -# return BigInteger - -# m = sil_type_pattern.match(data_type) -# if m: -# data_type, precision = m.groups() -# if precision.isdigit(): -# precision = int(precision) -# scale = 0 -# else: -# precision, scale = precision.split(',') -# precision = int(precision) -# scale = int(scale) -# if data_type == 'CHAR': -# assert not scale, "FIXME" -# return String(precision) -# if data_type == 'NUMBER': -# return Numeric(precision, scale) - -# assert False, "FIXME" - - -# class Batch(Base): -# """ -# Represents a batch of data, presumably in need of processing. -# """ - -# __tablename__ = 'batches' - -# _rowclass = None -# _row_classes = {} - -# uuid = uuid_column() -# source_uuid = Column(String(32), ForeignKey('batch_terminals.uuid')) -# source_description = Column(String(50)) -# source_batch_id = Column(String(8)) -# batch_id = Column(String(8)) -# dictionary_uuid = Column(String(32), ForeignKey('batch_dictionaries.uuid')) -# name = Column(String(30)) -# target_uuid = Column(String(32), ForeignKey('batch_terminals.uuid')) -# action_type = Column(String(6)) -# elements = Column(String(255)) -# description = Column(String(50)) -# rowcount = Column(Integer, default=0) -# effective = Column(DateTime) -# deleted = Column(Boolean, default=False) -# sil_type = Column(String(2)) -# sil_source_id = Column(String(20)) -# sil_target_id = Column(String(20)) -# sil_audit_file = Column(String(12)) -# sil_response_file = Column(String(12)) -# sil_origin_time = Column(DateTime) -# sil_purge_date = Column(Date) -# sil_user1 = Column(String(30)) -# sil_user2 = Column(String(30)) -# sil_user3 = Column(String(30)) -# sil_warning_level = Column(Integer) -# sil_max_errors = Column(Integer) -# sil_level = Column(String(7)) -# sil_software_revision = Column(String(4)) -# sil_primary_key = Column(String(50)) -# sil_sys_command = Column(String(512)) -# sil_dict_revision = Column(String(8)) - -# source = relationship( -# BatchTerminal, -# primaryjoin=BatchTerminal.uuid == source_uuid, -# order_by=[BatchTerminal.description], -# ) - -# dictionary = relationship( -# BatchDictionary, -# order_by=[BatchDictionary.name], -# ) - -# target = relationship( -# BatchTerminal, -# primaryjoin=BatchTerminal.uuid == target_uuid, -# order_by=[BatchTerminal.description], -# ) - -# columns = relationship( -# BatchColumn, -# backref='batch', -# ) - -# # _table = None -# # # _source_junction = 'not set' -# # # _target_junction = 'not set' - -# # invalid_name_chars = re.compile(r'[^A-Za-z0-9]') - -# def __repr__(self): -# return "" % (self.name or '(no name)') - -# def __str__(self): -# return str(self.name or '') - -# # @property -# # def source_junction(self): -# # """ -# # Returns the :class:`rattail.BatchJunction` instance associated with -# # this batch's :attr:`Batch.source` attribute. -# # """ - -# # if self._source_junction == 'not set': -# # from rattail.sil import get_available_junctions -# # self._source_junction = None -# # junctions = get_available_junctions() -# # if self.source in junctions: -# # self._source_junction = junctions[self.source] -# # return self._source_junction - -# # @property -# # def table(self): -# # """ -# # Returns the ``sqlalchemy.Table`` instance for the underlying batch -# # data. -# # """ - -# # # from sqlalchemy import MetaData, Table, Column, String -# # from sqlalchemy import Table, Column, String -# # from rattail import metadata -# # from rattail.sqlalchemy import get_sil_column - -# # if self._table is None: -# # # assert self.uuid -# # assert self.name -# # name = 'batch.%s.%s' % (self.source, self.batch_id) -# # if name in metadata.tables: -# # self._table = metadata.tables[name] -# # else: -# # # session = object_session(self) -# # # metadata = MetaData(session.bind) -# # columns = [Column('uuid', String(32), primary_key=True, default=get_uuid)] -# # # columns.extend([get_sil_column(x) for x in self.elements.split(',')]) -# # columns.extend([get_sil_column(x) for x in self.elements.split(',')]) -# # self._table = Table(name, metadata, *columns) -# # return self._table - -# # @property -# # def rowclass(self): -# # """ -# # Returns a unique subclass of :class:`rattail.BatchRow`, specific to the -# # batch. -# # """ - -# # if self._rowclass is None: -# # name = self.invalid_name_chars.sub('_', self.name) -# # self._rowclass = type('BatchRow_%s' % str(name), (BatchRow,), {}) -# # mapper(self._rowclass, self.table) -# # # session = object_session(self) -# # # engine = session.bind -# # # session.configure(binds={self._rowclass:engine}) -# # return self._rowclass - -# @property -# def rowclass(self): -# """ -# Returns the SQLAlchemy-mapped class for the underlying data table. -# """ - -# assert self.uuid -# if self.uuid not in self._row_classes: -# kwargs = { -# '__tablename__': 'batch.%s' % self.name, -# 'uuid': uuid_column(), -# } -# for col in self.columns: -# kwargs[col.sil_column.sil_name] = Column(get_sil_type(col.sil_column.data_type)) -# self._row_classes[self.uuid] = type('BatchRow', (Base,), kwargs) -# return self._row_classes[self.uuid] - -# def create_table(self): -# """ -# Creates the batch's data table within the database. -# """ - -# self.rowclass.__table__.create() - -# # @property -# # def target_junction(self): -# # """ -# # Returns the :class:`rattail.BatchJunction` instance associated with -# # this batch's :attr:`Batch.target` attribute. -# # """ - -# # if self._target_junction == 'not set': -# # from rattail.sil import get_available_junctions -# # self._target_junction = None -# # junctions = get_available_junctions() -# # if self.target in junctions: -# # self._target_junction = junctions[self.target] -# # return self._target_junction - -# # def append(self, **row): -# # """ -# # Appends a row of data to the batch. Note that this is done -# # immediately, and not within the context of any transaction. -# # """ - -# # # self.connection.execute(self.table.insert().values(**row)) -# # # self.rowcount += 1 -# # session = object_session(self) -# # session.add(self.rowclass(**row)) -# # self.rowcount += 1 -# # session.flush() - -# def add_rows(self, source, dictionary, **kwargs): -# session = object_session(self) -# source = source.get_terminal() -# for row in source.provide_rows(session, self.rowclass, -# dictionary, **kwargs): -# session.add(row) -# session.flush() - -# def execute(self): -# """ -# Invokes the batch execution logic. This will instantiate the -# :class:`rattail.batches.BatchTerminal` instance identified by the -# batch's :attr:`target` attribute and ask it to process the batch -# according to its action type. - -# .. note:: -# No check is performed to verify the current time is appropriate as -# far as the batch's effective date is concerned. It is assumed that -# other logic has already taken care of that and that yes, in fact it -# *is* time for the batch to be executed. -# """ - -# target = self.target.get_terminal() -# target.execute_batch(self) - -# def provide_rows(self): -# """ -# Generator which yields :class:`BatchRow` instances belonging to the -# batch. -# """ - -# session = object_session(self) -# for row in session.query(self.rowclass): -# yield row - - -# class BatchRow(edbob.Object): -# """ -# Superclass of batch row objects. -# """ - -# def __repr__(self): -# return "" % self.key_value + def iter_rows(self): + session = object_session(self) + q = session.query(self.rowclass) + q = q.order_by(self.rowclass.ordinal) + return q class Brand(Base): @@ -726,16 +423,6 @@ class ProductCost(Base): unit_cost = Column(Numeric(9,5)) effective = Column(DateTime) - # case_pack = Column(Integer) - # case_qty = Column(Integer) - # pack_qty = Column(Integer) - # # suggested_retail = Column(Numeric(6,2)) - # case_cost = Column(Numeric(9,5)) - # unit_cost = Column(Numeric(9,5)) - # # cost_effective = Column(DateTime) - # # cost_expires = Column(DateTime) - # # cost_recorded = Column(DateTime, default=datetime.datetime.now) - vendor = relationship(Vendor) def __repr__(self): @@ -772,7 +459,7 @@ class Product(Base): __tablename__ = 'products' uuid = uuid_column() - upc = Column(BigInteger, index=True) + upc = Column(GPCType, index=True) department_uuid = Column(String(32), ForeignKey('departments.uuid')) subdepartment_uuid = Column(String(32), ForeignKey('subdepartments.uuid')) category_uuid = Column(String(32), ForeignKey('categories.uuid')) @@ -794,16 +481,6 @@ class Product(Base): use_alter=True, name='products_current_price_uuid_fkey')) - # regular_price = Column(Numeric(10,4)) - # # package_price = Column(Numeric(10,4)) - # # package_price_quantity = Column(Integer) - # sale_price = Column(Numeric(10,4)) - # sale_price_quantity = Column(Integer) - - # case_quantity = Column(Integer) - # pack_quantity = Column(Integer) - # pack_price = Column(Numeric(8,3)) - department = relationship(Department) subdepartment = relationship(Subdepartment) category = relationship(Category) @@ -1102,3 +779,65 @@ Customer.groups = association_proxy( '_groups', 'group', getset_factory=getset_factory, creator=lambda x: CustomerGroupAssignment(group=x)) + + +class LabelProfile(Base): + """ + Represents a "profile" (collection of settings) for product label printing. + """ + + __tablename__ = 'label_profiles' + + uuid = uuid_column() + ordinal = Column(Integer) + code = Column(String(3)) + description = Column(String(50)) + printer_spec = Column(String(255)) + formatter_spec = Column(String(255)) + format = Column(Text) + + _printer = None + _formatter = None + + def __repr__(self): + return "" % self.code + + def __unicode__(self): + return unicode(self.description or '') + + def get_formatter(self): + if not self._formatter and self.formatter_spec: + try: + formatter = edbob.load_spec(self.formatter_spec) + except LoadSpecError: + pass + else: + self._formatter = formatter() + return self._formatter + + def get_printer(self): + if not self._printer and self.printer_spec: + try: + printer = edbob.load_spec(self.printer_spec) + except LoadSpecError: + pass + else: + self._printer = printer() + for name in printer.required_settings: + setattr(printer, name, self.get_printer_setting(name)) + self._printer.formatter = self.get_formatter() + return self._printer + + def get_printer_setting(self, name): + session = object_session(self) + if not self.uuid: + session.flush() + name = 'labels.%s.printer.%s' % (self.uuid, name) + return edbob.get_setting(name, session) + + def save_printer_setting(self, name, value): + session = object_session(self) + if not self.uuid: + session.flush() + name = 'labels.%s.printer.%s' % (self.uuid, name) + edbob.save_setting(name, value, session) diff --git a/rattail/enum.py b/rattail/enum.py new file mode 100644 index 0000000000000000000000000000000000000000..7746f2dbfdf8bae55db3ff46b3f3543197492c10 --- /dev/null +++ b/rattail/enum.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2012 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 Affero 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 Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Rattail. If not, see . +# +################################################################################ + +""" +``rattail.enum`` -- Enumerations +""" + + +BATCH_ACTION_ADD = 'ADD' +BATCH_ACTION_ADD_REPLACE = 'ADDRPL' +BATCH_ACTION_CHANGE = 'CHANGE' +BATCH_ACTION_LOAD = 'LOAD' +BATCH_ACTION_REMOVE = 'REMOVE' + +BATCH_ACTION = { + BATCH_ACTION_ADD : "Add", + BATCH_ACTION_ADD_REPLACE : "Add/Replace", + BATCH_ACTION_CHANGE : "Change", + BATCH_ACTION_LOAD : "Load", + BATCH_ACTION_REMOVE : "Remove", + } + + +PRICE_TYPE_REGULAR = 0 +PRICE_TYPE_TPR = 1 +PRICE_TYPE_SALE = 2 +PRICE_TYPE_MANAGER_SPECIAL = 3 +PRICE_TYPE_ALTERNATE = 4 +PRICE_TYPE_FREQUENT_SHOPPER = 5 +PRICE_TYPE_MFR_SUGGESTED = 901 + +PRICE_TYPE = { + PRICE_TYPE_REGULAR : "Regular Price", + PRICE_TYPE_TPR : "TPR", + PRICE_TYPE_SALE : "Sale", + PRICE_TYPE_MANAGER_SPECIAL : "Manager Special", + PRICE_TYPE_ALTERNATE : "Alternate Price", + PRICE_TYPE_FREQUENT_SHOPPER : "Frequent Shopper", + PRICE_TYPE_MFR_SUGGESTED : "Manufacturer's Suggested", + } + + +UNIT_OF_MEASURE_EACH = '01' +UNIT_OF_MEASURE_POUND = '49' + +UNIT_OF_MEASURE = { + UNIT_OF_MEASURE_EACH : "Each", + UNIT_OF_MEASURE_POUND : "Pound", + } diff --git a/rattail/exceptions.py b/rattail/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..16a1f34792f5509f2e85c427185e1029b86452ca --- /dev/null +++ b/rattail/exceptions.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2012 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 Affero 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 Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Rattail. If not, see . +# +################################################################################ + +""" +``rattail.exceptions`` -- Exceptions +""" + + +class LabelPrintingError(Exception): + + pass diff --git a/rattail/filemon.py b/rattail/filemon.py index ff073fd53d2092b6a007ae6ea8322a1399f8b361..d01ca3ae841deb3bcb561e3a3c146d7e0cbec12b 100644 --- a/rattail/filemon.py +++ b/rattail/filemon.py @@ -31,7 +31,7 @@ from edbob.filemon.win32 import FileMonitorService class RattailFileMonitor(FileMonitorService): - _svc_name_ = "Rattail File Monitor" + _svc_name_ = "RattailFileMonitor" _svc_display_name_ = "Rattail : File Monitoring Service" appname = 'rattail' diff --git a/rattail/gpc.py b/rattail/gpc.py new file mode 100644 index 0000000000000000000000000000000000000000..f82625ce1e80450c3fe53b9dc824883bd224d39b --- /dev/null +++ b/rattail/gpc.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2012 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 Affero 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 Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Rattail. If not, see . +# +################################################################################ + +""" +``rattail.gpc`` -- Global Product Code +""" + +from sqlalchemy import types + +from rattail import barcodes + + +class GPC(object): + """ + Class to abstract the details of Global Product Code data. Examples of + this would be UPC or EAN barcodes. + + The initial motivation for this class was to provide better SIL support. + To that end, the instances are assumed to always be comprised of only + numeric digits, and must include a check digit. If you do not know the + check digit, provide a ``calc_check_digit`` value to the constructor. + """ + + def __init__(self, value, calc_check_digit=False): + """ + Constructor. ``value`` must be either an integer or a long value, or a + string containing only digits. + + If ``calc_check_digit`` is ``False``, then ``value`` is assumed to + include the check digit. If the value does not include a check digit + and needs one to be calculated, then ``calc_check_digit`` should be a + keyword signifying the algorithm to be used. + + Currently the only check digit algorithm keyword supported is + ``'upc'``. As that is likely to always be the default, a + ``calc_check_digit`` value of ``True`` will be perceived as equivalent + to ``'upc'``. + """ + + value = str(value) + if calc_check_digit is True or calc_check_digit == 'upc': + value += str(barcodes.upc_check_digit(value)) + self.value = int(value) + + def __cmp__(self, other): + if int(self) < int(other): + return -1 + if int(self) > int(other): + return 1 + if int(self) == int(other): + return 0 + assert False + + def __hash__(self): + return hash(self.value) + + def __int__(self): + return int(self.value) + + def __long__(self): + return long(self.value) + + def __repr__(self): + return "GPC('%014d')" % self.value + + def __str__(self): + return str(unicode(self)) + + def __unicode__(self): + return u'%014d' % self.value + + +class GPCType(types.TypeDecorator): + """ + SQLAlchemy type engine for GPC data. + """ + + impl = types.BigInteger + + def process_bind_param(self, value, dialect): + if value is None: + return None + return int(value) + + def process_result_value(self, value, dialect): + if value is None: + return None + return GPC(value) diff --git a/rattail/labels.py b/rattail/labels.py index b536b01e94b740b9ce6f06d3792b1fb76da37d43..4fc312a98d0a869a99b773cb65ce722389369b37 100644 --- a/rattail/labels.py +++ b/rattail/labels.py @@ -29,18 +29,13 @@ import os import os.path import socket +import shutil from cStringIO import StringIO -try: - from collections import OrderedDict -except ImportError: - from ordereddict import OrderedDict - import edbob from edbob.util import requires_impl - -_profiles = OrderedDict() +from rattail.exceptions import LabelPrintingError class LabelPrinter(edbob.Object): @@ -55,6 +50,7 @@ class LabelPrinter(edbob.Object): profile_name = None formatter = None + required_settings = None @requires_impl() def print_labels(self, labels, *args, **kwargs): @@ -73,6 +69,9 @@ class CommandFilePrinter(LabelPrinter): from there. """ + required_settings = {'output_dir': "Output Folder"} + output_dir = None + def batch_header_commands(self): """ This method, if implemented, must return a sequence of string commands @@ -91,7 +90,7 @@ class CommandFilePrinter(LabelPrinter): return None - def print_labels(self, labels, output_dir=None): + def print_labels(self, labels, output_dir=None, progress=None): """ "Prints" ``labels`` by generating a command file in the output folder. The full path of the output file to which commands are written will be @@ -103,28 +102,36 @@ class CommandFilePrinter(LabelPrinter): current (working) directory will be assumed. """ - if not output_dir and self.profile_name: - output_dir = edbob.config.get('rattail.labels', '%s.output_dir' % self.profile_name) if not output_dir: - output_dir = os.getcwd() + output_dir = self.output_dir + if not output_dir: + raise LabelPrintingError("Printer does not have an output folder defined") - fn = '%s_%s.labels' % (socket.gethostname(), - edbob.local_time().strftime('%Y-%m-%d_%H-%M-%S')) - labels_path = os.path.join(output_dir, fn) + labels_path = edbob.temp_path(prefix='rattail.', suffix='.labels') labels_file = open(labels_path, 'w') header = self.batch_header_commands() if header: labels_file.write('%s\n' % '\n'.join(header)) - labels_file.write(self.formatter.format_labels(labels)) + commands = self.formatter.format_labels(labels, progress=progress) + if commands is None: + labels_file.close() + os.remove(labels_path) + return None + + labels_file.write(commands) footer = self.batch_footer_commands() if footer: labels_file.write('%s\n' % '\n'.join(footer)) labels_file.close() - return labels_path + fn = '%s_%s.labels' % (socket.gethostname(), + edbob.local_time().strftime('%Y-%m-%d_%H-%M-%S')) + final_path = os.path.join(output_dir, fn) + shutil.move(labels_path, final_path) + return final_path class LabelFormatter(edbob.Object): @@ -149,11 +156,16 @@ class CommandFormatter(LabelFormatter): (textual) commands. """ - def format_labels(self, labels): + def format_labels(self, labels, progress=None): + prog = None + if progress: + prog = progress("Formatting labels", len(labels)) + fmt = StringIO() - for product, quantity in labels: - for i in range(quantity): + cancel = False + for i, (product, quantity) in enumerate(labels, 1): + for j in range(quantity): header = self.label_header_commands() if header: fmt.write('%s\n' % '\n'.join(header)) @@ -161,6 +173,16 @@ class CommandFormatter(LabelFormatter): footer = self.label_footer_commands() if footer: fmt.write('%s\n' % '\n'.join(footer)) + if prog and not prog.update(i): + cancel = True + break + + if prog: + prog.destroy() + + if cancel: + fmt.close() + return None val = fmt.getvalue() fmt.close() @@ -238,68 +260,3 @@ class TwoUpCommandFormatter(CommandFormatter): val = fmt.getvalue() fmt.close() return val - - -class LabelProfile(edbob.Object): - """ - Represents a label printing profile. This abstraction is used to define - not only the physical (or otherwise?) device to which label should be sent, - but the label formatting specifics as well. - """ - - name = None - display_name = None - printer_factory = None - formatter_factory = None - format = None - - def get_formatter(self): - if self.formatter_factory: - return self.formatter_factory(format=self.format) - return None - - def get_printer(self): - if self.printer_factory: - return self.printer_factory( - profile_name=self.name, - formatter=self.get_formatter()) - return None - - -def init(config): - """ - Initializes the label printing system. - - This reads label profiles from config and caches the corresponding - :class:`LabelProfile` instances in memory. - """ - - profiles = config.require('rattail.labels', 'profiles') - profiles = profiles.split(',') - for key in profiles: - key = key.strip() - profile = LabelProfile(name=key) - profile.display_name = config.require('rattail.labels', '%s.display' % key) - profile.printer_factory = edbob.load_spec( - config.require('rattail.labels', '%s.printer' % key)) - profile.formatter_factory = edbob.load_spec( - config.require('rattail.labels', '%s.formatter' % key)) - profile.format = config.get('rattail.labels', '%s.format' % key) - _profiles[key] = profile - - -def get_profile(name): - """ - Returns the :class:`LabelProfile` instance corresponding to ``name``. - """ - - return _profiles.get(name) - - -def iter_profiles(): - """ - Returns an iterator over the collection of :class:`LabelProfile` instances - which were read and created from config. - """ - - return _profiles.itervalues() diff --git a/rattail/sil.py b/rattail/sil.py deleted file mode 100644 index 729f515e5bd2a177b7d4ba557c3cccdf11a8c55b..0000000000000000000000000000000000000000 --- a/rattail/sil.py +++ /dev/null @@ -1,286 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -################################################################################ -# -# Rattail -- Retail Software Framework -# Copyright © 2010-2012 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 Affero 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 Affero General Public License for -# more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with Rattail. If not, see . -# -################################################################################ - -""" -``rattail.sil`` -- Standard Interchange Language - -Please see the `Standard Interchange Language Specifications -`_ -for more information. -""" - -import datetime -from decimal import Decimal - -import edbob - -import rattail - - -def val(value): - """ - Returns a string version of ``value``, suitable for inclusion within a data - row of a SIL batch. The conversion is done as follows: - - If ``value`` is ``None``, an empty string is returned. - - If it is an ``int`` or ``decimal.Decimal`` instance, it is converted - directly to a string (i.e. not quoted). - - If it is a ``datetime.date`` instance, it will be formatted as ``'%Y%j'``. - - If it is a ``datetime.time`` instance, it will be formatted as ``'%H%M'``. - - Otherwise, it is converted to a string if necessary, and quoted with - apostrophes escaped. - """ - - if value is None: - return '' - if isinstance(value, int): - return str(value) - if isinstance(value, Decimal): - return str(value) - if isinstance(value, datetime.date): - return value.strftime('%Y%j') - if isinstance(value, datetime.time): - return value.strftime('%H%M') - if not isinstance(value, basestring): - value = str(value) - return "'%s'" % value.replace("'", "''") - - -def consume_batch_id(): - """ - Returns the next available batch identifier, incrementing the number to - preserve uniqueness. - """ - - config = edbob.AppConfigParser('rattail') - config_path = config.get_user_file('rattail.conf', create=True) - config.read(config_path) - - batch_id = config.get('rattail.sil', 'next_batch_id', default='') - if not batch_id.isdigit(): - batch_id = '1' - batch_id = int(batch_id) - - config.set('rattail.sil', 'next_batch_id', str(batch_id + 1)) - config_file = open(config_path, 'w') - config.write(config_file) - config_file.close() - return '%08u' % batch_id - - -def write_batch_header(fileobj, H03='RATAIL', **kwargs): - """ - Writes a SIL batch header string to ``fileobj``. All keyword arguments - correspond to the SIL specification for the Batch Header Dictionary. - - If you do not override ``H03`` (Source Identifier), then Rattail will - provide a default value for ``H20`` (Software Revision) - that is, unless - you've supplied it yourself. - - **Batch Header Dictionary:** - - ==== ==== ==== =========== - Name Type Size Description - ==== ==== ==== =========== - H01 CHAR 2 Batch Type - H02 CHAR 8 Batch Identifier - H03 CHAR 6 Source Identifier - H04 CHAR 6 Destination Identifier - H05 CHAR 12 Audit File Name - H06 CHAR 12 Response File Name - H07 DATE 7 Origin Date - H08 TIME 4 Origin Time - H09 DATE 7 Execution (Apply) Date - H10 DATE 4 Execution (Apply) Time - H11 DATE 7 Purge Date - H12 CHAR 6 Action Type - H13 CHAR 50 Batch Description - H14 CHAR 30 User Defined - H15 CHAR 30 User Defined - H16 CHAR 30 User Defined - H17 NUMBER 1 Warning Level - H18 NUMBER 5 Maximum Error Count - H19 CHAR 7 SIL Level/Revision - H20 CHAR 4 Software Revision - H21 CHAR 50 Primary Key - H22 CHAR 512 System Specific Command - H23 CHAR 8 Dictionary Revision - - Consult the SIL Specification for more information. - """ - - kw = kwargs - - # Provide default for H20 if batch origin is 'RATAIL'. - H20 = kw.get('H20') - if H03 == 'RATAIL' and H20 is None: - H20 = rattail.__version__[:4] - - # Don't quote H09 if special "immediate" value. - H09 = kw.get('H09') - if H09 != '0000000': - H09 = val(H09) - - # Don't quote H10 if special "immediate" value. - H10 = kw.get('H10') - if H10 != '0000': - H10 = val(H10) - - row = [ - val(kw.get('H01')), - val(kw.get('H02')), - val(H03), - val(kw.get('H04')), - val(kw.get('H05')), - val(kw.get('H06')), - val(kw.get('H07')), - val(kw.get('H08')), - H09, - H10, - val(kw.get('H11')), - val(kw.get('H12')), - val(kw.get('H13')), - val(kw.get('H14')), - val(kw.get('H15')), - val(kw.get('H16')), - val(kw.get('H17')), - val(kw.get('H18')), - val(kw.get('H19')), - val(H20), - val(kw.get('H21')), - val(kw.get('H22')), - val(kw.get('H23')), - ] - - fileobj.write('INSERT INTO HEADER_DCT VALUES\n') - write_row(fileobj, row, quote=False, last=True) - fileobj.write('\n') - - -def write_row(fileobj, row, quote=True, last=False): - """ - Writes a SIL row string to ``fileobj``. - - ``row`` should be a sequence of values. - - If ``quote`` is ``True``, each value in ``row`` will be ran through the - :func:`val()` function before being written. If it is ``False``, the - values are written as-is. - - If ``last`` is ``True``, then ``';'`` will be used as the statement - terminator; otherwise ``','`` is used. - """ - - terminator = ';' if last else ',' - if quote: - row = [val(x) for x in row] - fileobj.write('(' + ','.join(row) + ')' + terminator + '\n') - - -def write_rows(fileobj, rows): - """ - Writes a set of SIL row strings to ``fileobj``. - - ``rows`` should be a sequence of sequences, each of which should be - suitable for use with :func:`write_row()`. - - (This funcion primarily exists to handle the mundane task of setting the - ``last`` flag when calling :func:`write_row()`.) - """ - - last = len(rows) - 1 - for i, row in enumerate(rows): - write_row(fileobj, row, last=i == last) - - -# # from pkg_resources import iter_entry_points - -# # import rattail -# # from rattail.batch import make_batch, RattailBatchTerminal -# from rattail.batches import RattailBatchTerminal - - -# # _junctions = None - - -# # class SILError(Exception): -# # """ -# # Base class for SIL errors. -# # """ - -# # pass - - -# # class ElementRequiredError(SILError): -# # """ -# # Raised when a batch import or export is attempted, but the element list -# # supplied is missing a required element. -# # """ - -# # def __init__(self, required, using): -# # self.required = required -# # self.using = using - -# # def __str__(self): -# # return "The element list supplied is missing required element '%s': %s" % ( -# # self.required, self.using) - - -# def default_display(field): -# """ -# Returns the default UI display value for a SIL field, according to the -# Rattail field map. -# """ - -# return RattailBatchTerminal.fieldmap_user[field] - - -# # def get_available_junctions(): -# # """ -# # Returns a dictionary of available :class:`rattail.BatchJunction` classes, -# # keyed by entry point name. -# # """ - -# # global _junctions -# # if _junctions is None: -# # _junctions = {} -# # for entry_point in iter_entry_points('rattail.batch_junctions'): -# # _junctions[entry_point.name] = entry_point.load() -# # return _junctions - - -# # def get_junction_display(name): -# # """ -# # Returns the ``display`` value for a registered -# # :class:`rattail.BatchJunction` class, given its ``name``. -# # """ - -# # juncs = get_available_junctions() -# # if name in juncs: -# # return juncs[name].display -# # return None diff --git a/rattail/sil/__init__.py b/rattail/sil/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3fde469649b85d77028fbedebe1001edf305361e --- /dev/null +++ b/rattail/sil/__init__.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2012 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 Affero 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 Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Rattail. If not, see . +# +################################################################################ + +""" +``rattail.sil`` -- Standard Interchange Language + +Please see the `Standard Interchange Language Specifications +`_ +for more information. +""" + +from rattail.sil.columns import * +from rattail.sil.batches import * +from rattail.sil.sqlalchemy import * +from rattail.sil.writer import * diff --git a/rattail/sil/batches.py b/rattail/sil/batches.py new file mode 100644 index 0000000000000000000000000000000000000000..61a89a840bfafaea6fff00dba4aba4a760b7d364 --- /dev/null +++ b/rattail/sil/batches.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2012 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 Affero 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 Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Rattail. If not, see . +# +################################################################################ + +""" +``rattail.sil.batches`` -- Batch Stuff +""" + +import edbob + + +__all__ = ['consume_batch_id'] + + +def consume_batch_id(source='RATAIL'): + """ + Returns the next available batch identifier for ``source``, incrementing + the number to preserve uniqueness. + """ + + option = 'next_batch_id.%s' % source + + config = edbob.AppConfigParser('rattail') + config_path = config.get_user_file('rattail.conf', create=True) + config.read(config_path) + + batch_id = config.get('rattail.sil', option, default='') + if not batch_id.isdigit(): + batch_id = '1' + batch_id = int(batch_id) + + config.set('rattail.sil', option, str(batch_id + 1)) + config_file = open(config_path, 'w') + config.write(config_file) + config_file.close() + return '%08u' % batch_id diff --git a/rattail/sil/columns.py b/rattail/sil/columns.py new file mode 100644 index 0000000000000000000000000000000000000000..c3d44c49f6bebded51cfcd9211434e412a62cd11 --- /dev/null +++ b/rattail/sil/columns.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2012 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 Affero 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 Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Rattail. If not, see . +# +################################################################################ + +""" +``rattail.sil.columns`` -- SIL Columns +""" + +import edbob + +from rattail.sil.exceptions import SILColumnNotFound + + +__all__ = ['get_column'] + + +supported_columns = {} + + +class SILColumn(edbob.Object): + """ + Represents a column for use with SIL. + """ + + def __init__(self, name, data_type, description, display_name=None, **kwargs): + edbob.Object.__init__(self, **kwargs) + self.name = name + self.data_type = data_type + self.description = description + self.display_name = display_name or description + + def __repr__(self): + return "" % self.name + + def __unicode__(self): + return unicode(self.name) + + +def update_supported_columns(columns): + """ + Updates the global column collection with those natively supported by + Rattail. + """ + + SC = SILColumn + + standard = [ # These columns are part of the SIL standard. + + # ITEM_DCT + SC('F01', 'GPC(14)', "Primary Item U.P.C. Number (Key)", "UPC"), + SC('F02', 'CHAR(20)', "Descriptor", "Description"), + SC('F04', 'NUMBER(4,0)', "Sub-Department Number"), + SC('F22', 'CHAR(30)', "Size Description"), + SC('F90', 'FLAG(1)', "Authorized DSD Item"), + SC('F94', 'NUMBER(2,0)', "Shelf Tag Quantity"), + SC('F95', 'CHAR(3)', "Shelf Tag Type"), + SC('F155', 'CHAR(30)', "Brand"), + + # PRICE_DCT + SC('F30', 'NUMBER(8,3)', "Retail Sell Price"), + SC('F31', 'NUMBER(3,0)', "Price Multiple (Quantity/For)"), + SC('F35', 'DATE(7)', "Price Start Date"), + SC('F36', 'TIME(4)', "Price Start Time"), + SC('F126', 'NUMBER(2,0)', "Pricing Level"), + SC('F129', 'DATE(7)', "Price End Date"), + SC('F130', 'TIME(4)', "Price End Time"), + SC('F135', 'NUMBER(3,0)', "Sale Price Multiple (Quantity/For)"), + SC('F136', 'NUMBER(8,3)', "Sale Price"), + SC('F137', 'DATE(7)', "Sale Price Start Date"), + SC('F138', 'TIME(4)', "Sale Price End Date"), + SC('F139', 'NUMBER(8,3)', "Sale Package Price"), + SC('F140', 'NUMBER(8,3)', "Package Price"), + SC('F142', 'NUMBER(3,0)', "Package Price Multiple"), + SC('F143', 'NUMBER(3,0)', "Sale Package Price Multiple"), + SC('F144', 'TIME(4)', "Sale Price Start Time"), + SC('F145', 'TIME(4)', "Sale Price End Time"), + SC('F181', 'NUMBER(8,3)', "TPR (Temporary Price Reduction)"), + SC('F182', 'NUMBER(3,0)', "TPR Multiple (Quantity/For)"), + SC('F183', 'DATE(7)', "TPR Start Date"), + SC('F184', 'DATE(7)', "TPR End Date"), + SC('F387', 'NUMBER(3)', "Price Type Code"), + + # FCOST_DCT + SC('F19', 'NUMBER(4,0)', "Case Pack Size"), + SC('F20', 'NUMBER(4,0)', "Receiving Pack Size"), + SC('F38', 'NUMBER(9,5)', "Case Receiving Base Cost"), + SC('F212', 'TIME(4)', "Cost Change Time"), + SC('F227', 'DATE(7)', "Cost Change Date"), + + # DEPT_DCT + SC('F03', 'NUMBER(4,0)', "Department Number"), + SC('F238', 'CHAR(30)', "Department Description"), + + # VENDOR_DCT + SC('F27', 'CHAR(9)', "Vendor Number"), + SC('F334', 'CHAR(20)', "Vendor Name"), + SC('F335', 'CHAR(20)', "Vendor Contact Name"), + SC('F341', 'NUMBER(10,0)', "Vendor Phone Number - Voice"), + SC('F342', 'NUMBER(10,0)', "Vendor Phone Number - Fax"), + ] + + custom = [ # These columns are Rattail-specific. + + SC('R38', 'NUMBER(9,5)', "Unit Receiving Base Cost"), + SC('R101', 'NUMBER(9,5)', "Difference Amount", "Difference"), + ] + + for column in standard + custom: + columns[column.name] = column + + +def get_column(name): + """ + Returns the :class:`SILColumn` instance named ``name``. + """ + + column = supported_columns.get(name) + if not column: + raise SILColumnNotFound(name) + return column + + +def init(config): + """ + Initializes the collection of supported SIL columns. + """ + + global supported_columns + + update_supported_columns(supported_columns) diff --git a/rattail/sil/exceptions.py b/rattail/sil/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..629a0e5ab47c42e86c17aca7f3ae644ad4ca7421 --- /dev/null +++ b/rattail/sil/exceptions.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2012 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 Affero 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 Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Rattail. If not, see . +# +################################################################################ + +""" +``rattail.sil.exceptions`` -- SIL Exceptions +""" + + +class SILError(Exception): + + pass + + +class SILColumnNotFound(SILError): + + def __init__(self, name): + self.name = name + + def __str__(self): + return "SIL column not found: %s" % self.name diff --git a/rattail/sil/sqlalchemy.py b/rattail/sil/sqlalchemy.py new file mode 100644 index 0000000000000000000000000000000000000000..ffe61f1b69180bd44602ea15b325ab18d07a6ae3 --- /dev/null +++ b/rattail/sil/sqlalchemy.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2012 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 Affero 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 Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Rattail. If not, see . +# +################################################################################ + +""" +``rattail.sil.sqlalchemy`` -- SQLAlchemy Utilities +""" + +from __future__ import absolute_import + +import re + +from sqlalchemy import types + +from rattail.gpc import GPCType + + +__all__ = ['get_sqlalchemy_type'] + + +sil_type_pattern = re.compile(r'^(CHAR|NUMBER)\((\d+(?:\,\d+)?)\)$') + + +def get_sqlalchemy_type(sil_type): + """ + Returns a SQLAlchemy data type according to a SIL data type. + """ + + if sil_type == 'GPC(14)': + return GPCType + + if sil_type == 'FLAG(1)': + return types.Boolean + + m = sil_type_pattern.match(sil_type) + if m: + data_type, precision = m.groups() + if precision.isdigit(): + precision = int(precision) + scale = 0 + else: + precision, scale = precision.split(',') + precision = int(precision) + scale = int(scale) + if data_type == 'CHAR': + assert not scale, "FIXME" + return types.String(precision) + if data_type == 'NUMBER': + return types.Numeric(precision, scale) + + assert False, "FIXME" diff --git a/rattail/sil/writer.py b/rattail/sil/writer.py new file mode 100644 index 0000000000000000000000000000000000000000..9adec3f07142d67d88e3ae8ad9529c578f161eb1 --- /dev/null +++ b/rattail/sil/writer.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2012 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 Affero 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 Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Rattail. If not, see . +# +################################################################################ + +""" +``rattail.sil.writer`` -- SIL Writer +""" + +import datetime +from decimal import Decimal + +import edbob + +import rattail + + +__all__ = ['Writer'] + + +class Writer(edbob.Object): + + def __init__(self, path, **kwargs): + edbob.Object.__init__(self, **kwargs) + self.sil_path = path + self.fileobj = self.get_fileobj() + + def get_fileobj(self): + return open(self.sil_path, 'w') + + def close(self): + self.fileobj.close() + + def val(self, value): + """ + Returns a string version of ``value``, suitable for inclusion within a + data row of a SIL batch. The conversion is done as follows: + + If ``value`` is ``None``, an empty string is returned. + + If it is an ``int`` or ``decimal.Decimal`` instance, it is converted + directly to a string (i.e. not quoted). + + If it is a ``datetime.date`` instance, it will be formatted as + ``'%Y%j'``. + + If it is a ``datetime.time`` instance, it will be formatted as + ``'%H%M'``. + + Otherwise, it is converted to a string if necessary, and quoted with + apostrophes escaped. + """ + + if value is None: + return '' + if isinstance(value, rattail.GPC): + return str(value) + if isinstance(value, int): + return str(value) + if isinstance(value, Decimal): + return str(value) + if isinstance(value, datetime.date): + return value.strftime('%Y%j') + if isinstance(value, datetime.time): + return value.strftime('%H%M') + if not isinstance(value, basestring): + value = str(value) + return "'%s'" % value.replace("'", "''") + + def write(self, string): + self.fileobj.write(string) + + def write_batch_header_raw(self, **kwargs): + """ + Writes a SIL batch header string. All keyword arguments correspond to + the SIL specification for the Batch Header Dictionary. + + **Batch Header Dictionary:** + + ==== ==== ==== =========== + Name Type Size Description + ==== ==== ==== =========== + H01 CHAR 2 Batch Type + H02 CHAR 8 Batch Identifier + H03 CHAR 6 Source Identifier + H04 CHAR 6 Destination Identifier + H05 CHAR 12 Audit File Name + H06 CHAR 12 Response File Name + H07 DATE 7 Origin Date + H08 TIME 4 Origin Time + H09 DATE 7 Execution (Apply) Date + H10 DATE 4 Execution (Apply) Time + H11 DATE 7 Purge Date + H12 CHAR 6 Action Type + H13 CHAR 50 Batch Description + H14 CHAR 30 User Defined + H15 CHAR 30 User Defined + H16 CHAR 30 User Defined + H17 NUMBER 1 Warning Level + H18 NUMBER 5 Maximum Error Count + H19 CHAR 7 SIL Level/Revision + H20 CHAR 4 Software Revision + H21 CHAR 50 Primary Key + H22 CHAR 512 System Specific Command + H23 CHAR 8 Dictionary Revision + + Consult the SIL Specification for more information. + """ + + kw = kwargs + + # Don't quote H09 if special "immediate" value. + H09 = kw.get('H09') + if H09 != '0000000': + H09 = self.val(H09) + + # Don't quote H10 if special "immediate" value. + H10 = kw.get('H10') + if H10 != '0000': + H10 = self.val(H10) + + row = [ + self.val(kw.get('H01')), + self.val(kw.get('H02')), + self.val(kw.get('H03')), + self.val(kw.get('H04')), + self.val(kw.get('H05')), + self.val(kw.get('H06')), + self.val(kw.get('H07')), + self.val(kw.get('H08')), + H09, + H10, + self.val(kw.get('H11')), + self.val(kw.get('H12')), + self.val(kw.get('H13')), + self.val(kw.get('H14')), + self.val(kw.get('H15')), + self.val(kw.get('H16')), + self.val(kw.get('H17')), + self.val(kw.get('H18')), + self.val(kw.get('H19')), + self.val(kw.get('H20')), + self.val(kw.get('H21')), + self.val(kw.get('H22')), + self.val(kw.get('H23')), + ] + + self.fileobj.write('INSERT INTO HEADER_DCT VALUES\n') + self.write_row(row, quote=False, last=True) + self.fileobj.write('\n') + + def write_batch_header(self, **kwargs): + """ + Convenience method to take some of the gruntwork out of writing batch + headers. + + If you do not override ``H03`` (Source Identifier), then Rattail will + provide a default value for it, as well as ``H20`` (Software Revision) + - that is, unless you've supplied it yourself. + + If you do not provide values for ``H07`` or ``H08``, the current date + and time will be assumed. + + If you do not provide values for ``H09`` or ``H10``, it is assumed that + you wish the batch to be immediately executable. Default values will + be provided accordingly. + + If you do not provide a value for ``H11`` (Purge Date), a default of 90 + days from the current date will be assumed. + """ + + kw = kwargs + + # Provide default for H03 (Source Identifier) if none specified. + if 'H03' not in kw: + kw['H03'] = 'RATAIL' + + # Provide default for H20 (Software Revision) if none specified. + if 'H20' not in kw: + kw['H20'] = rattail.__version__[:4] + + # Provide default (current local time) values H07 and H08 (Origin Date / + # Time) if none was specified. + now = edbob.local_time() + if 'H07' not in kw: + kw['H07'] = now.date() + if 'H08' not in kw: + kw['H08'] = now.time() + + # Use special "immediate" values for H09 and H10 (Execution (Apply) Date / + # Time) if none was specified. + if 'H09' not in kw: + kw['H09'] = '0000000' + if 'H10' not in kw: + kw['H10'] = '0000' + + # Provide default value for H11 (Purge Date) if none was specified. + if 'H11' not in kw: + kw['H11'] = (now + datetime.timedelta(days=90)).date() + + self.write_batch_header_raw(**kw) + + def write_create_header(self, **kwargs): + """ + Convenience method to take some of the gruntwork out of writing batch + headers. + + The following default values are provided by this method: + + * ``H01`` = ``'HC'`` + * ``H12`` = ``'LOAD'`` + + This method also calls :meth:`write_batch_header()`; see its + documentation for the other default values provided. + """ + + kw = kwargs + kw.setdefault('H01', 'HC') + kw.setdefault('H12', 'LOAD') + self.write_batch_header(**kw) + + def write_maintenance_header(self, **kwargs): + """ + Convenience method to take some of the gruntwork out of writing batch + headers. + + The following default values are provided by this method: + + * ``H01`` = ``'HM'`` + + This method also calls :meth:`write_batch_header()`; see its + documentation for the other default values provided. + """ + + kw = kwargs + kw.setdefault('H01', 'HM') + self.write_batch_header(**kw) + + def write_row(self, row, quote=True, last=False): + """ + Writes a SIL row string. + + ``row`` should be a sequence of values. + + If ``quote`` is ``True``, each value in ``row`` will be ran through the + :func:`val()` function before being written. If it is ``False``, the + values are written as-is. + + If ``last`` is ``True``, then ``';'`` will be used as the statement + terminator; otherwise ``','`` is used. + """ + + terminator = ';' if last else ',' + if quote: + row = [self.val(x) for x in row] + self.fileobj.write('(' + ','.join(row) + ')' + terminator + '\n') + + def write_rows(self, rows): + """ + Writes a set of SIL row strings. + + ``rows`` should be a sequence of sequences, each of which should be + suitable for use with :meth:`write_row()`. + + (This funcion primarily exists to handle the mundane task of setting + the ``last`` flag when calling :meth:`write_row()`.) + """ + + last = len(rows) - 1 + for i, row in enumerate(rows): + self.write_row(row, last=i == last) diff --git a/setup.py b/setup.py index 5fb7b12a19e9c1dcc1dd10b66856aa1862572ea2..eb7e3a5a5ac45a146cee7d8ae87aed3737877d18 100644 --- a/setup.py +++ b/setup.py @@ -114,5 +114,8 @@ rattail = rattail.db.extension:RattailExtension [rattail.commands] filemon = rattail.commands:FileMonitorCommand +[rattail.batches.providers] +print_labels = rattail.batches.providers.labels:PrintLabels + """, )