Changeset - d6f004376d1c
[Not reviewed]
0 4 1
Lance Edgar (lance) - 4 years ago 2021-01-26 20:09:43
lance@edbob.org
Initial support for adding items to, executing customer order batch
5 files changed with 253 insertions and 5 deletions:
0 comments (0 inline, 0 general)
rattail/batch/custorder.py
Show inline comments
 
# -*- coding: utf-8; -*-
 
################################################################################
 
#
 
#  Rattail -- Retail Software Framework
 
#  Copyright © 2010-2020 Lance Edgar
 
#  Copyright © 2010-2021 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
 
@@ -23,18 +23,142 @@
 
"""
 
Handler for "customer order" batches
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import, division
 

	
 
import six
 
from sqlalchemy import orm
 

	
 
from rattail.db import model
 
from rattail.batch import BatchHandler
 

	
 

	
 
class CustomerOrderBatchHandler(BatchHandler):
 
    """
 
    Handler for all "customer order" batches, regardless of "mode".  The
 
    handler must inspect the
 
    :attr:`~rattail.db.model.batch.custorder.CustomerOrderBatch.mode` attribute
 
    of each batch it deals with, in order to determine which logic to apply.
 
    """
 
    batch_model_class = model.CustomerOrderBatch
 

	
 
    def refresh_row(self, row):
 
        if not row.product:
 
            if row.item_entry:
 
                session = orm.object_session(row)
 
                # TODO: should do more than just query for uuid here
 
                product = session.query(model.Product).get(row.item_entry)
 
                if product:
 
                    row.product = product
 
            if not row.product:
 
                row.status_code = row.STATUS_PRODUCT_NOT_FOUND
 
                return
 

	
 
        product = row.product
 
        row.product_upc = product.upc
 
        row.product_brand = six.text_type(product.brand or "")
 
        row.product_description = product.description
 
        row.product_size = product.size
 
        row.product_weighed = product.weighed
 
        row.case_quantity = product.case_size
 

	
 
        department = product.department
 
        row.department_number = department.number if department else None
 
        row.department_name = department.name if department else None
 

	
 
        cost = product.cost
 
        row.product_unit_cost = cost.unit_cost if cost else None
 

	
 
        regprice = product.regular_price
 
        row.unit_price = regprice.price if regprice else None
 

	
 
        # we need to know if total price is updated
 
        old_total = row.total_price
 

	
 
        # maybe update total price
 
        if row.unit_price is None:
 
            row.total_price = None
 
        elif not row.unit_price:
 
            row.total_price = 0
 
        else:
 
            row.total_price = row.unit_price * row.order_quantity
 
            if row.order_uom == self.enum.UNIT_OF_MEASURE_CASE:
 
                row.total_price *= row.case_quantity
 

	
 
        # update total price for batch too, if it changed
 
        if row.total_price != old_total:
 
            batch = row.batch
 
            batch.total_price = ((batch.total_price or 0)
 
                                 + (row.total_price or 0)
 
                                 - (old_total or 0))
 

	
 
        row.status_code = row.STATUS_OK
 

	
 
    def remove_row(self, row):
 
        batch = row.batch
 

	
 
        if not row.removed:
 
            row.removed = True
 

	
 
            if row.total_price:
 
                batch.total_price = (batch.total_price or 0) - row.total_price
 

	
 
        self.refresh_batch_status(batch)
 

	
 
    def execute(self, batch, user=None, progress=None, **kwargs):
 
        """
 
        Default behavior here will simply create a new (proper) Customer Order
 
        based on the batch contents.  Override as needed.
 
        """
 
        batch_fields = [
 
            'store',
 
            'id',
 
            'customer',
 
            'person',
 
            'phone_number',
 
            'email_address',
 
            'total_price',
 
        ]
 

	
 
        order = model.CustomerOrder()
 
        order.created_by = user
 
        order.status_code = self.enum.CUSTORDER_STATUS_ORDERED
 
        for field in batch_fields:
 
            setattr(order, field, getattr(batch, field))
 

	
 
        row_fields = [
 
            'product',
 
            'product_upc',
 
            'product_brand',
 
            'product_description',
 
            'product_size',
 
            'product_weighed',
 
            'department_number',
 
            'department_name',
 
            'case_quantity',
 
            'order_quantity',
 
            'order_uom',
 
            'product_unit_cost',
 
            'unit_price',
 
            'discount_percent',
 
            'total_price',
 
            'paid_amount',
 
            'payment_transaction_number',
 
        ]
 

	
 
        def convert(row, i):
 
            item = model.CustomerOrderItem()
 
            item.sequence = i + 1
 
            item.status_code = self.enum.CUSTORDER_ITEM_STATUS_ORDERED
 
            for field in row_fields:
 
                setattr(item, field, getattr(row, field))
 
            order.items.append(item)
 

	
 
        self.progress_loop(convert, batch.active_rows(), progress,
 
                           message="Converting batch rows to order items")
 

	
 
        session = orm.object_session(batch)
 
        session.add(order)
 
        session.flush()
 

	
 
        return order
rattail/batch/handlers.py
Show inline comments
 
@@ -175,13 +175,13 @@ class BatchHandler(object):
 
        """
 
        session = orm.object_session(batch)
 
        with session.no_autoflush:
 
            batch.data_rows.append(row)
 
            self.refresh_row(row)
 
        if not row.removed:
 
            batch.rowcount += 1
 
            batch.rowcount = (batch.rowcount or 0) + 1
 
            self.after_add_row(batch, row)
 

	
 
    def after_add_row(self, batch, row):
 
        """
 
        Event hook, called immediately after the given row has been "properly"
 
        added to the batch.  This is a good place to update totals for the
rattail/db/alembic/versions/2afee42cc24d_add_custorder_batch_row_product_upc.py
Show inline comments
 
new file 100644
 
# -*- coding: utf-8; -*-
 
"""add custorder_batch_row.product_upc
 

	
 
