Changeset - f7c03fd618a5
[Not reviewed]
0 5 3
Lance Edgar (lance) - 15 days ago 2024-10-22 14:25:55
lance@edbob.org
feat: add support for new ordering batch from parsed file
8 files changed with 484 insertions and 11 deletions:
0 comments (0 inline, 0 general)
docs/api/index.rst
Show inline comments
 
@@ -71,7 +71,8 @@ attributes and method signatures etc.
 
   rattail/time
 
   rattail/trainwreck/index
 
   rattail/upgrades
 
   rattail/util
 
   rattail/vendors.catalogs
 
   rattail/vendors.handler
 
   rattail/vendors.orders
 
   rattail/win32
docs/api/rattail/vendors.orders.rst
Show inline comments
 
new file 100644
 

	
 
``rattail.vendors.orders``
 
==========================
 

	
 
.. automodule:: rattail.vendors.orders
 
   :members:
pyproject.toml
Show inline comments
 
@@ -139,12 +139,16 @@ rattail = "rattail.sil.columns:provide_columns"
 
[project.entry-points."rattail.vendors.invoices.parsers"]
 
"rattail.contrib.alberts" = "rattail.contrib.vendors.invoices.alberts:AlbertsInvoiceParser"
 
"rattail.contrib.kehe" = "rattail.contrib.vendors.invoices.kehe:KeheInvoiceParser"
 
"rattail.contrib.unfi" = "rattail.contrib.vendors.invoices.unfi:UnfiInvoiceParser"
 

	
 

	
 
[project.entry-points."rattail.vendors.orders.parsers"]
 
"default" = "rattail.vendors.orders:DefaultOrderParser"
 

	
 

	
 
[project.urls]
 
Homepage = "https://rattailproject.org"
 
Repository = "https://forgejo.wuttaproject.org/rattail/rattail"
 
Issues = "https://forgejo.wuttaproject.org/rattail/rattail/issues"
 
Changelog = "https://forgejo.wuttaproject.org/rattail/rattail/src/branch/master/CHANGELOG.md"
 

	
rattail/batch/purchase.py
Show inline comments
 
