diff --git a/rattail/db/batches/__init__.py b/rattail/db/batches/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..ad7863ddb32c0975e3f2ee79659c6f5aec23abc9
--- /dev/null
+++ b/rattail/db/batches/__init__.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.db.batches`` -- Batch API
+"""
+
+from rattail.db.batches.types import *
+from rattail.db.batches.data import *
+from rattail.db.batches.makers import *
+from rattail.db.batches.executors import *
diff --git a/rattail/db/batches/data.py b/rattail/db/batches/data.py
new file mode 100644
index 0000000000000000000000000000000000000000..6d8c2b8edb4a4041e5b796bb6aa944d653846cf7
--- /dev/null
+++ b/rattail/db/batches/data.py
@@ -0,0 +1,111 @@
+#!/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.db.batches.data`` -- Batch Data Providers
+"""
+
+import csv
+
+from edbob.files import count_lines
+
+
+__all__ = ['ProductQueryDataProvider', 'CSVDataProxy', 'CSVDataProvider']
+
+
+class BatchDataProvider(object):
+
+ def __len__(self):
+ raise NotImplementedError
+
+ def __iter__(self):
+ raise NotImplementedError
+
+
+class QueryDataProvider(BatchDataProvider):
+
+ def __init__(self, query):
+ self.query = query
+
+ def __len__(self):
+ return self.query.count()
+
+ def __iter__(self):
+ for data in self.query:
+ yield data
+
+
+class ProductDataProxy(object):
+
+ def __init__(self, product):
+ self.product = product
+
+ def __getattr__(self, name):
+ if name == 'F01':
+ return self.product.upc
+ if name == 'F02':
+ return self.product.description
+ if name == 'F22':
+ return self.product.size
+ if name == 'F155':
+ if self.product.brand:
+ return self.product.brand.name
+ return ''
+ raise AttributeError("Product has no attribute '%s'" % name)
+
+
+class ProductQueryDataProvider(QueryDataProvider):
+
+ def __iter__(self):
+ for product in self.query:
+ yield ProductDataProxy(product)
+
+
+class CSVDataProxy(object):
+
+ def __init__(self, row):
+ self.row = row
+
+ def __getattr__(self, name):
+ if name in self.row:
+ return self.row[name]
+ raise AttributeError("CSV data row has no attribute '%s'" % name)
+
+
+class CSVDataProvider(BatchDataProvider):
+
+ proxy_class = CSVDataProxy
+
+ def __init__(self, csv_path):
+ self.csv_path = csv_path
+
+ def __len__(self):
+ return count_lines(self.csv_path)
+
+ def __iter__(self):
+ csv_file = open(self.csv_path, 'rb')
+ reader = csv.DictReader(csv_file)
+ for row in reader:
+ yield self.proxy_class(row)
+ csv_file.close()
diff --git a/rattail/db/batches/executors.py b/rattail/db/batches/executors.py
new file mode 100644
index 0000000000000000000000000000000000000000..fc2415cb1066c90a23919754198b4411d715d2a2
--- /dev/null
+++ b/rattail/db/batches/executors.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.db.batches.executors`` -- Batch Executors
+"""
+
+import pkg_resources
+
+from sqlalchemy.orm import object_session
+
+# from rattail.db.model import LabelProfile, Product
+from rattail.exceptions import BatchExecutorNotFound, BatchTypeNotSupported
+
+
+__all__ = ['get_batch_executor', 'BatchExecutor']
+
+
+batch_executors = None
+
+
+def get_batch_executor(name):
+ global batch_executors
+ if batch_executors is None:
+ batch_executors = {}
+ for entrypoint in pkg_resources.iter_entry_points('rattail.batches.executors'):
+ batch_executors[entrypoint.name] = entrypoint.load()
+ if name in batch_executors:
+ return batch_executors[name]()
+ raise BatchExecutorNotFound(name)
+
+
+class BatchExecutor(object):
+
+ batch_type = None
+
+ def execute(self, batch, progress=None):
+ # if batch.type != self.batch_type:
+ # raise BatchTypeNotSupported(self, batch.type)
+ session = object_session(batch)
+ return self.execute_batch(session, batch, progress)
+
+ def execute_batch(self, session, batch, progress=None):
+ raise NotImplementedError
+
+
+# class LabelsBatchExecutor(BatchExecutor):
+
+# batch_type = 'labels'
+
+# def execute_batch(self, session, batch, progress=None):
+# prog = None
+# if progress:
+# prog = progress("Loading product data", batch.rowcount)
+
+# profiles = {}
+
+# cancel = False
+# for i, row in enumerate(batch.rows, 1):
+
+# profile = profiles.get(row.F95)
+# if not profile:
+# q = session.query(LabelProfile)
+# q = q.filter(LabelProfile.code == row.F95)
+# profile = q.one()
+# profile.labels = []
+# profiles[row.F95] = profile
+
+# q = session.query(Product)
+# q = q.filter(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()
+# assert printer
+# if not printer.print_labels(profile.labels):
+# cancel = True
+# break
+
+# if prog:
+# prog.destroy()
+# return not cancel
diff --git a/rattail/db/batches/makers.py b/rattail/db/batches/makers.py
new file mode 100644
index 0000000000000000000000000000000000000000..4c21c16d216c75783855decb116691e81cc1b3df
--- /dev/null
+++ b/rattail/db/batches/makers.py
@@ -0,0 +1,128 @@
+#!/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.db.batches.makers`` -- Batch Makers
+"""
+
+from rattail.db.batches.types import get_batch_type
+from rattail.db.batches.data import BatchDataProvider
+# from rattail.db.model import Batch, LabelProfile
+from rattail.db.extension.model import Batch
+from rattail.sil import consume_batch_id
+
+
+class BatchMaker(object):
+
+ progress_message = "Making batch(es)"
+
+ def __init__(self, session, source=None):
+ self.session = session
+ self.source = source
+ self.batches = {}
+
+ def get_batch(self, name):
+ if name not in self.batches:
+ self.batches[name] = self.make_batch(name)
+ return self.batches[name]
+
+ def make_batch(self, name):
+ if hasattr(self, 'make_batch_%s' % name):
+ batch = getattr(self, 'make_batch_%s' % name)()
+
+ else:
+ batch_type = get_batch_type(name)
+ batch = Batch()
+ batch_type.initialize(batch)
+ if self.source:
+ batch.source = self.source
+ batch.id = consume_batch_id()
+
+ self.session.add(batch)
+ self.session.flush()
+ batch.create_table()
+ return batch
+
+ def make_batches_begin(self, data):
+ pass
+
+ def make_batches(self, data, progress=None):
+ if not isinstance(data, BatchDataProvider):
+ raise TypeError("Sorry, you must pass a BatchDataProvider instance")
+
+ result = self.make_batches_begin(data)
+ if result is not None and not result:
+ return False
+
+ prog = None
+ if progress and len(data):
+ prog = progress(self.progress_message, len(data))
+
+ cancel = False
+ for i, data_row in enumerate(data, 1):
+ self.process_data_row(data_row)
+ if prog and not prog.update(i):
+ cancel = True
+ break
+ self.session.flush()
+
+ if prog:
+ prog.destroy()
+
+ if not cancel:
+ result = self.make_batches_end()
+ if result is not None:
+ cancel = not result
+
+ return not cancel
+
+ def make_batches_end(self):
+ pass
+
+ def process_data_row(self, data_row):
+ raise NotImplementedError
+
+
+# class LabelsBatchMaker(BatchMaker):
+
+# default_profile = None
+# default_quantity = 1
+
+# def make_batches_begin(self, data):
+# if not self.default_profile:
+# q = self.session.query(LabelProfile)
+# q = q.order_by(LabelProfile.ordinal)
+# self.default_profile = q.first()
+# assert self.default_profile
+
+# def process_data_row(self, data_row):
+# batch = self.get_batch('labels')
+# row = batch.rowclass()
+# row.F01 = data_row.F01
+# row.F155 = data_row.F155
+# row.F02 = data_row.F02
+# row.F22 = data_row.F22
+# row.F95 = self.default_profile.code
+# row.F94 = self.default_quantity
+# batch.add_row(row)
diff --git a/rattail/db/batches/types.py b/rattail/db/batches/types.py
new file mode 100644
index 0000000000000000000000000000000000000000..329ab4c674fda8aaa2696fb7473b57ebd7a23ece
--- /dev/null
+++ b/rattail/db/batches/types.py
@@ -0,0 +1,94 @@
+#!/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.db.batches.types`` -- Batch Types
+"""
+
+import datetime
+import pkg_resources
+
+from edbob.time import local_time
+
+from rattail.exceptions import BatchTypeNotFound
+
+
+__all__ = ['get_batch_type', 'BatchType']
+
+
+batch_types = None
+
+
+def get_batch_type(name):
+ global batch_types
+ if batch_types is None:
+ batch_types = {}
+ for entrypoint in pkg_resources.iter_entry_points('rattail.batches.types'):
+ batch_types[entrypoint.name] = entrypoint.load()
+ if name in batch_types:
+ return batch_types[name]()
+ raise BatchTypeNotFound(name)
+
+
+class BatchType(object):
+
+ name = None
+ description = None
+ source = None
+ destination = None
+ action_type = None
+
+ purge_date_offset = None
+
+ def initialize(self, batch):
+ batch.provider = self.name
+ batch.description = self.description
+ batch.source = self.source
+ batch.destination = self.destination
+ batch.action_type = self.action_type
+ self.set_purge_date(batch)
+ self.add_columns(batch)
+
+ def set_purge_date(self, batch):
+ if self.purge_date_offset is not None:
+ today = local_time().date()
+ purge_offset = datetime.timedelta(days=self.purge_date_offset)
+ batch.purge = today + purge_offset
+
+ def add_columns(self, batch):
+ raise NotImplementedError
+
+
+# class LabelsBatchType(BatchType):
+
+# name = 'labels'
+# description = "Print Labels"
+
+# 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")
diff --git a/rattail/db/extension/model.py b/rattail/db/extension/model.py
index 336361b5da10dc96c8b83d1cbe0833d331c2b877..db3f34476a8df167f138cd4d3be8a2cddbe65305 100644
--- a/rattail/db/extension/model.py
+++ b/rattail/db/extension/model.py
@@ -218,10 +218,16 @@ class Batch(Base):
self.rowclass.__table__.drop(bind=session.bind, checkfirst=True)
def execute(self, progress=None):
- provider = self.get_provider()
- assert provider
- if not provider.execute(self, progress):
- return False
+ try:
+ provider = self.get_provider()
+ if not provider.execute(self, progress):
+ return False
+
+ except batches.BatchProviderNotFound:
+ executor = self.get_executor()
+ if not executor.execute(self, progress):
+ return False
+
self.executed = edbob.utc_time(naive=True)
object_session(self).flush()
return True
@@ -230,6 +236,11 @@ class Batch(Base):
assert self.provider
return batches.get_provider(self.provider)
+ def get_executor(self):
+ from rattail.db.batches import get_batch_executor
+ assert self.provider
+ return get_batch_executor(self.provider)
+
def iter_rows(self):
session = object_session(self)
q = session.query(self.rowclass)
diff --git a/rattail/exceptions.py b/rattail/exceptions.py
index 16a1f34792f5509f2e85c427185e1029b86452ca..8c093e293576cca8f4b416eda0789f335c1c90a6 100644
--- a/rattail/exceptions.py
+++ b/rattail/exceptions.py
@@ -27,6 +27,51 @@
"""
+class RattailError(Exception):
+ """
+ Base class for all Rattail exceptions.
+ """
+
+
+class BatchError(RattailError):
+ """
+ Base class for all batch-related errors.
+ """
+
+
+class BatchTypeNotFound(BatchError):
+
+ def __init__(self, name):
+ self.name = name
+
+ def __str__(self):
+ return "Batch type not found: %s" % self.name
+
+
+class BatchTypeNotSupported(BatchError):
+ """
+ Raised when a :class:`rattail.db.batches.BatchExecutor` instance is asked
+ to execute a batch, but the batch is of an unsupported type.
+ """
+
+ def __init__(self, executor, batch_type):
+ self.executor = executor
+ self.batch_type = batch_type
+
+ def __str__(self):
+ return "Batch type '%s' is not supported by executor: %s" % (
+ self.batch_type, repr(self.executor))
+
+
+class BatchExecutorNotFound(BatchError):
+
+ def __init__(self, name):
+ self.name = name
+
+ def __str__(self):
+ return "Batch executor not found: %s" % self.name
+
+
class LabelPrintingError(Exception):
pass