Revision ID: 2afee42cc24d
 
Revises: a7b286e87f89
 
Create Date: 2021-01-25 21:32:00.008209
 

	
 
"""
 

	
 
from __future__ import unicode_literals
 

	
 
# revision identifiers, used by Alembic.
 
revision = '2afee42cc24d'
 
down_revision = 'a7b286e87f89'
 
branch_labels = None
 
depends_on = None
 

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

	
 

	
 

	
 
def upgrade():
 

	
 
    # custorder
 
    op.add_column('custorder', sa.Column('total_price', sa.Numeric(precision=10, scale=3), nullable=True))
 

	
 
    # custorder_item
 
    op.alter_column('custorder_item', 'product_unit_of_measure',
 
               existing_type=sa.VARCHAR(length=4),
 
               nullable=True)
 
    op.add_column('custorder_item', sa.Column('product_upc', rattail.db.types.GPCType(), nullable=True))
 
    op.add_column('custorder_item', sa.Column('product_weighed', sa.Boolean(), nullable=True))
 
    op.add_column('custorder_item', sa.Column('order_quantity', sa.Numeric(precision=10, scale=4), nullable=True))
 
    op.add_column('custorder_item', sa.Column('order_uom', sa.String(length=4), nullable=True))
 

	
 
    # custorder_batch
 
    op.add_column('custorder_batch', sa.Column('total_price', sa.Numeric(precision=10, scale=3), nullable=True))
 

	
 
    # custorder_batch_row
 
    op.alter_column('custorder_batch_row', 'product_unit_of_measure',
 
               existing_type=sa.VARCHAR(length=4),
 
               nullable=True)
 
    op.add_column('custorder_batch_row', sa.Column('product_upc', rattail.db.types.GPCType(), nullable=True))
 
    op.add_column('custorder_batch_row', sa.Column('product_weighed', sa.Boolean(), nullable=True))
 
    op.add_column('custorder_batch_row', sa.Column('order_quantity', sa.Numeric(precision=10, scale=4), nullable=True))
 
    op.add_column('custorder_batch_row', sa.Column('order_uom', sa.String(length=4), nullable=True))
 

	
 

	
 
def downgrade():
 

	
 
    # custorder_batch_row
 
    op.drop_column('custorder_batch_row', 'order_uom')
 
    op.drop_column('custorder_batch_row', 'order_quantity')
 
    op.drop_column('custorder_batch_row', 'product_weighed')
 
    op.drop_column('custorder_batch_row', 'product_upc')
 
    # TODO: this is really a one-way change, cannot be easily undone
 
    # op.alter_column('custorder_batch_row', 'product_unit_of_measure',
 
    #            existing_type=sa.VARCHAR(length=4),
 
    #            nullable=False)
 

	
 
    # custorder_batch
 
    op.drop_column('custorder_batch', 'total_price')
 

	
 
    # custorder_item
 
    op.drop_column('custorder_item', 'order_uom')
 
    op.drop_column('custorder_item', 'order_quantity')
 
    op.drop_column('custorder_item', 'product_weighed')
 
    op.drop_column('custorder_item', 'product_upc')
 
    # TODO: this is really a one-way change, cannot be easily undone
 
    # op.alter_column('custorder_item', 'product_unit_of_measure',
 
    #            existing_type=sa.VARCHAR(length=4),
 
    #            nullable=False)
 

	
 
    # custorder
 
    op.drop_column('custorder', 'total_price')
rattail/db/model/custorders.py
Show inline comments
 
# -*- coding: utf-8; -*-
 
################################################################################
 
#
 
#  Rattail -- Retail Software Framework
 
#  Copyright © 2010-2020 Lance Edgar
 
#  Copyright © 2010-2021 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
 
@@ -33,12 +33,13 @@ import sqlalchemy as sa
 
from sqlalchemy import orm
 
from sqlalchemy.ext.orderinglist import ordering_list
 
from sqlalchemy.ext.declarative import declared_attr
 

	
 
from rattail.db.model import Base, uuid_column
 
from rattail.db.model import Store, Customer, Person, Product, User
 
from rattail.db.types import GPCType
 

	
 

	
 
class CustomerOrderBase(object):
 
    """
 
    Base class for customer orders; defines common fields.
 
    """
 
@@ -94,12 +95,16 @@ class CustomerOrderBase(object):
 
    """)
 

	
 
    email_address = sa.Column(sa.String(length=255), nullable=True, doc="""
 
    Customer contact email address for sake of this order.
 
    """)
 

	
 
    total_price = sa.Column(sa.Numeric(precision=10, scale=3), nullable=True, doc="""
 
    Full price (not including tax etc.) for all items on the order.
 
    """)
 

	
 

	
 