@@ -82,12 +82,36 @@ class PurchaseBatchHandler(BatchHandler):
 
        Returns boolean indicating whether partial/decimal unit
 
        quantities are allowed, for ordering and/or receiving.
 
        """
 
        return self.config.get_bool('rattail.batch.purchase.allow_decimal_quantities',
 
                                    default=False)
 

	
 
    def allow_ordering_any_vendor(self):
 
        """
 
        Return boolean indicating whether ordering from "any" vendor
 
        is allowed, vs. only supported vendors.
 
        """
 
        return self.config.get_bool('rattail.batch.purchase.allow_ordering_any_vendor',
 
                                    default=True)
 

	
 
    def allow_ordering_from_scratch(self):
 
        """
 
        Return boolean indicating whether ordering "from scratch" is
 
        allowed.
 
        """
 
        return self.config.get_bool('rattail.batch.purchase.allow_ordering_from_scratch',
 
                                    default=True)
 

	
 
    def allow_ordering_from_file(self):
 
        """
 
        Return boolean indicating whether ordering "from file" is
 
        allowed.
 
        """
 
        return self.config.get_bool('rattail.batch.purchase.allow_ordering_from_file',
 
                                    default=True)
 

	
 
    def allow_receiving_from_scratch(self, **kwargs):
 
        """
 
        Return boolean indicating whether receiving "from scratch" is allowed.
 
        """
 
        return self.config.getbool('rattail.batch',
 
                                   'purchase.allow_receiving_from_scratch',
 
@@ -141,12 +165,70 @@ class PurchaseBatchHandler(BatchHandler):
 
        be auto-generated for items not accounted for.
 
        """
 
        return self.config.getbool('rattail.batch',
 
                                   'purchase.receiving.auto_missing_credits',
 
                                   default=False)
 

	
 
    def possible_ordering_workflows(self, **kwargs):
 
        """
 
        Returns a list representing all "ordering workflows" which are
 
        *possible* when making a new batch.  Note that possible !=
 
        supported.  See :meth:`supported_ordering_workflows()` for the
 
        latter.
 

	
 
        Each element of the list will be a dict with 3 items; keys of which
 
        are: ``'workflow_key'``, ``'display'`` and ``'description'``.
 

	
 
        Maybe these "creation type key" strings should be defined
 
        within the ``enum`` module, but for now they're sort of just
 
        magical hard-coded values I guess:
 

	
 
        * ``'from_scratch'``
 
        * ``'from_file'``
 
        """
 
        return [
 
            {
 
                'workflow_key': 'from_scratch',
 
                'display': "From Scratch",
 
                'description': "Create a new empty batch and start scanning.",
 
            },
 
            {
 
                'workflow_key': 'from_file',
 
                'display': "From File",
 
                'description': "Upload a single file representing the order.",
 
            },
 
        ]
 

	
 
    def supported_ordering_workflows(self, **kwargs):
 
        """
 
        Returns a list representing which "creation types" are
 
        *supported* by the app, when making a new ordering batch.
 
        Elements of this list will be of the same type as returned by
 
        :meth:`possible_ordering_workflows()`.
 
        """
 
        possible = self.possible_ordering_workflows()
 
        possible = dict([(item['workflow_key'], item)
 
                         for item in possible])
 

	
 
        workflows = []
 
        if self.allow_ordering_from_scratch():
 
            workflows.append(possible['from_scratch'])
 
        if self.allow_ordering_from_file():
 
            workflows.append(possible['from_file'])
 
        return workflows
 

	
 
    def ordering_workflow_info(self, workflow_key):
 
        """
 
        Returns the info dict for the given "creation type" key.  The
 
        dict will be one of those as returned by
 
        :meth:`possible_ordering_workflows()`.
 
        """
 
        for workflow in self.possible_ordering_workflows():
 
            if workflow['workflow_key'] == workflow_key:
 
                return workflow
 

	
 
    def possible_receiving_workflows(self, **kwargs):
 
        """
 
        Returns a list representing all "receiving workflows" which are
 
        *possible* when making a new batch.  Note that possible != supported.
 
        See :meth:`supported_receiving_workflows()` for the latter.
 

	
 
@@ -327,13 +409,13 @@ class PurchaseBatchHandler(BatchHandler):
 
            store = self.config.get_store(session)
 
            if store:
 
                kwargs['store'] = store
 

	
 
        # remove some kwargs which are not meant for the primary batch
 
        # constructor; they will be dealt with within init_batch()
 
        kwargs.pop('receiving_workflow', None)
 
        kwargs.pop('workflow', None)
 
        kwargs.pop('purchase_key', None)
 

	
 
        # continue with usual logic
 
        batch = super().make_basic_batch(session, **kwargs)
 
        return batch
 

	
 
@@ -344,24 +426,24 @@ class PurchaseBatchHandler(BatchHandler):
 
        """
 
        purchase_key = kwargs.get('purchase_key')
 
        if purchase_key:
 
            self.assign_purchase_order(batch, purchase_key,
 
                                       session=kwargs.get('session'))
 

	
 
        receiving_workflow = kwargs.get('receiving_workflow')
 
        if receiving_workflow:
 
            batch.set_param('receiving_workflow', receiving_workflow)
 
        workflow = kwargs.get('workflow')
 
        if workflow:
 
            batch.set_param('workflow', workflow)
 

	
 
    def set_input_file(self, batch, path, attr=None, **kwargs):
 
        """
 
        Custom logic for setting batch input file, to allow for
 
        receiving from multiple invoice files.
 
        """
 
        # special logic for receiving from multiple invoice files
 
        if (batch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING
 
            and batch.get_param('receiving_workflow') == 'from_multi_invoice'
 
            and batch.get_param('workflow') == 'from_multi_invoice'
 
            and attr == 'invoice_files'):
 

	
 
            # store file as normal, but instead of setting filename
 
            # attr directly on batch, store that in the params
 
            datadir = self.make_datadir(batch)
 
            filename = os.path.basename(path)
 
