diff --git a/rattail/batch/custorder.py b/rattail/batch/custorder.py index cca4267059d431ecd2e6810e94de3f56801b25e3..4761756f5efe9e32d6ec637efa142d3731738340 100644 --- a/rattail/batch/custorder.py +++ b/rattail/batch/custorder.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2020 Lance Edgar +# Copyright © 2010-2021 Lance Edgar # # This file is part of Rattail. # @@ -26,6 +26,9 @@ 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 @@ -38,3 +41,124 @@ class CustomerOrderBatchHandler(BatchHandler): 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 diff --git a/rattail/batch/handlers.py b/rattail/batch/handlers.py index 3398c64dcf957e3a14df87e1ee5008b077d2c920..91c0f6e7c1c819f5e19151639c02fef907e5653b 100644 --- a/rattail/batch/handlers.py +++ b/rattail/batch/handlers.py @@ -178,7 +178,7 @@ class BatchHandler(object): 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): diff --git a/rattail/db/alembic/versions/2afee42cc24d_add_custorder_batch_row_product_upc.py b/rattail/db/alembic/versions/2afee42cc24d_add_custorder_batch_row_product_upc.py new file mode 100644 index 0000000000000000000000000000000000000000..d0253538b97daacc73fb3ef9fa8fa90d8cfc4b88 --- /dev/null +++ b/rattail/db/alembic/versions/2afee42cc24d_add_custorder_batch_row_product_upc.py @@ -0,0 +1,77 @@ +# -*- 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') diff --git a/rattail/db/model/custorders.py b/rattail/db/model/custorders.py index 0bf12d71fc508741de505dcc64979ad91323aaf1..9eb7a581987c5eb74105303da3daf872002be2b7 100644 --- a/rattail/db/model/custorders.py +++ b/rattail/db/model/custorders.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2020 Lance Edgar +# Copyright © 2010-2021 Lance Edgar # # This file is part of Rattail. # @@ -36,6 +36,7 @@ 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): @@ -97,6 +98,10 @@ class CustomerOrderBase(object): 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): @@ -170,6 +175,10 @@ class CustomerOrderItemBase(object): 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`. @@ -185,7 +194,14 @@ class CustomerOrderItemBase(object): :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`. """) @@ -200,9 +216,13 @@ class CustomerOrderItemBase(object): 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. """) @@ -211,6 +231,15 @@ class CustomerOrderItemBase(object): 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`. diff --git a/rattail/enum.py b/rattail/enum.py index 6719f7214bfad6abece5b508e649160b4b1d143f..f682f88dca7e9fe0e050c624440726ac28bb5257 100644 --- a/rattail/enum.py +++ b/rattail/enum.py @@ -81,6 +81,24 @@ CUSTORDER_BATCH_MODE = { } +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