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
+
""",
)