@@ -403,22 +485,26 @@ class PurchaseBatchHandler(BatchHandler):
 

	
 
    def should_populate(self, batch):
 
        """
 
        Must populate when e.g. making new receiving batch from PO or invoice,
 
        but otherwise not, e.g. receiving from scratch.
 
        """
 
        if batch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING:
 
        if batch.mode == self.enum.PURCHASE_BATCH_MODE_ORDERING:
 
            if batch.order_file:
 
                return True
 

	
 
        elif batch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING:
 

	
 
            workflow = batch.get_param('receiving_workflow')
 
            workflow = batch.get_param('workflow')
 
            if workflow in ('from_invoice',
 
                            'from_multi_invoice',
 
                            'from_po',
 
                            'from_po_with_invoice'):
 
                return True
 

	
 
        if batch.mode == self.enum.PURCHASE_BATCH_MODE_COSTING:
 
        elif batch.mode == self.enum.PURCHASE_BATCH_MODE_COSTING:
 
            if self.has_purchase_order(batch):
 
                return True
 
            if self.has_invoice_file(batch):
 
                return True
 

	
 
        return False
 
@@ -521,13 +607,19 @@ class PurchaseBatchHandler(BatchHandler):
 
        :attr:`~rattail.db.model.batch.purchase.PurchaseBatch.order_quantities_known`
 
        attribute set to ``True``.
 

	
 
        If the batch is a "truck dump child" and does not yet have a receiving
 
        date, it is given the same one as the parent batch.
 
        """
 
        if batch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING:
 
        if batch.mode == self.enum.PURCHASE_BATCH_MODE_ORDERING:
 

	
 
            if batch.order_file:
 
                self.populate_from_order_file(batch, progress=progress)
 
                return
 

	
 
        elif batch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING:
 

	
 
            if batch.is_truck_dump_parent():
 
                pass            # TODO?
 

	
 
            elif batch.is_truck_dump_child():
 

	
 
@@ -544,13 +636,13 @@ class PurchaseBatchHandler(BatchHandler):
 
                # assume receiving date is today, if none specified
 
                # TODO: probably should not do this here, instead
 
                # assume caller has set it already, if/as needed
 
                if not batch.date_received:
 
                    batch.date_received = self.app.today()
 

	
 
                workflow = batch.get_param('receiving_workflow')
 
                workflow = batch.get_param('workflow')
 

	
 
                if workflow == 'from_invoice':
 
                    self.populate_from_invoice(batch, progress=progress)
 
                    return
 

	
 
                if workflow == 'from_multi_invoice':
 
@@ -562,13 +654,13 @@ class PurchaseBatchHandler(BatchHandler):
 
                    return
 

	
 
                if workflow == 'from_po_with_invoice':
 
                    self.populate_from_invoice_with_po(batch, progress=progress)
 
                    return
 

	
 
        if batch.mode == self.enum.PURCHASE_BATCH_MODE_COSTING:
 
        elif batch.mode == self.enum.PURCHASE_BATCH_MODE_COSTING:
 

	
 
            if self.has_purchase_order(batch) and self.has_invoice_file(batch):
 
                self.populate_from_invoice_with_po(batch, progress=progress)
 
                return
 

	
 
            if self.has_invoice_file(batch):
 
@@ -641,12 +733,66 @@ class PurchaseBatchHandler(BatchHandler):
 
        if vendor is not batch.vendor:
 
            raise RuntimeError("Parser is for vendor '{}' "
 
                               "but batch is for: {}".format(
 
                                   parser.vendor_key, batch.vendor))
 
        return vendor
 

	
 
    def populate_from_order_file(self, batch, progress=None):
 
        """
 
        Populate an ordering batch from file.
 
        """
 
        if not batch.order_file:
 
            raise ValueError(f"batch does not have an order file: {batch}")
 

	
 
        vend = self.app.get_vendor_handler()
 
        parser = vend.get_order_parser(batch.order_parser_key, require=True)
 
        path = batch.filepath(self.config, batch.order_file)
 

	
 
        try:
 
            batch.date_ordered = parser.parse_order_date(path)
 
        except:
 
            log.warning("failed to parse order date from file: %s", path,
 
                        exc_info=True)
 

	
 
        try:
 
            batch.po_number = parser.parse_order_number(path)
 
        except:
 
            log.warning("failed to parse order number from file: %s", path,
 
                        exc_info=True)
 

	
 
        def append(item, i):
 
            row = self.make_row_from_order_parser(batch, item)
 
            self.add_row(batch, row)
 

	
 
        try:
 
            lines = list(parser.parse_order_items(path))
 
        except Exception as error:
 
            log.warning("failed to parse invoice lines from file: %s", path,
 
                        exc_info=True)
 
            error = simple_error(error)
 
            raise RuntimeError(f"Failed to parse invoice:  {error}")
 

	
 
        self.app.progress_loop(append, lines, progress,
 
                               message="Adding initial rows to batch")
 

	
 
        # set overall totals
 
        total = None
 
        try:
 
            total = parser.parse_order_total(path)
 
        except:
 
            pass
 
        if total is None:
 
            total = sum([row.po_total or 0
 
                         for row in batch.active_rows()])
 
        batch.po_total = total
 
        batch.po_total_calculated = sum([row.po_total_calculated or 0
 
                                         for row in batch.active_rows()])
 

	
 
        # set overall status
 
        self.refresh_batch_status(batch)
 

	
 
    def populate_from_invoice(self, batch, progress=None):
 
        """
 
        Populate a batch from vendor invoice file.
 
        """
 
        session = self.app.get_session(batch)
 
        parser = self.require_invoice_parser(batch)
 
@@ -1062,12 +1208,48 @@ class PurchaseBatchHandler(BatchHandler):
 
            rows = [row for row in parent_batch.active_rows()
 
                    if row.product_uuid is None
 
                    and row.item_entry == child_row.item_entry]
 
            if rows:
 
                return rows[0]
 

	
 
    def make_row_from_order_parser(self, batch, item):
 
        """
 
        Create a new batch row, from the given order item, as returned
 
        by an order file parser.
 
        """
 
        row = self.make_row()
 
        row.po_line_number = item.sequence
 

	
 
        product = item.product
 
        row.product = product
 
        if product:
 
            row.upc = product.upc
 
            row.item_id = product.item_id
 
        else:
 
            row.upc = item.upc
 
            row.item_id = item.item_id
 
            row.brand_name = item.brand_name
 
            row.description = item.description
 
            row.size = item.size
 
            row.department_name = item.department_name
 
            row.vendor_code = item.vendor_code
 
            row.case_quantity = item.case_quantity
 

	
 
        row.cases_ordered = item.cases_ordered
 
        row.units_ordered = item.units_ordered
 
        row.po_case_size = item.po_case_size
 
        row.po_unit_cost = item.po_unit_cost
 
        row.po_total = item.po_total
 

	
 
        # calculated PO total
 
        case_size = row.po_case_size or row.case_quantity or 1
 
        units = self.get_units_ordered(row, case_quantity=case_size)
 
        row.po_total_calculated = units * (row.po_unit_cost or 0)
 

	
 
        return row
 

	
 
    def make_row_from_po_item(self, batch, item):
 
        """
 
        Create a new batch row, from the given ``po_item`` object,
 
        which is assumed to be a purchase order line item of some
 
        sort.  Default implementation specifically assumes it is a
 
        `PurchaseItem` instance, but subclasses may assume something