@six.python_2_unicode_compatible
 
class CustomerOrder(CustomerOrderBase, Base):
 
    """
 
    Represents an order placed by the customer.
 
    """
 
@@ -167,12 +172,16 @@ class CustomerOrderItemBase(object):
 
        return orm.relationship(
 
            Product,
 
            doc="""
 
            Reference to the master product record for the line item.
 
            """)
 

	
 
    product_upc = sa.Column(GPCType(), nullable=True, doc="""
 
    UPC for the product associated with the row.
 
    """)
 

	
 
    product_brand = sa.Column(sa.String(length=100), nullable=True, doc="""
 
    Brand name for the product being ordered.  This should be a cache of the
 
    relevant :attr:`Brand.name`.
 
    """)
 

	
 
    product_description = sa.Column(sa.String(length=60), nullable=True, doc="""
 
@@ -182,13 +191,20 @@ class CustomerOrderItemBase(object):
 

	
 
    product_size = sa.Column(sa.String(length=30), nullable=True, doc="""
 
    Size of the product being ordered.  This should be a cache of
 
    :attr:`Product.size`.
 
    """)
 

	
 
    product_unit_of_measure = sa.Column(sa.String(length=4), nullable=False, doc="""
 
    product_weighed = sa.Column(sa.String(length=4), nullable=True, doc="""
 
    Flag indicating whether the product is sold by weight.  This should be a
 
    cache of :attr:`Product.weighed`.
 
    """)
 

	
 
    # TODO: probably should get rid of this, i can't think of why it's needed.
 
    # for now we just make sure it is nullable, since that wasn't the case.
 
    product_unit_of_measure = sa.Column(sa.String(length=4), nullable=True, doc="""
 
    Code indicating the unit of measure for the product.  This should be a
 
    cache of :attr:`Product.unit_of_measure`.
 
    """)
 

	
 
    department_number = sa.Column(sa.Integer(), nullable=True, doc="""
 
    Number of the department to which the product belongs.
 
@@ -197,23 +213,36 @@ class CustomerOrderItemBase(object):
 
    department_name = sa.Column(sa.String(length=30), nullable=True, doc="""
 
    Name of the department to which the product belongs.
 
    """)
 

	
 
    case_quantity = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
 
    Case pack count for the product being ordered.  This should be a cache of
 
    :attr:`Product.case_pack`.
 
    :attr:`Product.case_size`.
 
    """)
 

	
 
    # TODO: i now think that cases_ordered and units_ordered should go away.
 
    # but will wait until that idea has proven itself before removing.  am
 
    # pretty sure they are obviated by order_quantity and order_uom.
 

	
 
    cases_ordered = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
 
    Number of cases of product which were initially ordered.
 
    """)
 

	
 
    units_ordered = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
 
    Number of units of product which were initially ordered.
 
    """)
 

	
 
    order_quantity = sa.Column(sa.Numeric(precision=10, scale=4), nullable=True, doc="""
 
    Quantity being ordered by the customer.
 
    """)
 

	
 
    order_uom = sa.Column(sa.String(length=4), nullable=True, doc="""
 
    Code indicating the unit of measure for the order itself.  Does not
 
    directly reflect the :attr:`~rattail.db.model.Product.unit_of_measure`.
 
    """)
 

	
 
    product_unit_cost = sa.Column(sa.Numeric(precision=9, scale=5), nullable=True, doc="""
 
    Unit cost of the product being ordered.  This should be a cache of the
 
    relevant :attr:`rattail.db.model.ProductCost.unit_cost`.
 
    """)
 

	
 
    unit_price = sa.Column(sa.Numeric(precision=8, scale=3), nullable=True, doc="""
rattail/enum.py
Show inline comments
 
@@ -78,12 +78,30 @@ CUSTORDER_BATCH_MODE_GATHERING           = 20
 
CUSTORDER_BATCH_MODE = {
 
    CUSTORDER_BATCH_MODE_CREATING        : "creating",
 
    CUSTORDER_BATCH_MODE_GATHERING       : "gathering",
 
}
 

	
 

	
 
CUSTORDER_STATUS_ORDERED                = 10
 
# CUSTORDER_STATUS_PAID                   = 20
 

	
 
CUSTORDER_STATUS = {
 
    CUSTORDER_STATUS_ORDERED            : "ordered",
 
    # CUSTORDER_STATUS_PAID               : "paid",
 
}
 

	
 

	
 
CUSTORDER_ITEM_STATUS_ORDERED           = 10
 
# CUSTORDER_ITEM_STATUS_PAID              = 20
 

	
 
CUSTORDER_ITEM_STATUS = {
 
    CUSTORDER_ITEM_STATUS_ORDERED       : "ordered",
 
    # CUSTORDER_ITEM_STATUS_PAID          : "paid",
 
}
 

	
 

	
 
EMAIL_ATTEMPT_CREATED           = 0
 
EMAIL_ATTEMPT_SUCCESS           = 1
 
EMAIL_ATTEMPT_FAILURE           = 2
 
# EMAIL_ATTEMPT_BOUNCED           = 3
 

	
 
EMAIL_ATTEMPT = {
0 comments (0 inline, 0 general)