rattail/db/alembic/versions/6f0659414074_add_purchasebatch_order_file.py
Show inline comments
 
new file 100644
 
# -*- coding: utf-8; -*-
 
"""add PurchaseBatch.order_file
 

	
 
Revision ID: 6f0659414074
 
Revises: b9d58e803208
 
Create Date: 2024-10-21 14:53:02.779348
 

	
 
"""
 

	
 
# revision identifiers, used by Alembic.
 
revision = '6f0659414074'
 
down_revision = 'b9d58e803208'
 
branch_labels = None
 
depends_on = None
 

	
 
from alembic import op
 
import sqlalchemy as sa
 
import rattail.db.types
 

	
 

	
 

	
 
def upgrade():
 

	
 
    # batch_purchase
 
    op.add_column('batch_purchase', sa.Column('order_file', sa.String(length=255), nullable=True))
 
    op.add_column('batch_purchase', sa.Column('order_parser_key', sa.String(length=100), nullable=True))
 

	
 

	
 
def downgrade():
 

	
 
    # batch_purchase
 
    op.drop_column('batch_purchase', 'order_parser_key')
 
    op.drop_column('batch_purchase', 'order_file')
rattail/db/model/batch/purchase.py
Show inline comments
 
@@ -85,12 +85,21 @@ class PurchaseBatch(BatchMixin, PurchaseBase, Base):
 
            """))
 

	
 
    mode = sa.Column(sa.Integer(), nullable=False, doc="""
 
    Numeric "mode" for the purchase batch, to indicate new/receiving etc.
 
    """)
 

	
 
    order_file = filename_column(nullable=True, doc="""
 
    Base name for the associated order file, if any.
 
    """)
 

	
 
    order_parser_key = sa.Column(sa.String(length=100), nullable=True, doc="""
 
    The key of the parser used to read the contents of the order file,
 
    if applicable.
 
    """)
 

	
 
    invoice_file = filename_column(doc="Base name for the associated invoice file, if any.")
 

	
 
    invoice_parser_key = sa.Column(sa.String(length=100), nullable=True, doc="""
 
    The key of the parser used to read the contents of the invoice file.
 
    """)
 

	
rattail/vendors/handler.py
Show inline comments
 
@@ -159,6 +159,88 @@ class VendorHandler(GenericHandler):
 
        for Parser in self.get_all_catalog_parsers():
 
            if Parser.key == key:
 
                return Parser(self.config)
 

	
 
        if require:
 
            raise CatalogParserNotFound(key)
 

	
 
    def get_all_order_parsers(self):
 
        """
 
        Should return *all* order parsers known to exist.
 

	
 
        Note that this returns classes and not instances.
 

	
 
        See also :meth:`get_supported_order_parsers()` and
 
        :meth:`get_order_parser()`.
 

	
 
        :returns: List of :class:`~rattail.vendors.orders.OrderParser`
 
           classes.
 
        """
 
        Parsers = list(
 
            load_entry_points('rattail.vendors.orders.parsers').values())
 
        Parsers.sort(key=lambda Parser: Parser.title)
 
        return Parsers
 

	
 
    def get_supported_order_parsers(self, vendor=None, **kwargs):
 
        """
 
        Should return a list of order file parsers which are
 
        *supported* and should be exposed to the user.
 

	
 
        See also :meth:`get_all_order_parsers()` and
 
        :meth:`get_order_parser()`.
 

	
 
        :param vendor: Vendor for whom the parser is needed, if known.
 
           If you specify a vendor then parsers which indicate support
 
           for a *different* vendor will be excluded from the list.
 

	
 
        :returns: List of :class:`~rattail.vendors.orders.OrderParser`
 
           classes.
 
        """
 
        all_parsers = self.get_all_order_parsers()
 

	
 
        supported = self.config.get_list('rattail.vendors.supported_order_parsers')
 
        if supported:
 
            parsers = [Parser for Parser in all_parsers
 
                       if Parser.key in supported]
 
        else:
 
            parsers = all_parsers
 

	
 
        result = []
 
        if vendor:
 
            session = self.app.get_session(vendor)
 
            for Parser in parsers:
 
                parser = Parser(self.config)
 
                if parser.vendor_key:
 
                    # parser declares a vendor, so only add if it's a match
 
                    pvendor = parser.get_vendor(session)
 
                    if pvendor and pvendor is vendor:
 
                        result.append(parser)
 
                else: # parser is vendor-neutral; always add
 
                    result.append(parser)
 

	
 
        else: # no vendor specified, so show all parsers
 
            result = parsers
 

	
 
        return result
 

	
 
    def get_order_parser(self, key, require=False):
 
        """
 
        Retrieve the order file parser for the given key.
 

	
 
        Note that this returns an instance, not the class.
 

	
 
        See also :meth:`get_all_order_parsers()` and
 
        :meth:`get_supported_order_parsers()`.
 

	
 
        :param key: Unique key indicating which parser to get.
 

	
 
        :returns: An :class:`~rattail.vendors.orders.OrderParser`
 
           instance.
 
        """
 
        from rattail.vendors.orders import OrderParserNotFound
 

	
 
        for Parser in self.get_all_order_parsers():
 
            if Parser.key == key:
 
                return Parser(self.config)
 

	
 
        if require:
 
            raise OrderParserNotFound(key)
rattail/vendors/orders.py
Show inline comments
 
new file 100644
 
# -*- coding: utf-8; -*-
 
################################################################################
 
#
 
#  Rattail -- Retail Software Framework
 
#  Copyright © 2010-2024 Lance Edgar
 
#
 
#  This file is part of Rattail.
 
#
 
#  Rattail is free software: you can redistribute it and/or modify it under the
 
#  terms of the GNU General Public License as published by the Free Software
 
#  Foundation, either version 3 of the License, or (at your option) any later
 
#  version.
 
#
 
#  Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
 
#  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 
#  FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 
#  details.
 
#
 
#  You should have received a copy of the GNU General Public License along with
 
#  Rattail.  If not, see <http://www.gnu.org/licenses/>.
 
#
 
################################################################################
 
"""
 
Vendor Order Files
 
"""
 

	
 
import decimal
 

	
 
from rattail.exceptions import RattailError
 
from rattail.excel import ExcelReaderXLSX
 

	
 

	
 
class OrderParserNotFound(RattailError):
 
    """
 
    Exception raised when an order file parser is required, but cannot
 
    be located.
 
    """
 

	
 
    def __init__(self, key):
 
        self.key = key
 

	
 
    def __str__(self):
 
        return f"Vendor order parser not found for key: {self.key}"
 

	
 

	
 
class OrderParser:
 
    """
 
    Base class for all vendor order parsers.
 
    """
 
    vendor_key = None
 

	
 
    def __init__(self, config):
 
        self.config = config
 
        self.app = self.config.get_app()
 

	
 
    @property
 
    def key(self):
 
        """
 
        Key for the parser.  Must be unique among all order parsers.
 
        """
 
        raise NotImplementedError(f"Order parser has no key: {repr(self)}")
 

	
 
    @property
 
    def title(self):
 
        """
 
        Human-friendly title for the parser.
 
        """
 
        raise NotImplementedError(f"Order parser has no title: {self.key}")
 

	
 
    def get_vendor(self, session):
 
        """
 
        Fetch the :class:`~rattail.db.model.vendors.Vendor` record
 
        which is associated with the current parser, if any.
 
        """
 
        if self.vendor_key:
 
            return self.app.get_vendor_handler().get_vendor(session, self.vendor_key)
 

	
 
    def parse_order_date(self, path):
 
        """
 
        Parse the order date from the order file.
 
        """
 

	
 
    def parse_order_number(self, path):
 
        """
 
        Parse the order number from the order file.
 
        """
 

	
 
    def parse_order_items(self, path, progress=None):
 
        """
 
        Parse all data items (rows) from the order file.
 
        """
 
        raise NotImplementedError
 

	
 
    def make_order_item(self, **kwargs):
 
        """
 
        Make and return a
 
        :class:`~rattail.db.model.purchase.PurchaseItem` instance.
 
        """
 
        model = self.app.model
 
        return model.PurchaseItem(**kwargs)
 

	
 

	
 
class ExcelOrderParser(OrderParser):
 
    """
 
    Base class for Excel vendor order parsers.
 
    """
 

	
 
    def get_excel_reader(self, path):
 
        """
 
        Return an :class:`~rattail.excel.ExcelReaderXLSX` instance for
 
        the given path.
 
        """
 
        if not hasattr(self, 'excel_reader'):
 
            kwargs = self.get_excel_reader_kwargs()
 
            self.excel_reader = ExcelReaderXLSX(path, **kwargs)
 
        return self.excel_reader
 

	
 
    def get_excel_reader_kwargs(self, **kwargs):
 
        """
 
        Should return kwargs for the Excel reader factory.
 
        """
 
        return kwargs
 

	
 
    def decimal(self, value, scale=2):
 
        """
 
        Convert a value to a decimal.
 
        """
 
        if value is None:
 
            return
 

	
 
        # no reason to convert integers, really
 
        if isinstance(value, (int, decimal.Decimal)):
 
            return value
 

	
 
        # float becomes rounded decimal
 
        if isinstance(value, float):
 
            return decimal.Decimal(f"{{:0.{scale}f}}".format(value))
 

	
 
        # string becomes decimal
 
        value = value.strip()
 
        if value:
 
            return decimal.Decimal(value)
 

	
 

	
 
class DefaultOrderParser(ExcelOrderParser):
 
    """
 
    Default order parser for Excel files.
 

	
 
    .. autoattribute:: key
 

	
 
    .. autoattribute:: title
 
    """
 
    key = 'default'
 
    title = "Default Excel Parser"
 

	
 
    # TODO: needs sane default parser logic
0 comments (0 inline, 0 general)