diff --git a/docs/glossary.rst b/docs/glossary.rst index f4cf836..d639262 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -5,46 +5,6 @@ Glossary .. glossary:: :sorted: - external customer - A customer account from an external system. Sideshow can be - configured to lookup customer data from external system(s) when - creating an :term:`order`. - - See also :term:`local customer` and :term:`pending customer`. - - external product - A product record from an external system. Sideshow can be - configured to lookup customer data from external system(s) when - creating an :term:`order`. - - See also :term:`local product` and :term:`pending product`. - - local customer - A customer account in the :term:`app database`. By default, - Sideshow will use its native "Local Customers" table for lookup - when creating an :term:`order`. - - The data model for this is - :class:`~sideshow.db.model.customers.LocalCustomer`. - - See also :term:`external customer` and :term:`pending customer`. - - local product - A product record in the :term:`app database`. By default, - Sideshow will use its native "Local Products" table for lookup - when creating an :term:`order`. - - The data model for this is - :class:`~sideshow.db.model.products.LocalProduct`. - - See also :term:`external product` and :term:`pending product`. - - new order batch - When user is creating a new order, under the hood a :term:`batch` - is employed to keep track of user input. When user ultimately - "submits" the order, the batch is executed which creates a true - :term:`order`. - order This is the central focus of the app; it refers to a customer case/special order which is tracked over time, from placement to @@ -60,19 +20,17 @@ Glossary sibling items. pending customer - A "temporary" customer record used when creating an :term:`order` - for new/unknown customer. + Generally refers to a "new / unknown" customer, e.g. for whom a + new order is being created. This allows the order lifecycle to + get going before the customer has a proper account in the system. - The data model for this is - :class:`~sideshow.db.model.customers.PendingCustomer`. - - See also :term:`external customer` and :term:`pending customer`. + See :class:`~sideshow.db.model.customers.PendingCustomer` for the + data model. pending product - A "temporary" product record used when creating an :term:`order` - for new/unknown product. + Generally refers to a "new / unknown" product, e.g. for which a + new order is being created. This allows the order lifecycle to + get going before the product has a true record in the system. - The data model for this is - :class:`~sideshow.db.model.products.PendingProduct`. - - See also :term:`external product` and :term:`pending product`. + See :class:`~sideshow.db.model.products.PendingProduct` for the + data model. diff --git a/src/sideshow/batch/neworder.py b/src/sideshow/batch/neworder.py index 353af9c..5ec9b8d 100644 --- a/src/sideshow/batch/neworder.py +++ b/src/sideshow/batch/neworder.py @@ -27,8 +27,6 @@ New Order Batch Handler import datetime import decimal -import sqlalchemy as sa - from wuttjamaican.batch import BatchHandler from sideshow.db.model import NewOrderBatch @@ -36,8 +34,7 @@ from sideshow.db.model import NewOrderBatch class NewOrderBatchHandler(BatchHandler): """ - The :term:`batch handler` for :term:`new order batches <new order - batch>`. + The :term:`batch handler` for New Order Batches. This is responsible for business logic around the creation of new :term:`orders <order>`. A @@ -47,333 +44,188 @@ class NewOrderBatchHandler(BatchHandler): """ model_class = NewOrderBatch - def use_local_customers(self): + def set_pending_customer(self, batch, data): """ - Returns boolean indicating whether :term:`local customer` - accounts should be used. This is true by default, but may be - false for :term:`external customer` lookups. - """ - return self.config.get_bool('sideshow.orders.use_local_customers', - default=True) + Set (add or update) pending customer info for the batch. - def use_local_products(self): - """ - Returns boolean indicating whether :term:`local product` - records should be used. This is true by default, but may be - false for :term:`external product` lookups. - """ - return self.config.get_bool('sideshow.orders.use_local_products', - default=True) - - def allow_unknown_products(self): - """ - Returns boolean indicating whether :term:`pending products - <pending product>` are allowed when creating an order. - - This is true by default, so user can enter new/unknown product - when creating an order. This can be disabled, to force user - to choose existing local/external product. - """ - return self.config.get_bool('sideshow.orders.allow_unknown_products', - default=True) - - def set_customer(self, batch, customer_info, user=None): - """ - Set/update customer info for the batch. - - This will first set one of the following: - - * :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.customer_id` - * :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.local_customer` - * :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer` - - Note that a new - :class:`~sideshow.db.model.customers.PendingCustomer` record - is created if necessary. - - And then it will update these accordingly: - - * :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.customer_name` - * :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.phone_number` - * :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.email_address` - - Note that ``customer_info`` may be ``None``, which will cause - all the above to be set to ``None`` also. + This will clear the + :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.customer_id` + and set the + :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer`, + creating a new record if needed. It then updates the pending + customer record per the given ``data``. :param batch: - :class:`~sideshow.db.model.batch.neworder.NewOrderBatch` to - update. + :class:`~sideshow.db.model.batch.neworder.NewOrderBatch` + to be updated. - :param customer_info: Customer ID string, or dict of - :class:`~sideshow.db.model.customers.PendingCustomer` data, - or ``None`` to clear the customer info. - - :param user: - :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who - is performing the action. This is used to set - :attr:`~sideshow.db.model.customers.PendingCustomer.created_by` - on the pending customer, if applicable. If not specified, - the batch creator is assumed. + :param data: Dict of field data for the + :class:`~sideshow.db.model.customers.PendingCustomer` + record. """ model = self.app.model enum = self.app.enum - session = self.app.get_session(batch) - use_local = self.use_local_customers() - # set customer info - if isinstance(customer_info, str): - if use_local: + # remove customer account if set + batch.customer_id = None - # local_customer - customer = session.get(model.LocalCustomer, customer_info) - if not customer: - raise ValueError("local customer not found") - batch.local_customer = customer - batch.customer_name = customer.full_name - batch.phone_number = customer.phone_number - batch.email_address = customer.email_address + # create pending customer if needed + pending = batch.pending_customer + if not pending: + kw = dict(data) + kw.setdefault('status', enum.PendingCustomerStatus.PENDING) + pending = model.PendingCustomer(**kw) + batch.pending_customer = pending - else: # external customer_id - #batch.customer_id = customer_info - raise NotImplementedError + # update pending customer + if 'first_name' in data: + pending.first_name = data['first_name'] + if 'last_name' in data: + pending.last_name = data['last_name'] + if 'full_name' in data: + pending.full_name = data['full_name'] + elif 'first_name' in data or 'last_name' in data: + pending.full_name = self.app.make_full_name(data.get('first_name'), + data.get('last_name')) + if 'phone_number' in data: + pending.phone_number = data['phone_number'] + if 'email_address' in data: + pending.email_address = data['email_address'] - elif customer_info: + # update batch per pending customer + batch.customer_name = pending.full_name + batch.phone_number = pending.phone_number + batch.email_address = pending.email_address - # pending_customer - batch.customer_id = None - batch.local_customer = None - customer = batch.pending_customer - if not customer: - customer = model.PendingCustomer(status=enum.PendingCustomerStatus.PENDING, - created_by=user or batch.created_by) - session.add(customer) - batch.pending_customer = customer - fields = [ - 'full_name', - 'first_name', - 'last_name', - 'phone_number', - 'email_address', - ] - for key in fields: - setattr(customer, key, customer_info.get(key)) - if 'full_name' not in customer_info: - customer.full_name = self.app.make_full_name(customer.first_name, - customer.last_name) - batch.customer_name = customer.full_name - batch.phone_number = customer.phone_number - batch.email_address = customer.email_address - - else: - - # null - batch.customer_id = None - batch.local_customer = None - batch.customer_name = None - batch.phone_number = None - batch.email_address = None - - session.flush() - - def add_item(self, batch, product_info, order_qty, order_uom, user=None): + def add_pending_product(self, batch, pending_info, + order_qty, order_uom): """ - Add a new item/row to the batch, for given product and quantity. + Add a new row to the batch, for the given "pending" product + and order quantity. - See also :meth:`update_item()`. + See also :meth:`set_pending_product()` to update an existing row. :param batch: :class:`~sideshow.db.model.batch.neworder.NewOrderBatch` to - update. + which the row should be added. - :param product_info: Product ID string, or dict of - :class:`~sideshow.db.model.products.PendingProduct` data. + :param pending_info: Dict of kwargs to use when constructing a + new :class:`~sideshow.db.model.products.PendingProduct`. - :param order_qty: - :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_qty` - value for the new row. + :param order_qty: Quantity of the product to be added to the + order. - :param order_uom: - :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_uom` - value for the new row. - - :param user: - :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who - is performing the action. This is used to set - :attr:`~sideshow.db.model.products.PendingProduct.created_by` - on the pending product, if applicable. If not specified, - the batch creator is assumed. + :param order_uom: UOM for the order quantity; must be a code + from :data:`~sideshow.enum.ORDER_UOM`. :returns: :class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow` - instance. + which was added to the batch. """ model = self.app.model enum = self.app.enum session = self.app.get_session(batch) - use_local = self.use_local_products() - row = self.make_row() - # set product info - if isinstance(product_info, str): - if use_local: + # make new pending product + kw = dict(pending_info) + kw.setdefault('status', enum.PendingProductStatus.PENDING) + product = model.PendingProduct(**kw) + session.add(product) + session.flush() + # nb. this may convert float to decimal etc. + session.refresh(product) - # local_product - local = session.get(model.LocalProduct, product_info) - if not local: - raise ValueError("local product not found") - row.local_product = local - - else: # external product_id - #row.product_id = product_info - raise NotImplementedError - - else: - # pending_product - if not self.allow_unknown_products(): - raise TypeError("unknown/pending product not allowed for new orders") - row.product_id = None - row.local_product = None - pending = model.PendingProduct(status=enum.PendingProductStatus.PENDING, - created_by=user or batch.created_by) - fields = [ - 'scancode', - 'brand_name', - 'description', - 'size', - 'weighed', - 'department_id', - 'department_name', - 'special_order', - 'vendor_name', - 'vendor_item_code', - 'case_size', - 'unit_cost', - 'unit_price_reg', - 'notes', - ] - for key in fields: - setattr(pending, key, product_info.get(key)) - - # nb. this may convert float to decimal etc. - session.add(pending) - session.flush() - session.refresh(pending) - row.pending_product = pending - - # set order info - row.order_qty = order_qty - row.order_uom = order_uom - - # add row to batch + # make/add new row, w/ pending product + row = self.make_row(pending_product=product, + order_qty=order_qty, order_uom=order_uom) self.add_row(batch, row) + session.add(row) session.flush() return row - def update_item(self, row, product_info, order_qty, order_uom, user=None): + def set_pending_product(self, row, data): """ - Update an item/row, per given product and quantity. + Set (add or update) pending product info for the given batch row. - See also :meth:`add_item()`. + This will clear the + :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.product_id` + and set the + :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.pending_product`, + creating a new record if needed. It then updates the pending + product record per the given ``data``, and finally calls + :meth:`refresh_row()`. + + Note that this does not update order quantity for the item. + + See also :meth:`add_pending_product()` to add a new row + instead of updating. :param row: :class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow` - to update. + to be updated. - :param product_info: Product ID string, or dict of - :class:`~sideshow.db.model.products.PendingProduct` data. - - :param order_qty: New - :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_qty` - value for the row. - - :param order_uom: New - :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_uom` - value for the row. - - :param user: - :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who - is performing the action. This is used to set - :attr:`~sideshow.db.model.products.PendingProduct.created_by` - on the pending product, if applicable. If not specified, - the batch creator is assumed. + :param data: Dict of field data for the + :class:`~sideshow.db.model.products.PendingProduct` record. """ model = self.app.model enum = self.app.enum session = self.app.get_session(row) - use_local = self.use_local_products() - # set product info - if isinstance(product_info, str): - if use_local: + # values for these fields can be used as-is + simple_fields = [ + 'scancode', + 'brand_name', + 'description', + 'size', + 'weighed', + 'department_id', + 'department_name', + 'special_order', + 'vendor_name', + 'vendor_item_code', + 'notes', + 'unit_cost', + 'case_size', + 'case_cost', + 'unit_price_reg', + ] - # local_product - local = session.get(model.LocalProduct, product_info) - if not local: - raise ValueError("local product not found") - row.local_product = local + # clear true product id + row.product_id = None - else: # external product_id - #row.product_id = product_info - raise NotImplementedError - - else: - # pending_product - if not self.allow_unknown_products(): - raise TypeError("unknown/pending product not allowed for new orders") - row.product_id = None - row.local_product = None - pending = row.pending_product - if not pending: - pending = model.PendingProduct(status=enum.PendingProductStatus.PENDING, - created_by=user or row.batch.created_by) - session.add(pending) - row.pending_product = pending - fields = [ - 'scancode', - 'brand_name', - 'description', - 'size', - 'weighed', - 'department_id', - 'department_name', - 'special_order', - 'vendor_name', - 'vendor_item_code', - 'case_size', - 'unit_cost', - 'unit_price_reg', - 'notes', - ] - for key in fields: - setattr(pending, key, product_info.get(key)) - - # nb. this may convert float to decimal etc. + # make pending product if needed + product = row.pending_product + if not product: + kw = dict(data) + kw.setdefault('status', enum.PendingProductStatus.PENDING) + product = model.PendingProduct(**kw) + session.add(product) + row.pending_product = product session.flush() - session.refresh(pending) - # set order info - row.order_qty = order_qty - row.order_uom = order_uom + # update pending product + for field in simple_fields: + if field in data: + setattr(product, field, data[field]) # nb. this may convert float to decimal etc. session.flush() - session.refresh(row) + session.refresh(product) # refresh per new info self.refresh_row(row) - def refresh_row(self, row): + def refresh_row(self, row, now=None): """ - Refresh data for the row. This is called when adding a new - row to the batch, or anytime the row is updated (e.g. when + Refresh all data for the row. This is called when adding a + new row to the batch, or anytime the row is updated (e.g. when changing order quantity). This calls one of the following to update product-related - attributes: + attributes for the row: - * :meth:`refresh_row_from_external_product()` - * :meth:`refresh_row_from_local_product()` * :meth:`refresh_row_from_pending_product()` + * :meth:`refresh_row_from_true_product()` It then re-calculates the row's :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.total_price` @@ -387,7 +239,7 @@ class NewOrderBatchHandler(BatchHandler): row.status_text = None # ensure product - if not row.product_id and not row.local_product and not row.pending_product: + if not row.product_id and not row.pending_product: row.status_code = row.STATUS_MISSING_PRODUCT return @@ -398,9 +250,7 @@ class NewOrderBatchHandler(BatchHandler): # update product attrs on row if row.product_id: - self.refresh_row_from_external_product(row) - elif row.local_product: - self.refresh_row_from_local_product(row) + self.refresh_row_from_true_product(row) else: self.refresh_row_from_pending_product(row) @@ -412,7 +262,7 @@ class NewOrderBatchHandler(BatchHandler): row.case_price_quoted = None if row.unit_price_sale is not None and ( not row.sale_ends - or row.sale_ends > datetime.datetime.now()): + or row.sale_ends > (now or datetime.datetime.now())): row.unit_price_quoted = row.unit_price_sale else: row.unit_price_quoted = row.unit_price_reg @@ -440,27 +290,6 @@ class NewOrderBatchHandler(BatchHandler): # all ok row.status_code = row.STATUS_OK - def refresh_row_from_local_product(self, row): - """ - Update product-related attributes on the row, from its - :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.local_product` - record. - - This is called automatically from :meth:`refresh_row()`. - """ - product = row.local_product - row.product_scancode = product.scancode - row.product_brand = product.brand_name - row.product_description = product.description - row.product_size = product.size - row.product_weighed = product.weighed - row.department_id = product.department_id - row.department_name = product.department_name - row.special_order = product.special_order - row.case_size = product.case_size - row.unit_cost = product.unit_cost - row.unit_price_reg = product.unit_price_reg - def refresh_row_from_pending_product(self, row): """ Update product-related attributes on the row, from its @@ -470,6 +299,7 @@ class NewOrderBatchHandler(BatchHandler): This is called automatically from :meth:`refresh_row()`. """ product = row.pending_product + row.product_scancode = product.scancode row.product_brand = product.brand_name row.product_description = product.description @@ -482,10 +312,10 @@ class NewOrderBatchHandler(BatchHandler): row.unit_cost = product.unit_cost row.unit_price_reg = product.unit_price_reg - def refresh_row_from_external_product(self, row): + def refresh_row_from_true_product(self, row): """ - Update product-related attributes on the row, from its - :term:`external product` record indicated by + Update product-related attributes on the row, from its "true" + product record indicated by :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.product_id`. This is called automatically from :meth:`refresh_row()`. @@ -510,39 +340,31 @@ class NewOrderBatchHandler(BatchHandler): def do_delete(self, batch, user, **kwargs): """ - Delete a batch completely. + Delete the given batch entirely. - If the batch has :term:`pending customer` or :term:`pending - product` records, they are also deleted - unless still - referenced by some order(s). + If the batch has a + :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer` + record, that is deleted also. """ - session = self.app.get_session(batch) - - # maybe delete pending customer - customer = batch.pending_customer - if customer and not customer.orders: - session.delete(customer) - - # maybe delete pending products - for row in batch.rows: - product = row.pending_product - if product and not product.order_items: - session.delete(product) + # maybe delete pending customer record, if it only exists for + # sake of this batch + if batch.pending_customer: + if len(batch.pending_customer.new_order_batches) == 1: + # TODO: check for past orders too + session = self.app.get_session(batch) + session.delete(batch.pending_customer) # continue with normal deletion super().do_delete(batch, user, **kwargs) def why_not_execute(self, batch, **kwargs): """ - By default this checks to ensure the batch has a customer with - phone number, and at least one item. + By default this checks to ensure the batch has a customer and + at least one item. """ - if not batch.customer_name: + if not batch.customer_id and not batch.pending_customer: return "Must assign the customer" - if not batch.phone_number: - return "Customer phone number is required" - rows = self.get_effective_rows(batch) if not rows: return "Must add at least one valid item" @@ -559,113 +381,17 @@ class NewOrderBatchHandler(BatchHandler): def execute(self, batch, user=None, progress=None, **kwargs): """ - Execute the batch; this should make a proper :term:`order`. - - By default, this will call: - - * :meth:`make_local_customer()` - * :meth:`make_local_products()` - * :meth:`make_new_order()` - - And will return the new - :class:`~sideshow.db.model.orders.Order` instance. + By default, this will call :meth:`make_new_order()` and return + the new :class:`~sideshow.db.model.orders.Order` instance. Note that callers should use :meth:`~wuttjamaican:wuttjamaican.batch.BatchHandler.do_execute()` instead, which calls this method automatically. """ rows = self.get_effective_rows(batch) - self.make_local_customer(batch) - self.make_local_products(batch, rows) order = self.make_new_order(batch, rows, user=user, progress=progress, **kwargs) return order - def make_local_customer(self, batch): - """ - If applicable, this converts the batch :term:`pending - customer` into a :term:`local customer`. - - This is called automatically from :meth:`execute()`. - - This logic will happen only if :meth:`use_local_customers()` - returns true, and the batch has pending instead of local - customer (so far). - - It will create a new - :class:`~sideshow.db.model.customers.LocalCustomer` record and - populate it from the batch - :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer`. - The latter is then deleted. - """ - if not self.use_local_customers(): - return - - # nothing to do if no pending customer - pending = batch.pending_customer - if not pending: - return - - session = self.app.get_session(batch) - - # maybe convert pending to local customer - if not batch.local_customer: - model = self.app.model - inspector = sa.inspect(model.LocalCustomer) - local = model.LocalCustomer() - for prop in inspector.column_attrs: - if hasattr(pending, prop.key): - setattr(local, prop.key, getattr(pending, prop.key)) - session.add(local) - batch.local_customer = local - - # remove pending customer - batch.pending_customer = None - session.delete(pending) - session.flush() - - def make_local_products(self, batch, rows): - """ - If applicable, this converts all :term:`pending products - <pending product>` into :term:`local products <local - product>`. - - This is called automatically from :meth:`execute()`. - - This logic will happen only if :meth:`use_local_products()` - returns true, and the batch has pending instead of local items - (so far). - - For each affected row, it will create a new - :class:`~sideshow.db.model.products.LocalProduct` record and - populate it from the row - :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.pending_product`. - The latter is then deleted. - """ - if not self.use_local_products(): - return - - model = self.app.model - session = self.app.get_session(batch) - inspector = sa.inspect(model.LocalProduct) - for row in rows: - - if row.local_product or not row.pending_product: - continue - - pending = row.pending_product - local = model.LocalProduct() - - for prop in inspector.column_attrs: - if hasattr(pending, prop.key): - setattr(local, prop.key, getattr(pending, prop.key)) - session.add(local) - - row.local_product = local - row.pending_product = None - session.delete(pending) - - session.flush() - def make_new_order(self, batch, rows, user=None, progress=None, **kwargs): """ Create a new :term:`order` from the batch data. @@ -689,7 +415,6 @@ class NewOrderBatchHandler(BatchHandler): batch_fields = [ 'store_id', 'customer_id', - 'local_customer', 'pending_customer', 'customer_name', 'phone_number', @@ -698,9 +423,7 @@ class NewOrderBatchHandler(BatchHandler): ] row_fields = [ - 'product_id', - 'local_product', - 'pending_product', + 'pending_product_uuid', 'product_scancode', 'product_brand', 'product_description', diff --git a/src/sideshow/db/alembic/versions/7a6df83afbd4_initial_order_tables.py b/src/sideshow/db/alembic/versions/7a6df83afbd4_initial_order_tables.py index be6eee8..da1d591 100644 --- a/src/sideshow/db/alembic/versions/7a6df83afbd4_initial_order_tables.py +++ b/src/sideshow/db/alembic/versions/7a6df83afbd4_initial_order_tables.py @@ -26,8 +26,8 @@ def upgrade() -> None: sa.Enum('PENDING', 'READY', 'RESOLVED', name='pendingcustomerstatus').create(op.get_bind()) sa.Enum('PENDING', 'READY', 'RESOLVED', name='pendingproductstatus').create(op.get_bind()) - # sideshow_customer_pending - op.create_table('sideshow_customer_pending', + # sideshow_pending_customer + op.create_table('sideshow_pending_customer', sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False), sa.Column('customer_id', sa.String(length=20), nullable=True), sa.Column('full_name', sa.String(length=100), nullable=True), @@ -38,24 +38,12 @@ def upgrade() -> None: sa.Column('status', postgresql.ENUM('PENDING', 'READY', 'RESOLVED', name='pendingcustomerstatus', create_type=False), nullable=False), sa.Column('created', sa.DateTime(timezone=True), nullable=False), sa.Column('created_by_uuid', wuttjamaican.db.util.UUID(), nullable=False), - sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_customer_pending_created_by_uuid_user')), - sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_customer_pending')) + sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_pending_customer_created_by_uuid_user')), + sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_pending_customer')) ) - # sideshow_customer_local - op.create_table('sideshow_customer_local', - sa.Column('full_name', sa.String(length=100), nullable=True), - sa.Column('first_name', sa.String(length=50), nullable=True), - sa.Column('last_name', sa.String(length=50), nullable=True), - sa.Column('phone_number', sa.String(length=20), nullable=True), - sa.Column('email_address', sa.String(length=255), nullable=True), - sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False), - sa.Column('external_id', sa.String(length=20), nullable=True), - sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_customer_local')) - ) - - # sideshow_product_pending - op.create_table('sideshow_product_pending', + # sideshow_pending_product + op.create_table('sideshow_pending_product', sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False), sa.Column('product_id', sa.String(length=20), nullable=True), sa.Column('scancode', sa.String(length=14), nullable=True), @@ -75,29 +63,8 @@ def upgrade() -> None: sa.Column('status', postgresql.ENUM('PENDING', 'READY', 'RESOLVED', name='pendingproductstatus', create_type=False), nullable=False), sa.Column('created', sa.DateTime(timezone=True), nullable=False), sa.Column('created_by_uuid', wuttjamaican.db.util.UUID(), nullable=False), - sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_product_pending_created_by_uuid_user')), - sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_product_pending')) - ) - - # sideshow_product_local - op.create_table('sideshow_product_local', - sa.Column('scancode', sa.String(length=14), nullable=True), - sa.Column('brand_name', sa.String(length=100), nullable=True), - sa.Column('description', sa.String(length=255), nullable=True), - sa.Column('size', sa.String(length=30), nullable=True), - sa.Column('weighed', sa.Boolean(), nullable=True), - sa.Column('department_id', sa.String(length=10), nullable=True), - sa.Column('department_name', sa.String(length=30), nullable=True), - sa.Column('special_order', sa.Boolean(), nullable=True), - sa.Column('vendor_name', sa.String(length=50), nullable=True), - sa.Column('vendor_item_code', sa.String(length=20), nullable=True), - sa.Column('case_size', sa.Numeric(precision=9, scale=4), nullable=True), - sa.Column('unit_cost', sa.Numeric(precision=9, scale=5), nullable=True), - sa.Column('unit_price_reg', sa.Numeric(precision=8, scale=3), nullable=True), - sa.Column('notes', sa.Text(), nullable=True), - sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False), - sa.Column('external_id', sa.String(length=20), nullable=True), - sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_product_local')) + sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_pending_product_created_by_uuid_user')), + sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_pending_product')) ) # sideshow_order @@ -106,7 +73,6 @@ def upgrade() -> None: sa.Column('order_id', sa.Integer(), nullable=False), sa.Column('store_id', sa.String(length=10), nullable=True), sa.Column('customer_id', sa.String(length=20), nullable=True), - sa.Column('local_customer_uuid', wuttjamaican.db.util.UUID(), nullable=True), sa.Column('pending_customer_uuid', wuttjamaican.db.util.UUID(), nullable=True), sa.Column('customer_name', sa.String(length=100), nullable=True), sa.Column('phone_number', sa.String(length=20), nullable=True), @@ -114,8 +80,7 @@ def upgrade() -> None: sa.Column('total_price', sa.Numeric(precision=10, scale=3), nullable=True), sa.Column('created', sa.DateTime(timezone=True), nullable=False), sa.Column('created_by_uuid', wuttjamaican.db.util.UUID(), nullable=False), - sa.ForeignKeyConstraint(['local_customer_uuid'], ['sideshow_customer_local.uuid'], name=op.f('fk_order_local_customer_uuid_local_customer')), - sa.ForeignKeyConstraint(['pending_customer_uuid'], ['sideshow_customer_pending.uuid'], name=op.f('fk_order_pending_customer_uuid_pending_customer')), + sa.ForeignKeyConstraint(['pending_customer_uuid'], ['sideshow_pending_customer.uuid'], name=op.f('fk_order_pending_customer_uuid_pending_customer')), sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_order_created_by_uuid_user')), sa.PrimaryKeyConstraint('uuid', name=op.f('pk_order')) ) @@ -126,7 +91,6 @@ def upgrade() -> None: sa.Column('order_uuid', wuttjamaican.db.util.UUID(), nullable=False), sa.Column('sequence', sa.Integer(), nullable=False), sa.Column('product_id', sa.String(length=20), nullable=True), - sa.Column('local_product_uuid', wuttjamaican.db.util.UUID(), nullable=True), sa.Column('pending_product_uuid', wuttjamaican.db.util.UUID(), nullable=True), sa.Column('product_scancode', sa.String(length=14), nullable=True), sa.Column('product_brand', sa.String(length=100), nullable=True), @@ -151,8 +115,7 @@ def upgrade() -> None: sa.Column('paid_amount', sa.Numeric(precision=8, scale=3), nullable=False), sa.Column('payment_transaction_number', sa.String(length=20), nullable=True), sa.ForeignKeyConstraint(['order_uuid'], ['sideshow_order.uuid'], name=op.f('fk_sideshow_order_item_order_uuid_order')), - sa.ForeignKeyConstraint(['local_product_uuid'], ['sideshow_product_local.uuid'], name=op.f('fk_sideshow_order_item_local_product_uuid_local_product')), - sa.ForeignKeyConstraint(['pending_product_uuid'], ['sideshow_product_pending.uuid'], name=op.f('fk_sideshow_order_item_pending_product_uuid_pending_product')), + sa.ForeignKeyConstraint(['pending_product_uuid'], ['sideshow_pending_product.uuid'], name=op.f('fk_sideshow_order_item_pending_product_uuid_pending_product')), sa.PrimaryKeyConstraint('uuid', name=op.f('pk_order_item')) ) @@ -171,7 +134,6 @@ def upgrade() -> None: sa.Column('executed_by_uuid', wuttjamaican.db.util.UUID(), nullable=True), sa.Column('store_id', sa.String(length=10), nullable=True), sa.Column('customer_id', sa.String(length=20), nullable=True), - sa.Column('local_customer_uuid', wuttjamaican.db.util.UUID(), nullable=True), sa.Column('pending_customer_uuid', wuttjamaican.db.util.UUID(), nullable=True), sa.Column('customer_name', sa.String(length=100), nullable=True), sa.Column('phone_number', sa.String(length=20), nullable=True), @@ -179,8 +141,7 @@ def upgrade() -> None: sa.Column('total_price', sa.Numeric(precision=10, scale=3), nullable=True), sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_batch_neworder_created_by_uuid_user')), sa.ForeignKeyConstraint(['executed_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_batch_neworder_executed_by_uuid_user')), - sa.ForeignKeyConstraint(['local_customer_uuid'], ['sideshow_customer_local.uuid'], name=op.f('fk_sideshow_batch_neworder_local_customer_uuid_local_customer')), - sa.ForeignKeyConstraint(['pending_customer_uuid'], ['sideshow_customer_pending.uuid'], name=op.f('fk_sideshow_batch_neworder_pending_customer_uuid_pending_customer')), + sa.ForeignKeyConstraint(['pending_customer_uuid'], ['sideshow_pending_customer.uuid'], name=op.f('fk_sideshow_batch_neworder_pending_customer_uuid_pending_customer')), sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_batch_neworder')) ) @@ -191,9 +152,8 @@ def upgrade() -> None: sa.Column('sequence', sa.Integer(), nullable=False), sa.Column('status_text', sa.String(length=255), nullable=True), sa.Column('modified', sa.DateTime(timezone=True), nullable=True), - sa.Column('product_id', sa.String(length=20), nullable=True), - sa.Column('local_product_uuid', wuttjamaican.db.util.UUID(), nullable=True), sa.Column('pending_product_uuid', wuttjamaican.db.util.UUID(), nullable=True), + sa.Column('product_id', sa.String(length=20), nullable=True), sa.Column('product_scancode', sa.String(length=14), nullable=True), sa.Column('product_brand', sa.String(length=100), nullable=True), sa.Column('product_description', sa.String(length=255), nullable=True), @@ -213,10 +173,9 @@ def upgrade() -> None: sa.Column('case_price_quoted', sa.Numeric(precision=8, scale=3), nullable=True), sa.Column('discount_percent', sa.Numeric(precision=5, scale=3), nullable=True), sa.Column('total_price', sa.Numeric(precision=8, scale=3), nullable=True), - sa.Column('status_code', sa.Integer(), nullable=True), + sa.Column('status_code', sa.Integer(), nullable=False), sa.ForeignKeyConstraint(['batch_uuid'], ['sideshow_batch_neworder.uuid'], name=op.f('fk_sideshow_batch_neworder_row_batch_uuid_batch_neworder')), - sa.ForeignKeyConstraint(['local_product_uuid'], ['sideshow_product_local.uuid'], name=op.f('fk_sideshow_batch_neworder_row_local_product_uuid_local_product')), - sa.ForeignKeyConstraint(['pending_product_uuid'], ['sideshow_product_pending.uuid'], name=op.f('fk_sideshow_batch_neworder_row_pending_product_uuid_pending_product')), + sa.ForeignKeyConstraint(['pending_product_uuid'], ['sideshow_pending_product.uuid'], name=op.f('fk_sideshow_batch_neworder_row_pending_product_uuid_pending_product')), sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_batch_neworder_row')) ) @@ -233,17 +192,11 @@ def downgrade() -> None: # sideshow_order op.drop_table('sideshow_order') - # sideshow_product_local - op.drop_table('sideshow_product_local') + # sideshow_pending_product + op.drop_table('sideshow_pending_product') - # sideshow_product_pending - op.drop_table('sideshow_product_pending') - - # sideshow_customer_local - op.drop_table('sideshow_customer_local') - - # sideshow_customer_pending - op.drop_table('sideshow_customer_pending') + # sideshow_pending_customer + op.drop_table('sideshow_pending_customer') # enums sa.Enum('PENDING', 'READY', 'RESOLVED', name='pendingproductstatus').drop(op.get_bind()) diff --git a/src/sideshow/db/model/__init__.py b/src/sideshow/db/model/__init__.py index 28d09f3..1395d59 100644 --- a/src/sideshow/db/model/__init__.py +++ b/src/sideshow/db/model/__init__.py @@ -32,8 +32,6 @@ Primary :term:`data models <data model>`: * :class:`~sideshow.db.model.orders.Order` * :class:`~sideshow.db.model.orders.OrderItem` -* :class:`~sideshow.db.model.customers.LocalCustomer` -* :class:`~sideshow.db.model.products.LocalProduct` * :class:`~sideshow.db.model.customers.PendingCustomer` * :class:`~sideshow.db.model.products.PendingProduct` @@ -47,8 +45,8 @@ And the :term:`batch` models: from wuttjamaican.db.model import * # sideshow models -from .customers import LocalCustomer, PendingCustomer -from .products import LocalProduct, PendingProduct +from .customers import PendingCustomer +from .products import PendingProduct from .orders import Order, OrderItem # batch models diff --git a/src/sideshow/db/model/batch/neworder.py b/src/sideshow/db/model/batch/neworder.py index 9121dc6..f121b5c 100644 --- a/src/sideshow/db/model/batch/neworder.py +++ b/src/sideshow/db/model/batch/neworder.py @@ -58,8 +58,7 @@ class NewOrderBatch(model.BatchMixin, model.Base): @declared_attr def __table_args__(cls): return cls.__default_table_args__() + ( - sa.ForeignKeyConstraint(['local_customer_uuid'], ['sideshow_customer_local.uuid']), - sa.ForeignKeyConstraint(['pending_customer_uuid'], ['sideshow_customer_pending.uuid']), + sa.ForeignKeyConstraint(['pending_customer_uuid'], ['sideshow_pending_customer.uuid']), ) STATUS_OK = 1 @@ -73,27 +72,13 @@ class NewOrderBatch(model.BatchMixin, model.Base): """) customer_id = sa.Column(sa.String(length=20), nullable=True, doc=""" - Proper account ID for the :term:`external customer` to which the - order pertains, if applicable. + ID of the proper customer account to which the order pertains, if + applicable. - See also :attr:`local_customer` and :attr:`pending_customer`. + This will be set only when an "existing" customer account can be + selected for the order. See also :attr:`pending_customer`. """) - local_customer_uuid = sa.Column(model.UUID(), nullable=True) - - @declared_attr - def local_customer(cls): - return orm.relationship( - 'LocalCustomer', - back_populates='new_order_batches', - doc=""" - Reference to the - :class:`~sideshow.db.model.customers.LocalCustomer` record - for the order, if applicable. - - See also :attr:`customer_id` and :attr:`pending_customer`. - """) - pending_customer_uuid = sa.Column(model.UUID(), nullable=True) @declared_attr @@ -106,7 +91,8 @@ class NewOrderBatch(model.BatchMixin, model.Base): :class:`~sideshow.db.model.customers.PendingCustomer` record for the order, if applicable. - See also :attr:`customer_id` and :attr:`local_customer`. + This is set only when making an order for a "new / + unknown" customer. See also :attr:`customer_id`. """) customer_name = sa.Column(sa.String(length=100), nullable=True, doc=""" @@ -140,8 +126,7 @@ class NewOrderBatchRow(model.BatchRowMixin, model.Base): @declared_attr def __table_args__(cls): return cls.__default_table_args__() + ( - sa.ForeignKeyConstraint(['local_product_uuid'], ['sideshow_product_local.uuid']), - sa.ForeignKeyConstraint(['pending_product_uuid'], ['sideshow_product_pending.uuid']), + sa.ForeignKeyConstraint(['pending_product_uuid'], ['sideshow_pending_product.uuid']), ) STATUS_OK = 1 @@ -173,27 +158,13 @@ class NewOrderBatchRow(model.BatchRowMixin, model.Base): """ product_id = sa.Column(sa.String(length=20), nullable=True, doc=""" - Proper ID for the :term:`external product` which the order item - represents, if applicable. + ID of the true product which the order item represents, if + applicable. - See also :attr:`local_product` and :attr:`pending_product`. + This will be set only when an "existing" product can be selected + for the order. See also :attr:`pending_product`. """) - local_product_uuid = sa.Column(model.UUID(), nullable=True) - - @declared_attr - def local_product(cls): - return orm.relationship( - 'LocalProduct', - back_populates='new_order_batch_rows', - doc=""" - Reference to the - :class:`~sideshow.db.model.products.LocalProduct` record - for the order item, if applicable. - - See also :attr:`product_id` and :attr:`pending_product`. - """) - pending_product_uuid = sa.Column(model.UUID(), nullable=True) @declared_attr @@ -206,7 +177,8 @@ class NewOrderBatchRow(model.BatchRowMixin, model.Base): :class:`~sideshow.db.model.products.PendingProduct` record for the order item, if applicable. - See also :attr:`product_id` and :attr:`local_product`. + This is set only when making an order for a "new / + unknown" product. See also :attr:`product_id`. """) product_scancode = sa.Column(sa.String(length=14), nullable=True, doc=""" diff --git a/src/sideshow/db/model/customers.py b/src/sideshow/db/model/customers.py index aa62098..f845b30 100644 --- a/src/sideshow/db/model/customers.py +++ b/src/sideshow/db/model/customers.py @@ -34,13 +34,19 @@ from wuttjamaican.db import model from sideshow.enum import PendingCustomerStatus -class CustomerMixin: +class PendingCustomer(model.Base): """ - Base class for customer tables. This has shared columns, used by e.g.: + A "pending" customer record, used when entering an :term:`order` + for new/unknown customer. + """ + __tablename__ = 'sideshow_pending_customer' - * :class:`LocalCustomer` - * :class:`PendingCustomer` - """ + uuid = model.uuid_column() + + customer_id = sa.Column(sa.String(length=20), nullable=True, doc=""" + ID of the proper customer account associated with this record, if + applicable. + """) full_name = sa.Column(sa.String(length=100), nullable=True, doc=""" Full display name for the customer account. @@ -62,74 +68,6 @@ class CustomerMixin: Email address for the customer. """) - def __str__(self): - return self.full_name or "" - - -class LocalCustomer(CustomerMixin, model.Base): - """ - This table contains the :term:`local customer` records. - - Sideshow will do customer lookups against this table by default, - unless it's configured to use :term:`external customers <external - customer>` instead. - - Also by default, when a :term:`new order batch` with a - :term:`pending customer` is executed, a new record is added to - this local customers table, for lookup next time. - """ - __tablename__ = 'sideshow_customer_local' - - uuid = model.uuid_column() - - external_id = sa.Column(sa.String(length=20), nullable=True, doc=""" - ID of the proper customer account associated with this record, if - applicable. - """) - - orders = orm.relationship( - 'Order', - order_by='Order.order_id.desc()', - back_populates='local_customer', - cascade_backrefs=False, - doc=""" - List of :class:`~sideshow.db.model.orders.Order` records - associated with this customer. - """) - - new_order_batches = orm.relationship( - 'NewOrderBatch', - order_by='NewOrderBatch.id.desc()', - back_populates='local_customer', - cascade_backrefs=False, - doc=""" - List of - :class:`~sideshow.db.model.batch.neworder.NewOrderBatch` - records associated with this customer. - """) - - -class PendingCustomer(CustomerMixin, model.Base): - """ - This table contains the :term:`pending customer` records, used - when creating an :term:`order` for new/unknown customer. - - Sideshow will automatically create and (hopefully) delete these - records as needed. - - By default, when a :term:`new order batch` with a pending customer - is executed, a new record is added to the :term:`local customers - <local customer>` table, for lookup next time. - """ - __tablename__ = 'sideshow_customer_pending' - - uuid = model.uuid_column() - - customer_id = sa.Column(sa.String(length=20), nullable=True, doc=""" - ID of the proper customer account associated with this record, if - applicable. - """) - status = sa.Column(sa.Enum(PendingCustomerStatus), nullable=False, doc=""" Status code for the customer record. """) @@ -169,3 +107,6 @@ class PendingCustomer(CustomerMixin, model.Base): :class:`~sideshow.db.model.batch.neworder.NewOrderBatch` records associated with this customer. """) + + def __str__(self): + return self.full_name or "" diff --git a/src/sideshow/db/model/orders.py b/src/sideshow/db/model/orders.py index f694028..c3392f7 100644 --- a/src/sideshow/db/model/orders.py +++ b/src/sideshow/db/model/orders.py @@ -63,26 +63,14 @@ class Order(model.Base): """) customer_id = sa.Column(sa.String(length=20), nullable=True, doc=""" - Proper account ID for the :term:`external customer` to which the - order pertains, if applicable. + ID of the proper customer account to which the order pertains, if + applicable. - See also :attr:`local_customer` and :attr:`pending_customer`. + This will be set only when an "existing" customer account can be + assigned for the order. See also :attr:`pending_customer`. """) - local_customer_uuid = model.uuid_fk_column('sideshow_customer_local.uuid', nullable=True) - local_customer = orm.relationship( - 'LocalCustomer', - cascade_backrefs=False, - back_populates='orders', - doc=""" - Reference to the - :class:`~sideshow.db.model.customers.LocalCustomer` record - for the order, if applicable. - - See also :attr:`customer_id` and :attr:`pending_customer`. - """) - - pending_customer_uuid = model.uuid_fk_column('sideshow_customer_pending.uuid', nullable=True) + pending_customer_uuid = model.uuid_fk_column('sideshow_pending_customer.uuid', nullable=True) pending_customer = orm.relationship( 'PendingCustomer', cascade_backrefs=False, @@ -92,7 +80,8 @@ class Order(model.Base): :class:`~sideshow.db.model.customers.PendingCustomer` record for the order, if applicable. - See also :attr:`customer_id` and :attr:`local_customer`. + This is set only when the order is for a "new / unknown" + customer. See also :attr:`customer_id`. """) customer_name = sa.Column(sa.String(length=100), nullable=True, doc=""" @@ -169,26 +158,14 @@ class OrderItem(model.Base): """) product_id = sa.Column(sa.String(length=20), nullable=True, doc=""" - Proper ID for the :term:`external product` which the order item - represents, if applicable. + ID of the true product which the order item represents, if + applicable. - See also :attr:`local_product` and :attr:`pending_product`. + This will be set only when an "existing" product can be selected + for the order. See also :attr:`pending_product`. """) - local_product_uuid = model.uuid_fk_column('sideshow_product_local.uuid', nullable=True) - local_product = orm.relationship( - 'LocalProduct', - cascade_backrefs=False, - back_populates='order_items', - doc=""" - Reference to the - :class:`~sideshow.db.model.products.LocalProduct` record for - the order item, if applicable. - - See also :attr:`product_id` and :attr:`pending_product`. - """) - - pending_product_uuid = model.uuid_fk_column('sideshow_product_pending.uuid', nullable=True) + pending_product_uuid = model.uuid_fk_column('sideshow_pending_product.uuid', nullable=True) pending_product = orm.relationship( 'PendingProduct', cascade_backrefs=False, @@ -198,7 +175,8 @@ class OrderItem(model.Base): :class:`~sideshow.db.model.products.PendingProduct` record for the order item, if applicable. - See also :attr:`product_id` and :attr:`local_product`. + This is set only when the order item is for a "new / unknown" + product. See also :attr:`product_id`. """) product_scancode = sa.Column(sa.String(length=14), nullable=True, doc=""" @@ -332,15 +310,5 @@ class OrderItem(model.Base): applicable/known. """) - @property - def full_description(self): - """ """ - fields = [ - self.product_brand or '', - self.product_description or '', - self.product_size or ''] - fields = [f.strip() for f in fields if f.strip()] - return ' '.join(fields) - def __str__(self): - return self.full_description + return str(self.pending_product or self.product_description or "") diff --git a/src/sideshow/db/model/products.py b/src/sideshow/db/model/products.py index 70c166d..6113621 100644 --- a/src/sideshow/db/model/products.py +++ b/src/sideshow/db/model/products.py @@ -34,13 +34,18 @@ from wuttjamaican.db import model from sideshow.enum import PendingProductStatus -class ProductMixin: +class PendingProduct(model.Base): """ - Base class for product tables. This has shared columns, used by e.g.: + A "pending" product record, used when entering an :term:`order + item` for new/unknown product. + """ + __tablename__ = 'sideshow_pending_product' - * :class:`LocalProduct` - * :class:`PendingProduct` - """ + uuid = model.uuid_column() + + product_id = sa.Column(sa.String(length=20), nullable=True, doc=""" + ID of the true product associated with this record, if applicable. + """) scancode = sa.Column(sa.String(length=14), nullable=True, doc=""" Scancode for the product, as string. @@ -112,82 +117,6 @@ class ProductMixin: Arbitrary notes regarding the product, if applicable. """) - @property - def full_description(self): - """ """ - fields = [ - self.brand_name or '', - self.description or '', - self.size or ''] - fields = [f.strip() for f in fields if f.strip()] - return ' '.join(fields) - - def __str__(self): - return self.full_description - - -class LocalProduct(ProductMixin, model.Base): - """ - This table contains the :term:`local product` records. - - Sideshow will do customer lookups against this table by default, - unless it's configured to use :term:`external products <external - product>` instead. - - Also by default, when a :term:`new order batch` with - :term:`pending product(s) <pending product>` is executed, new - record(s) will be added to this local products table, for lookup - next time. - """ - __tablename__ = 'sideshow_product_local' - - uuid = model.uuid_column() - - external_id = sa.Column(sa.String(length=20), nullable=True, doc=""" - ID of the true external product associated with this record, if - applicable. - """) - - order_items = orm.relationship( - 'OrderItem', - back_populates='local_product', - cascade_backrefs=False, - doc=""" - List of :class:`~sideshow.db.model.orders.OrderItem` records - associated with this product. - """) - - new_order_batch_rows = orm.relationship( - 'NewOrderBatchRow', - back_populates='local_product', - cascade_backrefs=False, - doc=""" - List of - :class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow` - records associated with this product. - """) - - -class PendingProduct(ProductMixin, model.Base): - """ - This table contains the :term:`pending product` records, used when - creating an :term:`order` for new/unknown product(s). - - Sideshow will automatically create and (hopefully) delete these - records as needed. - - By default, when a :term:`new order batch` with pending product(s) - is executed, new record(s) will be added to the :term:`local - products <local product>` table, for lookup next time. - """ - __tablename__ = 'sideshow_product_pending' - - uuid = model.uuid_column() - - product_id = sa.Column(sa.String(length=20), nullable=True, doc=""" - ID of the true product associated with this record, if applicable. - """) - status = sa.Column(sa.Enum(PendingProductStatus), nullable=False, doc=""" Status code for the product record. """) @@ -209,8 +138,10 @@ class PendingProduct(ProductMixin, model.Base): order_items = orm.relationship( 'OrderItem', - back_populates='pending_product', + # TODO + # order_by='NewOrderBatchRow.id.desc()', cascade_backrefs=False, + back_populates='pending_product', doc=""" List of :class:`~sideshow.db.model.orders.OrderItem` records associated with this product. @@ -218,10 +149,25 @@ class PendingProduct(ProductMixin, model.Base): new_order_batch_rows = orm.relationship( 'NewOrderBatchRow', - back_populates='pending_product', + # TODO + # order_by='NewOrderBatchRow.id.desc()', cascade_backrefs=False, + back_populates='pending_product', doc=""" List of :class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow` records associated with this product. """) + + @property + def full_description(self): + """ """ + fields = [ + self.brand_name or '', + self.description or '', + self.size or ''] + fields = [f.strip() for f in fields if f.strip()] + return ' '.join(fields) + + def __str__(self): + return self.full_description diff --git a/src/sideshow/web/forms/schema.py b/src/sideshow/web/forms/schema.py index 85269c9..4b78a4b 100644 --- a/src/sideshow/web/forms/schema.py +++ b/src/sideshow/web/forms/schema.py @@ -29,7 +29,7 @@ from wuttaweb.forms.schema import ObjectRef class OrderRef(ObjectRef): """ - Schema type for an :class:`~sideshow.db.model.orders.Order` + Custom schema type for an :class:`~sideshow.db.model.orders.Order` reference field. This is a subclass of @@ -51,34 +51,9 @@ class OrderRef(ObjectRef): return self.request.route_url('orders.view', uuid=order.uuid) -class LocalCustomerRef(ObjectRef): - """ - Schema type for a - :class:`~sideshow.db.model.customers.LocalCustomer` reference - field. - - This is a subclass of - :class:`~wuttaweb:wuttaweb.forms.schema.ObjectRef`. - """ - - @property - def model_class(self): - """ """ - model = self.app.model - return model.LocalCustomer - - def sort_query(self, query): - """ """ - return query.order_by(self.model_class.full_name) - - def get_object_url(self, customer): - """ """ - return self.request.route_url('local_customers.view', uuid=customer.uuid) - - class PendingCustomerRef(ObjectRef): """ - Schema type for a + Custom schema type for a :class:`~sideshow.db.model.customers.PendingCustomer` reference field. @@ -101,33 +76,9 @@ class PendingCustomerRef(ObjectRef): return self.request.route_url('pending_customers.view', uuid=customer.uuid) -class LocalProductRef(ObjectRef): - """ - Schema type for a - :class:`~sideshow.db.model.products.LocalProduct` reference field. - - This is a subclass of - :class:`~wuttaweb:wuttaweb.forms.schema.ObjectRef`. - """ - - @property - def model_class(self): - """ """ - model = self.app.model - return model.LocalProduct - - def sort_query(self, query): - """ """ - return query.order_by(self.model_class.scancode) - - def get_object_url(self, product): - """ """ - return self.request.route_url('local_products.view', uuid=product.uuid) - - class PendingProductRef(ObjectRef): """ - Schema type for a + Custom schema type for a :class:`~sideshow.db.model.products.PendingProduct` reference field. diff --git a/src/sideshow/web/menus.py b/src/sideshow/web/menus.py index 2c319cc..5feb017 100644 --- a/src/sideshow/web/menus.py +++ b/src/sideshow/web/menus.py @@ -36,15 +36,14 @@ class SideshowMenuHandler(base.MenuHandler): """ """ return [ self.make_orders_menu(request), - self.make_customers_menu(request), - self.make_products_menu(request), + self.make_pending_menu(request), self.make_batch_menu(request), self.make_admin_menu(request), ] def make_orders_menu(self, request, **kwargs): """ - Generate the Orders menu. + Generate a typical Orders menu. """ return { 'title': "Orders", @@ -56,55 +55,34 @@ class SideshowMenuHandler(base.MenuHandler): 'perm': 'orders.create', }, {'type': 'sep'}, - { - 'title': "All Order Items", - 'route': 'order_items', - 'perm': 'order_items.list', - }, { 'title': "All Orders", 'route': 'orders', 'perm': 'orders.list', }, + { + 'title': "All Order Items", + 'route': 'order_items', + 'perm': 'order_items.list', + }, ], } - def make_customers_menu(self, request, **kwargs): + def make_pending_menu(self, request, **kwargs): """ - Generate the Customers menu. + Generate a typical Pending menu. """ return { - 'title': "Customers", + 'title': "Pending", 'type': 'menu', 'items': [ { - 'title': "Local Customers", - 'route': 'local_customers', - 'perm': 'local_customers.list', - }, - { - 'title': "Pending Customers", + 'title': "Customers", 'route': 'pending_customers', 'perm': 'pending_customers.list', }, - ], - } - - def make_products_menu(self, request, **kwargs): - """ - Generate the Products menu. - """ - return { - 'title': "Products", - 'type': 'menu', - 'items': [ { - 'title': "Local Products", - 'route': 'local_products', - 'perm': 'local_products.list', - }, - { - 'title': "Pending Products", + 'title': "Products", 'route': 'pending_products', 'perm': 'pending_products.list', }, @@ -113,7 +91,7 @@ class SideshowMenuHandler(base.MenuHandler): def make_batch_menu(self, request, **kwargs): """ - Generate the Batch menu. + Generate a typical Batch menu. """ return { 'title': "Batches", diff --git a/src/sideshow/web/templates/orders/configure.mako b/src/sideshow/web/templates/orders/configure.mako deleted file mode 100644 index 4dc23f4..0000000 --- a/src/sideshow/web/templates/orders/configure.mako +++ /dev/null @@ -1,41 +0,0 @@ -## -*- coding: utf-8; -*- -<%inherit file="/configure.mako" /> - -<%def name="form_content()"> - - <h3 class="block is-size-3">Products</h3> - <div class="block" style="padding-left: 2rem;"> - - <b-field message="If set, user can enter details of an arbitrary new "pending" product."> - <b-checkbox name="sideshow.orders.allow_unknown_products" - v-model="simpleSettings['sideshow.orders.allow_unknown_products']" - native-value="true" - @input="settingsNeedSaved = true"> - Allow creating orders for "unknown" products - </b-checkbox> - </b-field> - - <div v-show="simpleSettings['sideshow.orders.allow_unknown_products']" - style="padding-left: 2rem;"> - - <p class="block"> - Require these fields for new product: - </p> - - <div class="block" - style="margin-left: 2rem;"> - % for field in pending_product_fields: - <b-field> - <b-checkbox name="sideshow.orders.unknown_product.fields.${field}.required" - v-model="simpleSettings['sideshow.orders.unknown_product.fields.${field}.required']" - native-value="true" - @input="settingsNeedSaved = true"> - ${field} - </b-checkbox> - </b-field> - % endfor - </div> - - </div> - </div> -</%def> diff --git a/src/sideshow/web/templates/orders/create.mako b/src/sideshow/web/templates/orders/create.mako index 72345b5..7763775 100644 --- a/src/sideshow/web/templates/orders/create.mako +++ b/src/sideshow/web/templates/orders/create.mako @@ -130,20 +130,28 @@ <b-field label="Customer"> <div style="display: flex; gap: 1rem; width: 100%;"> - <wutta-autocomplete ref="customerAutocomplete" - v-model="customerID" - :display="customerName" - service-url="${url(f'{route_prefix}.customer_autocomplete')}" - placeholder="Enter name or phone number" + <b-autocomplete ref="customerAutocomplete" + v-model="customerID" + :style="{'flex-grow': customerID ? '0' : '1'}" + expanded + placeholder="Enter name or phone number" + ## TODO + ## serviceUrl="${url(f'{route_prefix}.customer_autocomplete')}" + % if request.use_oruga: + ## :assigned-label="customerName" + @update:model-value="customerChanged" + % else: + ## :initial-label="customerName" @input="customerChanged" - :style="{'flex-grow': customerID ? '0' : '1'}" - expanded /> + % endif + > + </b-autocomplete> <b-button v-if="customerID" @click="refreshCustomer" icon-pack="fas" icon-left="redo" :disabled="refreshingCustomer"> - {{ refreshingCustomer ? "Refreshing" : "Refresh" }} + {{ refreshingCustomer ? "Refreshig" : "Refresh" }} </b-button> </div> </b-field> @@ -340,9 +348,9 @@ <${b}-modal % if request.use_oruga: - v-model:active="editItemShowDialog" + v-model:active="showingItemDialog" % else: - :active.sync="editItemShowDialog" + :active.sync="showingItemDialog" % endif :can-cancel="['escape', 'x']" > @@ -374,19 +382,28 @@ <div style="flex-grow: 1;"> <b-field label="Product"> - <wutta-autocomplete ref="productAutocomplete" - v-model="productID" - :display="productDisplay" - service-url="${url(f'{route_prefix}.product_autocomplete')}" - placeholder="Enter brand, description etc." - @input="productChanged" /> + <b-autocomplete ref="productLookup" + v-model="productID" + ## :style="{'flex-grow': customerID ? '0' : '1'}" + ## expanded + ## placeholder="Enter name or phone number" + ## ## serviceUrl="${url(f'{route_prefix}.customer_autocomplete')}" + ## % if request.use_oruga: + ## ## :assigned-label="customerName" + ## @update:model-value="customerChanged" + ## % else: + ## ## :initial-label="customerName" + ## @input="customerChanged" + ## % endif + > + </b-autocomplete> </b-field> <div v-if="productID"> <b-field grouped> - <b-field label="Scancode"> - <span>{{ productScancode }}</span> + <b-field :label="productKeyLabel"> + <span>{{ productKey }}</span> </b-field> <b-field label="Unit Size"> @@ -426,16 +443,16 @@ </div> </div> -## <img v-if="productID" -## :src="productImageURL" -## style="max-height: 150px; max-width: 150px; "/> + <img v-if="productID" + :src="productImageURL" + style="max-height: 150px; max-width: 150px; "/> </div> <br /> <div class="field"> <b-radio v-model="productIsKnown" - % if not allow_unknown_products: + % if not allow_unknown_product: disabled % endif :native-value="false"> @@ -476,12 +493,12 @@ <div style="display: flex; gap: 1rem;"> - <b-field label="Scancode" - % if 'scancode' in pending_product_required_fields: - :type="pendingProduct.scancode ? null : 'is-danger'" + <b-field :label="productKeyLabel" + % if 'key' in pending_product_required_fields: + :type="pendingProduct[productKeyField] ? null : 'is-danger'" % endif style="width: 100%;"> - <b-input v-model="pendingProduct.scancode" /> + <b-input v-model="pendingProduct[productKeyField]" /> </b-field> <b-field label="Department" @@ -688,7 +705,7 @@ </${b}-tabs> <div class="buttons"> - <b-button @click="editItemShowDialog = false"> + <b-button @click="showingItemDialog = false"> Cancel </b-button> <b-button type="is-primary" @@ -696,7 +713,7 @@ :disabled="itemDialogSaveDisabled" icon-pack="fas" icon-left="save"> - {{ itemDialogSaving ? "Working, please wait..." : (this.editItemRow ? "Update Item" : "Add Item") }} + {{ itemDialogSaving ? "Working, please wait..." : (this.editingItem ? "Update Item" : "Add Item") }} </b-button> </div> @@ -708,9 +725,9 @@ :data="items" :row-class="(row, i) => row.product_id ? null : 'has-text-success'"> - <${b}-table-column label="Scancode" + <${b}-table-column :label="productKeyLabel" v-slot="props"> - {{ props.row.product_scancode }} + {{ props.row.product_key }} </${b}-table-column> <${b}-table-column label="Brand" @@ -740,10 +757,8 @@ <${b}-table-column label="Unit Price" v-slot="props"> - <span - ##:class="props.row.pricing_reflects_sale ? 'has-background-warning' : null" - > - {{ props.row.unit_price_quoted_display }} + <span :class="props.row.pricing_reflects_sale ? 'has-background-warning' : null"> + {{ props.row.unit_price_display }} </span> </${b}-table-column> @@ -756,15 +771,17 @@ <${b}-table-column label="Vendor" v-slot="props"> - {{ props.row.vendor_name }} + {{ props.row.vendor_display }} </${b}-table-column> <${b}-table-column field="actions" label="Actions" v-slot="props"> <a href="#" - @click.prevent="editItemInit(props.row)"> - + % if not request.use_oruga: + class="grid-action" + % endif + @click.prevent="showEditItemDialog(props.row)"> % if request.use_oruga: <span class="icon-text"> <o-icon icon="edit" /> @@ -829,14 +846,13 @@ batchTotalPriceDisplay: ${json.dumps(normalized_batch['total_price_display'])|n}, customerPanelOpen: false, - customerIsKnown: ${json.dumps(customer_is_known)|n}, - customerID: ${json.dumps(customer_id)|n}, - customerName: ${json.dumps(customer_name)|n}, - orderPhoneNumber: ${json.dumps(phone_number)|n}, - orderEmailAddress: ${json.dumps(email_address)|n}, + customerIsKnown: ${json.dumps(bool(batch.customer_id))|n}, + customerID: ${json.dumps(batch.customer_id)|n}, + customerName: ${json.dumps(batch.customer_name)|n}, + orderPhoneNumber: ${json.dumps(batch.phone_number)|n}, + orderEmailAddress: ${json.dumps(batch.email_address)|n}, refreshingCustomer: false, - newCustomerFullName: ${json.dumps(new_customer_full_name)|n}, newCustomerFirstName: ${json.dumps(new_customer_first_name)|n}, newCustomerLastName: ${json.dumps(new_customer_last_name)|n}, newCustomerPhone: ${json.dumps(new_customer_phone)|n}, @@ -851,8 +867,8 @@ items: ${json.dumps(order_items)|n}, - editItemRow: null, - editItemShowDialog: false, + editingItem: null, + showingItemDialog: false, itemDialogSaving: false, % if request.use_oruga: itemDialogTab: 'product', @@ -860,11 +876,16 @@ itemDialogTabIndex: 0, % endif - productIsKnown: true, + ## TODO + productIsKnown: false, + selectedProduct: null, productID: null, productDisplay: null, - productScancode: null, + ## TODO + productKey: null, + productKeyField: 'scancode', + productKeyLabel: "Scancode", productSize: null, productCaseQuantity: null, productUnitPrice: null, @@ -905,8 +926,20 @@ customerPanelHeader() { let text = "Customer" - if (this.customerName) { - text = "Customer: " + this.customerName + if (this.customerIsKnown) { + if (this.customerID) { + ## TODO + text = "Customer: TODO" +## if (this.$refs.customerAutocomplete) { +## text = "Customer: " + this.$refs.customerAutocomplete.getDisplayText() +## } else { +## text = "Customer: " + this.customerName +## } + } + } else { + if (this.customerName) { + text = "Customer: " + this.customerName + } } if (!this.customerPanelOpen) { @@ -1049,29 +1082,41 @@ } }, - watch: { - - customerIsKnown: function(val) { - - if (val) { - // user clicks "customer is in the system" - - // clear customer - this.customerChanged(null) - - // focus customer autocomplete - this.$nextTick(() => { - this.$refs.customerAutocomplete.focus() - }) - - } else { - // user clicks "customer is NOT in the system" - - // remove true customer; set pending (or null) - this.setPendingCustomer() - } - }, - }, +## TODO +## watch: { +## +## contactIsKnown: function(val) { +## +## // when user clicks "contact is known" then we want to +## // set focus to the autocomplete component +## if (val) { +## this.$nextTick(() => { +## this.$refs.customerAutocomplete.focus() +## }) +## +## // if user has already specified a proper contact, +## // i.e. `contactUUID` is not null, *and* user has +## // clicked the "contact is not yet in the system" +## // button, i.e. `val` is false, then we want to *clear +## // out* the existing contact selection. this is +## // primarily to avoid any ambiguity. +## } else if (this.contactUUID) { +## this.$refs.customerAutocomplete.clearSelection() +## } +## }, +## +## productIsKnown(newval, oldval) { +## // TODO: seems like this should be better somehow? +## // e.g. maybe we should not be clearing *everything* +## // in case user accidentally clicks, and then clicks +## // "is known" again? and if we *should* clear all, +## // why does that require 2 steps? +## if (!newval) { +## this.selectedProduct = null +## this.clearProduct() +## } +## }, +## }, methods: { @@ -1117,7 +1162,7 @@ this.submittingOrder = true const params = { - action: 'submit_order', + action: 'submit_new_order', } this.submitBatchData(params, response => { @@ -1133,28 +1178,29 @@ customerChanged(customerID, callback) { - const params = {} - if (customerID) { - params.action = 'assign_customer' - params.customer_id = customerID + let params + if (!customerID) { + params = { + action: 'unassign_contact', + } } else { - params.action = 'unassign_customer' + params = { + action: 'assign_contact', + customer_id: customerID, + } } - - this.submitBatchData(params, ({data}) => { - this.customerID = data.customer_id - this.customerName = data.customer_name - this.orderPhoneNumber = data.phone_number - this.orderEmailAddress = data.email_address + this.submitBatchData(params, response => { + this.customerID = response.data.customer_id + this.customerName = response.data.customer_name + this.orderPhoneNumber = response.data.phone_number + this.orderEmailAddress = response.data.email_address + this.addOtherPhoneNumber = response.data.add_phone_number + this.addOtherEmailAddress = response.data.add_email_address + this.contactPhones = response.data.contact_phones + this.contactEmails = response.data.contact_emails if (callback) { callback() } - }, response => { - this.$buefy.toast.open({ - message: "Update failed: " + (response.data.error || "(unknown error)"), - type: 'is-danger', - duration: 2000, // 2 seconds - }) }) }, @@ -1193,8 +1239,7 @@ } this.submitBatchData(params, response => { - this.customerName = response.data.new_customer_full_name - this.newCustomerFullName = response.data.new_customer_full_name + this.customerName = response.data.new_customer_name this.newCustomerFirstName = response.data.new_customer_first_name this.newCustomerLastName = response.data.new_customer_last_name this.newCustomerPhone = response.data.phone_number @@ -1214,40 +1259,6 @@ }, - // remove true customer; set pending customer if present - // (else null). this happens when user clicks "customer is - // NOT in the system" - setPendingCustomer() { - - let params - if (this.newCustomerFirstName) { - params = { - action: 'set_pending_customer', - first_name: this.newCustomerFirstName, - last_name: this.newCustomerLastName, - phone_number: this.newCustomerPhone, - email_address: this.newCustomerEmail, - } - } else { - params = { - action: 'unassign_customer', - } - } - - this.submitBatchData(params, ({data}) => { - this.customerID = data.customer_id - this.customerName = data.new_customer_full_name - this.orderPhoneNumber = data.phone_number - this.orderEmailAddress = data.email_address - }, response => { - this.$buefy.toast.open({ - message: "Update failed: " + (response.data.error || "(unknown error)"), - type: 'is-danger', - duration: 2000, // 2 seconds - }) - }) - }, - getCasePriceDisplay() { if (this.productIsKnown) { return this.productCasePriceDisplay @@ -1302,76 +1313,6 @@ } }, - clearProduct() { - this.productID = null - this.productDisplay = null - this.productScancode = null - this.productSize = null - this.productCaseQuantity = null - this.productUnitPrice = null - this.productUnitPriceDisplay = null - this.productUnitRegularPriceDisplay = null - this.productCasePrice = null - this.productCasePriceDisplay = null - this.productSalePrice = null - this.productSalePriceDisplay = null - this.productSaleEndsDisplay = null - this.productUnitChoices = this.defaultUnitChoices - }, - - productChanged(productID) { - if (productID) { - const params = { - action: 'get_product_info', - product_id: productID, - } - // nb. it is possible for the handler to "swap" - // the product selection, i.e. user chooses a "per - // LB" item but the handler only allows selling by - // the "case" item. so we do not assume the uuid - // received above is the correct one, but just use - // whatever came back from handler - this.submitBatchData(params, ({data}) => { - this.selectedProduct = data - - this.productID = data.product_id - this.productScancode = data.scancode - this.productDisplay = data.full_description - this.productSize = data.size - this.productCaseQuantity = data.case_size - - // TODO: what is the difference here - this.productUnitPrice = data.unit_price_reg - this.productUnitPriceDisplay = data.unit_price_reg_display - this.productUnitRegularPriceDisplay = data.unit_price_display - - this.productCasePrice = data.case_price_quoted - this.productCasePriceDisplay = data.case_price_quoted_display - - this.productSalePrice = data.unit_price_sale - this.productSalePriceDisplay = data.unit_price_sale_display - this.productSaleEndsDisplay = data.sale_ends_display - - // this.setProductUnitChoices(data.uom_choices) - - % if request.use_oruga: - this.itemDialogTab = 'quantity' - % else: - this.itemDialogTabIndex = 1 - % endif - - // nb. hack to force refresh for vue3 - this.refreshProductDescription += 1 - this.refreshTotalPrice += 1 - - }, response => { - this.clearProduct() - }) - } else { - this.clearProduct() - } - }, - ## TODO ## productLookupSelected(selected) { ## // TODO: this still is a hack somehow, am sure of it. @@ -1399,12 +1340,14 @@ showAddItemDialog() { this.customerPanelOpen = false - this.editItemRow = null - this.productIsKnown = true + this.editingItem = null + // TODO + // this.productIsKnown = true + this.productIsKnown = false ## this.selectedProduct = null this.productID = null this.productDisplay = null - this.productScancode = null + ## this.productKey = null this.productSize = null this.productCaseQuantity = null this.productUnitPrice = null @@ -1428,15 +1371,14 @@ % else: this.itemDialogTabIndex = 0 % endif - this.editItemShowDialog = true + this.showingItemDialog = true this.$nextTick(() => { - // this.$refs.productLookup.focus() - this.$refs.productAutocomplete.focus() + this.$refs.productLookup.focus() }) }, - editItemInit(row) { - this.editItemRow = row + showEditItemDialog(row) { + this.editingItem = row this.productIsKnown = !!row.product_id this.productID = row.product_id @@ -1460,9 +1402,10 @@ this.pendingProduct = pending this.productDisplay = row.product_full_description - this.productScancode = row.product_scancode + this.productKey = row.product_key this.productSize = row.product_size - this.productCaseQuantity = row.case_size + this.productCaseQuantity = row.case_quantity + this.productURL = row.product_url this.productUnitPrice = row.unit_price_quoted this.productUnitPriceDisplay = row.unit_price_quoted_display this.productUnitRegularPriceDisplay = row.unit_price_reg_display @@ -1486,7 +1429,7 @@ % else: this.itemDialogTabIndex = 1 % endif - this.editItemShowDialog = true + this.showingItemDialog = true }, deleteItem(index) { @@ -1515,20 +1458,25 @@ itemDialogAttemptSave() { this.itemDialogSaving = true - const params = { + let params = { + product_is_known: this.productIsKnown, order_qty: this.productQuantity, order_uom: this.productUOM, } + % if allow_item_discounts: + params.discount_percent = this.productDiscountPercent + % endif + if (this.productIsKnown) { - params.product_info = this.productID + params.product_uuid = this.productUUID } else { - params.product_info = this.pendingProduct + params.pending_product = this.pendingProduct } - if (this.editItemRow) { + if (this.editingItem) { params.action = 'update_item' - params.uuid = this.editItemRow.uuid + params.uuid = this.editingItem.uuid } else { params.action = 'add_item' } @@ -1543,7 +1491,7 @@ // overwriting the item record, or else display will // not update properly for (let [key, value] of Object.entries(response.data.row)) { - this.editItemRow[key] = value + this.editingItem[key] = value } } @@ -1551,7 +1499,7 @@ this.batchTotalPriceDisplay = response.data.batch.total_price_display this.itemDialogSaving = false - this.editItemShowDialog = false + this.showingItemDialog = false }, response => { this.itemDialogSaving = false }) diff --git a/src/sideshow/web/views/batch/neworder.py b/src/sideshow/web/views/batch/neworder.py index 5e45da1..0c0aad5 100644 --- a/src/sideshow/web/views/batch/neworder.py +++ b/src/sideshow/web/views/batch/neworder.py @@ -29,7 +29,7 @@ from wuttaweb.forms.schema import WuttaMoney from sideshow.db.model import NewOrderBatch from sideshow.batch.neworder import NewOrderBatchHandler -from sideshow.web.forms.schema import LocalCustomerRef, PendingCustomerRef +from sideshow.web.forms.schema import PendingCustomerRef class NewOrderBatchView(BatchMasterView): @@ -87,7 +87,6 @@ class NewOrderBatchView(BatchMasterView): 'id', 'store_id', 'customer_id', - 'local_customer', 'pending_customer', 'customer_name', 'phone_number', @@ -116,11 +115,9 @@ class NewOrderBatchView(BatchMasterView): 'product_description', 'product_size', 'special_order', - 'unit_price_quoted', - 'case_size', - 'case_price_quoted', 'order_qty', 'order_uom', + 'case_size', 'total_price', 'status_code', ] @@ -141,9 +138,6 @@ class NewOrderBatchView(BatchMasterView): """ """ super().configure_form(f) - # local_customer - f.set_node('local_customer', LocalCustomerRef(self.request)) - # pending_customer f.set_node('pending_customer', PendingCustomerRef(self.request)) @@ -159,14 +153,6 @@ class NewOrderBatchView(BatchMasterView): # order_uom #g.set_renderer('order_uom', self.grid_render_enum, enum=enum.ORDER_UOM) - # unit_price_quoted - g.set_label('unit_price_quoted', "Unit Price", column_only=True) - g.set_renderer('unit_price_quoted', 'currency') - - # case_price_quoted - g.set_label('case_price_quoted', "Case Price", column_only=True) - g.set_renderer('case_price_quoted', 'currency') - # total_price g.set_renderer('total_price', 'currency') diff --git a/src/sideshow/web/views/customers.py b/src/sideshow/web/views/customers.py index 850ec5e..9d1720e 100644 --- a/src/sideshow/web/views/customers.py +++ b/src/sideshow/web/views/customers.py @@ -27,161 +27,7 @@ Views for Customers from wuttaweb.views import MasterView from wuttaweb.forms.schema import UserRef, WuttaEnum -from sideshow.db.model import LocalCustomer, PendingCustomer - - -class LocalCustomerView(MasterView): - """ - Master view for - :class:`~sideshow.db.model.customers.LocalCustomer`; route prefix - is ``local_customers``. - - Notable URLs provided by this class: - - * ``/local/customers/`` - * ``/local/customers/new`` - * ``/local/customers/XXX`` - * ``/local/customers/XXX/edit`` - * ``/local/customers/XXX/delete`` - """ - model_class = LocalCustomer - model_title = "Local Customer" - route_prefix = 'local_customers' - url_prefix = '/local/customers' - - labels = { - 'external_id': "External ID", - } - - grid_columns = [ - 'external_id', - 'full_name', - 'first_name', - 'last_name', - 'phone_number', - 'email_address', - ] - - sort_defaults = 'full_name' - - form_fields = [ - 'external_id', - 'full_name', - 'first_name', - 'last_name', - 'phone_number', - 'email_address', - 'orders', - 'new_order_batches', - ] - - def configure_grid(self, g): - """ """ - super().configure_grid(g) - - # links - g.set_link('full_name') - g.set_link('first_name') - g.set_link('last_name') - g.set_link('phone_number') - g.set_link('email_address') - - def configure_form(self, f): - """ """ - super().configure_form(f) - customer = f.model_instance - - # external_id - if self.creating: - f.remove('external_id') - else: - f.set_readonly('external_id') - - # full_name - if self.creating or self.editing: - f.remove('full_name') - - # orders - if self.creating or self.editing: - f.remove('orders') - else: - f.set_grid('orders', self.make_orders_grid(customer)) - - # new_order_batches - if self.creating or self.editing: - f.remove('new_order_batches') - else: - f.set_grid('new_order_batches', self.make_new_order_batches_grid(customer)) - - def make_orders_grid(self, customer): - """ - Make and return the grid for the Orders field. - """ - model = self.app.model - route_prefix = self.get_route_prefix() - - grid = self.make_grid(key=f'{route_prefix}.view.orders', - model_class=model.Order, - data=customer.orders, - columns=[ - 'order_id', - 'total_price', - 'created', - 'created_by', - ], - labels={ - 'order_id': "Order ID", - }) - grid.set_renderer('total_price', grid.render_currency) - - if self.request.has_perm('orders.view'): - url = lambda order, i: self.request.route_url('orders.view', uuid=order.uuid) - grid.add_action('view', icon='eye', url=url) - grid.set_link('order_id') - - return grid - - def make_new_order_batches_grid(self, customer): - """ - Make and return the grid for the New Order Batches field. - """ - model = self.app.model - route_prefix = self.get_route_prefix() - - grid = self.make_grid(key=f'{route_prefix}.view.new_order_batches', - model_class=model.NewOrderBatch, - data=customer.new_order_batches, - columns=[ - 'id', - 'total_price', - 'created', - 'created_by', - 'executed', - ], - labels={ - 'id': "Batch ID", - }, - renderers={ - 'id': 'batch_id', - 'total_price': 'currency', - }) - - if self.request.has_perm('neworder_batches.view'): - url = lambda batch, i: self.request.route_url('neworder_batches.view', uuid=batch.uuid) - grid.add_action('view', icon='eye', url=url) - grid.set_link('id') - - return grid - - def objectify(self, form): - """ """ - enum = self.app.enum - customer = super().objectify(form) - - customer.full_name = self.app.make_full_name(customer.first_name, - customer.last_name) - - return customer +from sideshow.db.model import PendingCustomer class PendingCustomerView(MasterView): @@ -225,9 +71,12 @@ class PendingCustomerView(MasterView): 'customer_id', 'full_name', 'first_name', + 'middle_name', 'last_name', 'phone_number', + 'phone_type', 'email_address', + 'email_type', 'status', 'created', 'created_by', @@ -389,9 +238,6 @@ class PendingCustomerView(MasterView): def defaults(config, **kwargs): base = globals() - LocalCustomerView = kwargs.get('LocalCustomerView', base['LocalCustomerView']) - LocalCustomerView.defaults(config) - PendingCustomerView = kwargs.get('PendingCustomerView', base['PendingCustomerView']) PendingCustomerView.defaults(config) diff --git a/src/sideshow/web/views/orders.py b/src/sideshow/web/views/orders.py index bbd5811..76b49f3 100644 --- a/src/sideshow/web/views/orders.py +++ b/src/sideshow/web/views/orders.py @@ -31,13 +31,11 @@ import colander from sqlalchemy import orm from wuttaweb.views import MasterView -from wuttaweb.forms.schema import UserRef, WuttaMoney, WuttaQuantity, WuttaEnum, WuttaDictEnum +from wuttaweb.forms.schema import UserRef, WuttaMoney, WuttaQuantity, WuttaEnum from sideshow.db.model import Order, OrderItem from sideshow.batch.neworder import NewOrderBatchHandler -from sideshow.web.forms.schema import (OrderRef, - LocalCustomerRef, LocalProductRef, - PendingCustomerRef, PendingProductRef) +from sideshow.web.forms.schema import OrderRef, PendingCustomerRef, PendingProductRef log = logging.getLogger(__name__) @@ -60,7 +58,6 @@ class OrderView(MasterView): """ model_class = Order editable = False - configurable = True labels = { 'order_id': "Order ID", @@ -84,7 +81,6 @@ class OrderView(MasterView): 'order_id', 'store_id', 'customer_id', - 'local_customer', 'pending_customer', 'customer_name', 'phone_number', @@ -126,14 +122,15 @@ class OrderView(MasterView): PENDING_PRODUCT_ENTRY_FIELDS = [ 'scancode', + 'department_id', + 'department_name', 'brand_name', 'description', 'size', - 'department_name', 'vendor_name', 'vendor_item_code', - 'case_size', 'unit_cost', + 'case_size', 'unit_price_reg', ] @@ -168,20 +165,6 @@ class OrderView(MasterView): batch, which in turn creates a true :class:`~sideshow.db.model.orders.Order`, and user is redirected to the "view order" page. - - See also these methods which may be called from this one, - based on user actions: - - * :meth:`start_over()` - * :meth:`cancel_order()` - * :meth:`assign_customer()` - * :meth:`unassign_customer()` - * :meth:`set_pending_customer()` - * :meth:`get_product_info()` - * :meth:`add_item()` - * :meth:`update_item()` - * :meth:`delete_item()` - * :meth:`submit_order()` """ enum = self.app.enum self.creating = True @@ -205,25 +188,22 @@ class OrderView(MasterView): data = dict(self.request.json_body) action = data.pop('action') json_actions = [ - 'assign_customer', - 'unassign_customer', + # 'assign_contact', + # 'unassign_contact', # 'update_phone_number', # 'update_email_address', 'set_pending_customer', # 'get_customer_info', # # 'set_customer_data', - 'get_product_info', + # 'get_product_info', # 'get_past_items', 'add_item', 'update_item', 'delete_item', - 'submit_order', + 'submit_new_order', ] if action in json_actions: - try: - result = getattr(self, action)(batch, data) - except Exception as error: - result = {'error': self.app.render_error(error)} + result = getattr(self, action)(batch, data) return self.json_response(result) return self.json_response({'error': "unknown form action"}) @@ -233,10 +213,10 @@ class OrderView(MasterView): 'normalized_batch': self.normalize_batch(batch), 'order_items': [self.normalize_row(row) for row in batch.rows], + + 'allow_unknown_product': True, # TODO 'default_uom_choices': self.get_default_uom_choices(), 'default_uom': None, # TODO? - 'allow_unknown_products': (self.batch_handler.allow_unknown_products() - and self.has_perm('create_unknown_product')), 'pending_product_required_fields': self.get_pending_product_required_fields(), }) return self.render_to_response('create', context) @@ -275,96 +255,6 @@ class OrderView(MasterView): return batch - def customer_autocomplete(self): - """ - AJAX view for customer autocomplete, when entering new order. - - This should invoke a configured handler for the autocomplete - behavior, but that is not yet implemented. For now it uses - built-in logic only, which queries the - :class:`~sideshow.db.model.customers.LocalCustomer` table. - """ - session = self.Session() - term = self.request.GET.get('term', '').strip() - if not term: - return [] - return self.mock_autocomplete_customers(session, term, user=self.request.user) - - # TODO: move this to some handler - def mock_autocomplete_customers(self, session, term, user=None): - """ """ - import sqlalchemy as sa - - model = self.app.model - - # base query - query = session.query(model.LocalCustomer) - - # filter query - criteria = [model.LocalCustomer.full_name.ilike(f'%{word}%') - for word in term.split()] - query = query.filter(sa.and_(*criteria)) - - # sort query - query = query.order_by(model.LocalCustomer.full_name) - - # get data - # TODO: need max_results option - customers = query.all() - - # get results - def result(customer): - return {'value': customer.uuid.hex, - 'label': customer.full_name} - return [result(c) for c in customers] - - def product_autocomplete(self): - """ - AJAX view for product autocomplete, when entering new order. - - This should invoke a configured handler for the autocomplete - behavior, but that is not yet implemented. For now it uses - built-in logic only, which queries the - :class:`~sideshow.db.model.products.LocalProduct` table. - """ - session = self.Session() - term = self.request.GET.get('term', '').strip() - if not term: - return [] - return self.mock_autocomplete_products(session, term, user=self.request.user) - - # TODO: move this to some handler - def mock_autocomplete_products(self, session, term, user=None): - """ """ - import sqlalchemy as sa - - model = self.app.model - - # base query - query = session.query(model.LocalProduct) - - # filter query - criteria = [] - for word in term.split(): - criteria.append(sa.or_( - model.LocalProduct.brand_name.ilike(f'%{word}%'), - model.LocalProduct.description.ilike(f'%{word}%'))) - query = query.filter(sa.and_(*criteria)) - - # sort query - query = query.order_by(model.LocalProduct.brand_name, - model.LocalProduct.description) - - # get data - # TODO: need max_results option - products = query.all() - - # get results - def result(product): - return {'value': product.uuid.hex, - 'label': product.full_description} - return [result(c) for c in products] - def get_pending_product_required_fields(self): """ """ required = [] @@ -384,10 +274,7 @@ class OrderView(MasterView): new batch for them. This is a "batch action" method which may be called from - :meth:`create()`. See also: - - * :meth:`cancel_order()` - * :meth:`submit_order()` + :meth:`create()`. """ # drop current batch self.batch_handler.do_delete(batch, self.request.user) @@ -404,10 +291,7 @@ class OrderView(MasterView): back to "List Orders" page. This is a "batch action" method which may be called from - :meth:`create()`. See also: - - * :meth:`start_over()` - * :meth:`submit_order()` + :meth:`create()`. """ self.batch_handler.do_delete(batch, self.request.user) self.Session.flush() @@ -422,193 +306,86 @@ class OrderView(MasterView): def get_context_customer(self, batch): """ """ context = { - 'customer_is_known': True, - 'customer_id': None, + 'customer_id': batch.customer_id, 'customer_name': batch.customer_name, 'phone_number': batch.phone_number, 'email_address': batch.email_address, + 'new_customer_name': None, + 'new_customer_first_name': None, + 'new_customer_last_name': None, + 'new_customer_phone': None, + 'new_customer_email': None, } - # customer_id - use_local = self.batch_handler.use_local_customers() - if use_local: - local = batch.local_customer - if local: - context['customer_id'] = local.uuid.hex - else: # use external - context['customer_id'] = batch.customer_id - - # pending customer pending = batch.pending_customer if pending: context.update({ 'new_customer_first_name': pending.first_name, 'new_customer_last_name': pending.last_name, - 'new_customer_full_name': pending.full_name, + 'new_customer_name': pending.full_name, 'new_customer_phone': pending.phone_number, 'new_customer_email': pending.email_address, }) - # declare customer "not known" only if pending is in use - if (pending - and not batch.customer_id and not batch.local_customer - and batch.customer_name): - context['customer_is_known'] = False + # figure out if customer is "known" from user's perspective. + # if we have an ID then it's definitely known, otherwise if we + # have a pending customer then it's definitely *not* known, + # but if no pending customer yet then we can still "assume" it + # is known, by default, until user specifies otherwise. + if batch.customer_id: + context['customer_is_known'] = True + else: + context['customer_is_known'] = not pending return context - def assign_customer(self, batch, data): - """ - Assign the true customer account for a batch. - - This calls - :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.set_customer()` - for the heavy lifting. - - This is a "batch action" method which may be called from - :meth:`create()`. See also: - - * :meth:`unassign_customer()` - * :meth:`set_pending_customer()` - """ - customer_id = data.get('customer_id') - if not customer_id: - return {'error': "Must provide customer_id"} - - self.batch_handler.set_customer(batch, customer_id) - return self.get_context_customer(batch) - - def unassign_customer(self, batch, data): - """ - Clear the customer info for a batch. - - This calls - :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.set_customer()` - for the heavy lifting. - - This is a "batch action" method which may be called from - :meth:`create()`. See also: - - * :meth:`assign_customer()` - * :meth:`set_pending_customer()` - """ - self.batch_handler.set_customer(batch, None) - return self.get_context_customer(batch) - def set_pending_customer(self, batch, data): """ This will set/update the batch pending customer info. This calls - :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.set_customer()` + :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.set_pending_customer()` for the heavy lifting. - This is a "batch action" method which may be called from - :meth:`create()`. See also: - - * :meth:`assign_customer()` - * :meth:`unassign_customer()` - """ - self.batch_handler.set_customer(batch, data, user=self.request.user) - return self.get_context_customer(batch) - - def get_product_info(self, batch, data): - """ - Fetch data for a specific product. (Nothing is modified.) - - Depending on config, this will fetch a :term:`local product` - or :term:`external product` to get the data. - - This should invoke a configured handler for the query - behavior, but that is not yet implemented. For now it uses - built-in logic only, which queries the - :class:`~sideshow.db.model.products.LocalProduct` table. - This is a "batch action" method which may be called from :meth:`create()`. """ - product_id = data.get('product_id') - if not product_id: - return {'error': "Must specify a product ID"} + data['created_by'] = self.request.user + try: + self.batch_handler.set_pending_customer(batch, data) + except Exception as error: + return {'error': self.app.render_error(error)} - use_local = self.batch_handler.use_local_products() - if use_local: - data = self.get_local_product_info(product_id) - else: - raise NotImplementedError("TODO: add integration handler") - - if 'error' in data: - return data - - if 'unit_price_reg' in data and 'unit_price_reg_display' not in data: - data['unit_price_reg_display'] = self.app.render_currency(data['unit_price_reg']) - - if 'unit_price_reg' in data and 'unit_price_quoted' not in data: - data['unit_price_quoted'] = data['unit_price_reg'] - - if 'unit_price_quoted' in data and 'unit_price_quoted_display' not in data: - data['unit_price_quoted_display'] = self.app.render_currency(data['unit_price_quoted']) - - if 'case_price_quoted' not in data: - if data.get('unit_price_quoted') is not None and data.get('case_size') is not None: - data['case_price_quoted'] = data['unit_price_quoted'] * data['case_size'] - - if 'case_price_quoted' in data and 'case_price_quoted_display' not in data: - data['case_price_quoted_display'] = self.app.render_currency(data['case_price_quoted']) - - decimal_fields = [ - 'case_size', - 'unit_price_reg', - 'unit_price_quoted', - 'case_price_quoted', - ] - - for field in decimal_fields: - if field in list(data): - value = data[field] - if isinstance(value, decimal.Decimal): - data[field] = float(value) - - return data - - # TODO: move this to some handler - def get_local_product_info(self, product_id): - """ """ - model = self.app.model - session = self.Session() - product = session.get(model.LocalProduct, product_id) - if not product: - return {'error': "Product not found"} - - return { - 'product_id': product.uuid.hex, - 'scancode': product.scancode, - 'brand_name': product.brand_name, - 'description': product.description, - 'size': product.size, - 'full_description': product.full_description, - 'weighed': product.weighed, - 'special_order': product.special_order, - 'department_id': product.department_id, - 'department_name': product.department_name, - 'case_size': product.case_size, - 'unit_price_reg': product.unit_price_reg, - 'vendor_name': product.vendor_name, - 'vendor_item_code': product.vendor_item_code, - } + self.Session.flush() + context = self.get_context_customer(batch) + return context def add_item(self, batch, data): """ This adds a row to the user's current new order batch. This is a "batch action" method which may be called from - :meth:`create()`. See also: - - * :meth:`update_item()` - * :meth:`delete_item()` + :meth:`create()`. """ - row = self.batch_handler.add_item(batch, data['product_info'], - data['order_qty'], data['order_uom']) + order_qty = decimal.Decimal(data.get('order_qty') or '0') + order_uom = data['order_uom'] + + if data.get('product_is_known'): + raise NotImplementedError + + else: # unknown product; add pending + pending = data['pending_product'] + + for field in ('unit_cost', 'unit_price_reg', 'case_size'): + if field in pending: + try: + pending[field] = decimal.Decimal(pending[field]) + except decimal.InvalidOperation: + return {'error': f"Invalid entry for field: {field}"} + + pending['created_by'] = self.request.user + row = self.batch_handler.add_pending_product(batch, pending, + order_qty, order_uom) return {'batch': self.normalize_batch(batch), 'row': self.normalize_row(row)} @@ -618,17 +395,15 @@ class OrderView(MasterView): This updates a row in the user's current new order batch. This is a "batch action" method which may be called from - :meth:`create()`. See also: - - * :meth:`add_item()` - * :meth:`delete_item()` + :meth:`create()`. """ model = self.app.model + enum = self.app.enum session = self.Session() uuid = data.get('uuid') if not uuid: - return {'error': "Must specify row UUID"} + return {'error': "Must specify a row UUID"} row = session.get(model.NewOrderBatchRow, uuid) if not row: @@ -637,8 +412,20 @@ class OrderView(MasterView): if row.batch is not batch: return {'error': "Row is for wrong batch"} - self.batch_handler.update_item(row, data['product_info'], - data['order_qty'], data['order_uom']) + order_qty = decimal.Decimal(data.get('order_qty') or '0') + order_uom = data['order_uom'] + + if data.get('product_is_known'): + raise NotImplementedError + + else: # pending product + + # set these first, since row will be refreshed below + row.order_qty = order_qty + row.order_uom = order_uom + + # nb. this will refresh the row + self.batch_handler.set_pending_product(row, data['pending_product']) return {'batch': self.normalize_batch(batch), 'row': self.normalize_row(row)} @@ -648,10 +435,7 @@ class OrderView(MasterView): This deletes a row from the user's current new order batch. This is a "batch action" method which may be called from - :meth:`create()`. See also: - - * :meth:`add_item()` - * :meth:`update_item()` + :meth:`create()`. """ model = self.app.model session = self.app.get_session(batch) @@ -668,18 +452,16 @@ class OrderView(MasterView): return {'error': "Row is for wrong batch"} self.batch_handler.do_remove_row(row) + session.flush() return {'batch': self.normalize_batch(batch)} - def submit_order(self, batch, data): + def submit_new_order(self, batch, data): """ This submits the user's current new order batch, hence executing the batch and creating the true order. This is a "batch action" method which may be called from - :meth:`create()`. See also: - - * :meth:`start_over()` - * :meth:`cancel_order()` + :meth:`create()`. """ user = self.request.user reason = self.batch_handler.why_not_execute(batch, user=user) @@ -720,7 +502,6 @@ class OrderView(MasterView): data = { 'uuid': row.uuid.hex, 'sequence': row.sequence, - 'product_id': None, 'product_scancode': row.product_scancode, 'product_brand': row.product_brand, 'product_description': row.product_description, @@ -728,8 +509,8 @@ class OrderView(MasterView): 'product_weighed': row.product_weighed, 'department_display': row.department_name, 'special_order': row.special_order, - 'case_size': float(row.case_size) if row.case_size is not None else None, - 'order_qty': float(row.order_qty), + 'case_size': self.app.render_quantity(row.case_size), + 'order_qty': self.app.render_quantity(row.order_qty), 'order_uom': row.order_uom, 'order_uom_choices': self.get_default_uom_choices(), 'unit_price_quoted': float(row.unit_price_quoted) if row.unit_price_quoted is not None else None, @@ -742,33 +523,6 @@ class OrderView(MasterView): 'status_text': row.status_text, } - use_local = self.batch_handler.use_local_products() - - # product_id - if use_local: - if row.local_product: - data['product_id'] = row.local_product.uuid.hex - else: - data['product_id'] = row.product_id - - # product_full_description - if use_local: - if row.local_product: - data['product_full_description'] = row.local_product.full_description - else: # use external - pass # TODO - if not data.get('product_id') and row.pending_product: - data['product_full_description'] = row.pending_product.full_description - - # vendor_name - if use_local: - if row.local_product: - data['vendor_name'] = row.local_product.vendor_name - else: # use external - pass # TODO - if not data.get('product_id') and row.pending_product: - data['vendor_name'] = row.pending_product.vendor_name - if row.unit_price_reg: data['unit_price_reg'] = float(row.unit_price_reg) data['unit_price_reg_display'] = self.app.render_currency(row.unit_price_reg) @@ -781,8 +535,21 @@ class OrderView(MasterView): data['sale_ends'] = str(row.sale_ends) data['sale_ends_display'] = self.app.render_date(row.sale_ends) + # if row.unit_price_sale and row.unit_price_quoted == row.unit_price_sale: + # data['pricing_reflects_sale'] = True + + # TODO + if row.pending_product: + data['product_full_description'] = row.pending_product.full_description + # else: + # data['product_full_description'] = row.product_description + + # if row.pending_product: + # data['vendor_display'] = row.pending_product.vendor_name + if row.pending_product: pending = row.pending_product + # data['vendor_display'] = pending.vendor_name data['pending_product'] = { 'uuid': pending.uuid.hex, 'scancode': pending.scancode, @@ -800,17 +567,19 @@ class OrderView(MasterView): 'special_order': pending.special_order, } + # TODO: remove this + data['product_key'] = row.product_scancode + # display text for order qty/uom if row.order_uom == enum.ORDER_UOM_CASE: - order_qty = self.app.render_quantity(row.order_qty) if row.case_size is None: case_qty = unit_qty = '??' else: - case_qty = self.app.render_quantity(row.case_size) + case_qty = data['case_size'] unit_qty = self.app.render_quantity(row.order_qty * row.case_size) CS = enum.ORDER_UOM[enum.ORDER_UOM_CASE] EA = enum.ORDER_UOM[enum.ORDER_UOM_UNIT] - data['order_qty_display'] = (f"{order_qty} {CS} " + data['order_qty_display'] = (f"{data['order_qty']} {CS} " f"(× {case_qty} = {unit_qty} {EA})") else: unit_qty = self.app.render_quantity(row.order_qty) @@ -826,16 +595,9 @@ class OrderView(MasterView): def configure_form(self, f): """ """ super().configure_form(f) - order = f.model_instance - - # local_customer - f.set_node('local_customer', LocalCustomerRef(self.request)) # pending_customer - if order.customer_id or order.local_customer: - f.remove('pending_customer') - else: - f.set_node('pending_customer', PendingCustomerRef(self.request)) + f.set_node('pending_customer', PendingCustomerRef(self.request)) # total_price f.set_node('total_price', WuttaMoney(self.request)) @@ -908,75 +670,6 @@ class OrderView(MasterView): """ """ return self.request.route_url('order_items.view', uuid=item.uuid) - def configure_get_simple_settings(self): - """ """ - settings = [ - - # products - {'name': 'sideshow.orders.allow_unknown_products', - 'type': bool, - 'default': True}, - ] - - # required fields for new product entry - for field in self.PENDING_PRODUCT_ENTRY_FIELDS: - setting = {'name': f'sideshow.orders.unknown_product.fields.{field}.required', - 'type': bool} - if field == 'description': - setting['default'] = True - settings.append(setting) - - return settings - - def configure_get_context(self, **kwargs): - """ """ - context = super().configure_get_context(**kwargs) - - context['pending_product_fields'] = self.PENDING_PRODUCT_ENTRY_FIELDS - - return context - - @classmethod - def defaults(cls, config): - cls._order_defaults(config) - cls._defaults(config) - - @classmethod - def _order_defaults(cls, config): - route_prefix = cls.get_route_prefix() - permission_prefix = cls.get_permission_prefix() - url_prefix = cls.get_url_prefix() - model_title = cls.get_model_title() - model_title_plural = cls.get_model_title_plural() - - # fix perm group - config.add_wutta_permission_group(permission_prefix, - model_title_plural, - overwrite=False) - - # extra perm required to create order with unknown/pending product - config.add_wutta_permission(permission_prefix, - f'{permission_prefix}.create_unknown_product', - f"Create new {model_title} for unknown/pending product") - - # customer autocomplete - config.add_route(f'{route_prefix}.customer_autocomplete', - f'{url_prefix}/customer-autocomplete', - request_method='GET') - config.add_view(cls, attr='customer_autocomplete', - route_name=f'{route_prefix}.customer_autocomplete', - renderer='json', - permission=f'{permission_prefix}.list') - - # product autocomplete - config.add_route(f'{route_prefix}.product_autocomplete', - f'{url_prefix}/product-autocomplete', - request_method='GET') - config.add_view(cls, attr='product_autocomplete', - route_name=f'{route_prefix}.product_autocomplete', - renderer='json', - permission=f'{permission_prefix}.list') - class OrderItemView(MasterView): """ @@ -1006,8 +699,7 @@ class OrderItemView(MasterView): 'product_brand': "Brand", 'product_description': "Description", 'product_size': "Size", - 'product_weighed': "Sold by Weight", - 'department_id': "Department ID", + 'department_name': "Department", 'order_uom': "Order UOM", 'status_code': "Status", } @@ -1035,7 +727,6 @@ class OrderItemView(MasterView): # 'customer_name', 'sequence', 'product_id', - 'local_product', 'pending_product', 'product_scancode', 'product_brand', @@ -1115,46 +806,27 @@ class OrderItemView(MasterView): enum = self.app.enum return enum.ORDER_ITEM_STATUS[value] - def get_instance_title(self, item): - """ """ - enum = self.app.enum - title = str(item) - status = enum.ORDER_ITEM_STATUS[item.status_code] - return f"({status}) {title}" - def configure_form(self, f): """ """ super().configure_form(f) enum = self.app.enum - item = f.model_instance # order f.set_node('order', OrderRef(self.request)) - # local_product - f.set_node('local_product', LocalProductRef(self.request)) - # pending_product - if item.product_id or item.local_product: - f.remove('pending_product') - else: - f.set_node('pending_product', PendingProductRef(self.request)) + f.set_node('pending_product', PendingProductRef(self.request)) # order_qty f.set_node('order_qty', WuttaQuantity(self.request)) # order_uom - f.set_node('order_uom', WuttaDictEnum(self.request, enum.ORDER_UOM)) + # TODO + #f.set_node('order_uom', WuttaEnum(self.request, enum.OrderUOM)) # case_size f.set_node('case_size', WuttaQuantity(self.request)) - # unit_cost - f.set_node('unit_cost', WuttaMoney(self.request, scale=4)) - - # unit_price_reg - f.set_node('unit_price_reg', WuttaMoney(self.request)) - # unit_price_quoted f.set_node('unit_price_quoted', WuttaMoney(self.request)) @@ -1164,21 +836,18 @@ class OrderItemView(MasterView): # total_price f.set_node('total_price', WuttaMoney(self.request)) - # status - f.set_node('status_code', WuttaDictEnum(self.request, enum.ORDER_ITEM_STATUS)) - # paid_amount f.set_node('paid_amount', WuttaMoney(self.request)) def get_xref_buttons(self, item): """ """ buttons = super().get_xref_buttons(item) + model = self.app.model if self.request.has_perm('orders.view'): url = self.request.route_url('orders.view', uuid=item.order_uuid) buttons.append( - self.make_button("View the Order", url=url, - primary=True, icon_left='eye')) + self.make_button("View the Order", primary=True, icon_left='eye', url=url)) return buttons diff --git a/src/sideshow/web/views/products.py b/src/sideshow/web/views/products.py index 98341b7..90b94ab 100644 --- a/src/sideshow/web/views/products.py +++ b/src/sideshow/web/views/products.py @@ -25,194 +25,9 @@ Views for Products """ from wuttaweb.views import MasterView -from wuttaweb.forms.schema import UserRef, WuttaEnum, WuttaMoney, WuttaQuantity +from wuttaweb.forms.schema import UserRef, WuttaEnum, WuttaMoney -from sideshow.db.model import LocalProduct, PendingProduct - - -class LocalProductView(MasterView): - """ - Master view for :class:`~sideshow.db.model.products.LocalProduct`; - route prefix is ``local_products``. - - Notable URLs provided by this class: - - * ``/local/products/`` - * ``/local/products/new`` - * ``/local/products/XXX`` - * ``/local/products/XXX/edit`` - * ``/local/products/XXX/delete`` - """ - model_class = LocalProduct - model_title = "Local Product" - route_prefix = 'local_products' - url_prefix = '/local/products' - - labels = { - 'external_id': "External ID", - 'department_id': "Department ID", - } - - grid_columns = [ - 'scancode', - 'brand_name', - 'description', - 'size', - 'department_name', - 'special_order', - 'case_size', - 'unit_cost', - 'unit_price_reg', - ] - - sort_defaults = 'scancode' - - form_fields = [ - 'external_id', - 'scancode', - 'brand_name', - 'description', - 'size', - 'department_id', - 'department_name', - 'special_order', - 'vendor_name', - 'vendor_item_code', - 'case_size', - 'unit_cost', - 'unit_price_reg', - 'notes', - 'orders', - 'new_order_batches', - ] - - def configure_grid(self, g): - """ """ - super().configure_grid(g) - - # unit_cost - g.set_renderer('unit_cost', 'currency', scale=4) - - # unit_price_reg - g.set_label('unit_price_reg', "Reg. Price", column_only=True) - g.set_renderer('unit_price_reg', 'currency') - - # links - g.set_link('scancode') - g.set_link('brand_name') - g.set_link('description') - g.set_link('size') - - def configure_form(self, f): - """ """ - super().configure_form(f) - enum = self.app.enum - product = f.model_instance - - # external_id - if self.creating: - f.remove('external_id') - else: - f.set_readonly('external_id') - - # TODO: should not have to explicitly mark these nodes - # as required=False.. i guess i do for now b/c i am - # totally overriding the node from colanderlachemy - - # case_size - f.set_node('case_size', WuttaQuantity(self.request)) - f.set_required('case_size', False) - - # unit_cost - f.set_node('unit_cost', WuttaMoney(self.request, scale=4)) - f.set_required('unit_cost', False) - - # unit_price_reg - f.set_node('unit_price_reg', WuttaMoney(self.request)) - f.set_required('unit_price_reg', False) - - # notes - f.set_widget('notes', 'notes') - - # orders - if self.creating or self.editing: - f.remove('orders') - else: - f.set_grid('orders', self.make_orders_grid(product)) - - # new_order_batches - if self.creating or self.editing: - f.remove('new_order_batches') - else: - f.set_grid('new_order_batches', self.make_new_order_batches_grid(product)) - - def make_orders_grid(self, product): - """ - Make and return the grid for the Orders field. - """ - model = self.app.model - route_prefix = self.get_route_prefix() - - orders = set([item.order for item in product.order_items]) - orders = sorted(orders, key=lambda order: order.order_id) - - grid = self.make_grid(key=f'{route_prefix}.view.orders', - model_class=model.Order, - data=orders, - columns=[ - 'order_id', - 'total_price', - 'created', - 'created_by', - ], - labels={ - 'order_id': "Order ID", - }, - renderers={ - 'total_price': 'currency', - }) - - if self.request.has_perm('orders.view'): - url = lambda order, i: self.request.route_url('orders.view', uuid=order.uuid) - grid.add_action('view', icon='eye', url=url) - grid.set_link('order_id') - - return grid - - def make_new_order_batches_grid(self, product): - """ - Make and return the grid for the New Order Batches field. - """ - model = self.app.model - route_prefix = self.get_route_prefix() - - batches = set([row.batch for row in product.new_order_batch_rows]) - batches = sorted(batches, key=lambda batch: batch.id) - - grid = self.make_grid(key=f'{route_prefix}.view.new_order_batches', - model_class=model.NewOrderBatch, - data=batches, - columns=[ - 'id', - 'total_price', - 'created', - 'created_by', - 'executed', - ], - labels={ - 'id': "Batch ID", - 'status_code': "Status", - }, - renderers={ - 'id': 'batch_id', - }) - - if self.request.has_perm('neworder_batches.view'): - url = lambda batch, i: self.request.route_url('neworder_batches.view', uuid=batch.uuid) - grid.add_action('view', icon='eye', url=url) - grid.set_link('id') - - return grid +from sideshow.db.model import PendingProduct class PendingProductView(MasterView): @@ -434,9 +249,6 @@ class PendingProductView(MasterView): def defaults(config, **kwargs): base = globals() - LocalProductView = kwargs.get('LocalProductView', base['LocalProductView']) - LocalProductView.defaults(config) - PendingProductView = kwargs.get('PendingProductView', base['PendingProductView']) PendingProductView.defaults(config) diff --git a/tests/batch/test_neworder.py b/tests/batch/test_neworder.py index 719efcb..66e625e 100644 --- a/tests/batch/test_neworder.py +++ b/tests/batch/test_neworder.py @@ -1,8 +1,6 @@ # -*- coding: utf-8; -*- -import datetime import decimal -from unittest.mock import patch from wuttjamaican.testing import DataTestCase @@ -20,133 +18,61 @@ class TestNewOrderBatchHandler(DataTestCase): def make_handler(self): return mod.NewOrderBatchHandler(self.config) - def tets_use_local_customers(self): - handler = self.make_handler() - - # true by default - self.assertTrue(handler.use_local_customers()) - - # config can disable - config.setdefault('sideshow.orders.use_local_customers', 'false') - self.assertFalse(handler.use_local_customers()) - - def tets_use_local_products(self): - handler = self.make_handler() - - # true by default - self.assertTrue(handler.use_local_products()) - - # config can disable - config.setdefault('sideshow.orders.use_local_products', 'false') - self.assertFalse(handler.use_local_products()) - - def tets_allow_unknown_products(self): - handler = self.make_handler() - - # true by default - self.assertTrue(handler.allow_unknown_products()) - - # config can disable - config.setdefault('sideshow.orders.allow_unknown_products', 'false') - self.assertFalse(handler.allow_unknown_products()) - - def test_set_customer(self): + def test_set_pending_customer(self): model = self.app.model handler = self.make_handler() user = model.User(username='barney') self.session.add(user) - # customer starts blank - batch = handler.make_batch(self.session, created_by=user) - self.session.add(batch) - self.session.flush() - self.assertIsNone(batch.customer_id) - self.assertIsNone(batch.local_customer) + batch = handler.make_batch(self.session, created_by=user, customer_id=42) + self.assertEqual(batch.customer_id, 42) self.assertIsNone(batch.pending_customer) self.assertIsNone(batch.customer_name) self.assertIsNone(batch.phone_number) self.assertIsNone(batch.email_address) - # pending, typical (nb. full name is automatic) - handler.set_customer(batch, { + # auto full_name + handler.set_pending_customer(batch, { 'first_name': "Fred", 'last_name': "Flintstone", 'phone_number': '555-1234', 'email_address': 'fred@mailinator.com', }) self.assertIsNone(batch.customer_id) - self.assertIsNone(batch.local_customer) self.assertIsInstance(batch.pending_customer, model.PendingCustomer) customer = batch.pending_customer + self.assertEqual(customer.full_name, "Fred Flintstone") self.assertEqual(customer.first_name, "Fred") self.assertEqual(customer.last_name, "Flintstone") - self.assertEqual(customer.full_name, "Fred Flintstone") self.assertEqual(customer.phone_number, '555-1234') self.assertEqual(customer.email_address, 'fred@mailinator.com') self.assertEqual(batch.customer_name, "Fred Flintstone") self.assertEqual(batch.phone_number, '555-1234') self.assertEqual(batch.email_address, 'fred@mailinator.com') - # pending, minimal - last_customer = customer # save ref to prev record - handler.set_customer(batch, {'full_name': "Wilma Flintstone"}) + # explicit full_name + batch = handler.make_batch(self.session, created_by=user, customer_id=42) + handler.set_pending_customer(batch, { + 'full_name': "Freddy Flintstone", + 'first_name': "Fred", + 'last_name': "Flintstone", + 'phone_number': '555-1234', + 'email_address': 'fred@mailinator.com', + }) self.assertIsNone(batch.customer_id) - self.assertIsNone(batch.local_customer) self.assertIsInstance(batch.pending_customer, model.PendingCustomer) customer = batch.pending_customer - self.assertIs(customer, last_customer) - self.assertEqual(customer.full_name, "Wilma Flintstone") - self.assertIsNone(customer.first_name) - self.assertIsNone(customer.last_name) - self.assertIsNone(customer.phone_number) - self.assertIsNone(customer.email_address) - self.assertEqual(batch.customer_name, "Wilma Flintstone") - self.assertIsNone(batch.phone_number) - self.assertIsNone(batch.email_address) + self.assertEqual(customer.full_name, "Freddy Flintstone") + self.assertEqual(customer.first_name, "Fred") + self.assertEqual(customer.last_name, "Flintstone") + self.assertEqual(customer.phone_number, '555-1234') + self.assertEqual(customer.email_address, 'fred@mailinator.com') + self.assertEqual(batch.customer_name, "Freddy Flintstone") + self.assertEqual(batch.phone_number, '555-1234') + self.assertEqual(batch.email_address, 'fred@mailinator.com') - # local customer - local = model.LocalCustomer(full_name="Bam Bam", - first_name="Bam", last_name="Bam", - phone_number='555-4321') - self.session.add(local) - self.session.flush() - handler.set_customer(batch, local.uuid.hex) - self.session.flush() - self.assertIsNone(batch.customer_id) - # nb. pending customer does not get removed - self.assertIsInstance(batch.pending_customer, model.PendingCustomer) - self.assertIsInstance(batch.local_customer, model.LocalCustomer) - customer = batch.local_customer - self.assertEqual(customer.full_name, "Bam Bam") - self.assertEqual(customer.first_name, "Bam") - self.assertEqual(customer.last_name, "Bam") - self.assertEqual(customer.phone_number, '555-4321') - self.assertIsNone(customer.email_address) - self.assertEqual(batch.customer_name, "Bam Bam") - self.assertEqual(batch.phone_number, '555-4321') - self.assertIsNone(batch.email_address) - - # local customer, not found - mock_uuid = self.app.make_true_uuid() - self.assertRaises(ValueError, handler.set_customer, batch, mock_uuid.hex) - - # external lookup not implemented - self.config.setdefault('sideshow.orders.use_local_customers', 'false') - self.assertRaises(NotImplementedError, handler.set_customer, batch, '42') - - # null - handler.set_customer(batch, None) - self.session.flush() - self.assertIsNone(batch.customer_id) - # nb. pending customer does not get removed - self.assertIsInstance(batch.pending_customer, model.PendingCustomer) - self.assertIsNone(batch.local_customer) - self.assertIsNone(batch.customer_name) - self.assertIsNone(batch.phone_number) - self.assertIsNone(batch.email_address) - - def test_add_item(self): + def test_add_pending_product(self): model = self.app.model enum = self.app.enum handler = self.make_handler() @@ -158,115 +84,42 @@ class TestNewOrderBatchHandler(DataTestCase): self.session.add(batch) self.assertEqual(len(batch.rows), 0) - # pending, typical kw = dict( - scancode='07430500001', + scancode='07430500132', brand_name='Bragg', description='Vinegar', - size='1oz', + size='32oz', case_size=12, - unit_cost=decimal.Decimal('1.99'), - unit_price_reg=decimal.Decimal('2.99'), + unit_cost=decimal.Decimal('3.99'), + unit_price_reg=decimal.Decimal('5.99'), + created_by=user, ) - row = handler.add_item(batch, kw, 1, enum.ORDER_UOM_UNIT) - # nb. this is the first row in batch + row = handler.add_pending_product(batch, kw, 1, enum.ORDER_UOM_UNIT) self.assertEqual(len(batch.rows), 1) self.assertIs(batch.rows[0], row) - self.assertIsNone(row.product_id) - self.assertIsNone(row.local_product) - product = row.pending_product - self.assertIsInstance(product, model.PendingProduct) - self.assertEqual(product.scancode, '07430500001') - self.assertEqual(product.brand_name, 'Bragg') - self.assertEqual(product.description, 'Vinegar') - self.assertEqual(product.size, '1oz') - self.assertEqual(product.case_size, 12) - self.assertEqual(product.unit_cost, decimal.Decimal('1.99')) - self.assertEqual(product.unit_price_reg, decimal.Decimal('2.99')) - self.assertEqual(row.product_scancode, '07430500001') + + self.assertEqual(row.product_scancode, '07430500132') self.assertEqual(row.product_brand, 'Bragg') self.assertEqual(row.product_description, 'Vinegar') - self.assertEqual(row.product_size, '1oz') + self.assertEqual(row.product_size, '32oz') self.assertEqual(row.case_size, 12) - self.assertEqual(row.unit_cost, decimal.Decimal('1.99')) - self.assertEqual(row.unit_price_reg, decimal.Decimal('2.99')) - self.assertEqual(row.unit_price_quoted, decimal.Decimal('2.99')) - self.assertEqual(row.case_price_quoted, decimal.Decimal('35.88')) - self.assertEqual(row.total_price, decimal.Decimal('2.99')) + self.assertEqual(row.unit_cost, decimal.Decimal('3.99')) + self.assertEqual(row.unit_price_reg, decimal.Decimal('5.99')) + self.assertEqual(row.unit_price_quoted, decimal.Decimal('5.99')) + self.assertEqual(row.case_price_quoted, decimal.Decimal('71.88')) - # pending, minimal - row = handler.add_item(batch, {'description': "Tangerines"}, 1, enum.ORDER_UOM_UNIT) - self.assertIsNone(row.product_id) - self.assertIsNone(row.local_product) product = row.pending_product self.assertIsInstance(product, model.PendingProduct) - self.assertIsNone(product.scancode) - self.assertIsNone(product.brand_name) - self.assertEqual(product.description, 'Tangerines') - self.assertIsNone(product.size) - self.assertIsNone(product.case_size) - self.assertIsNone(product.unit_cost) - self.assertIsNone(product.unit_price_reg) - self.assertIsNone(row.product_scancode) - self.assertIsNone(row.product_brand) - self.assertEqual(row.product_description, 'Tangerines') - self.assertIsNone(row.product_size) - self.assertIsNone(row.case_size) - self.assertIsNone(row.unit_cost) - self.assertIsNone(row.unit_price_reg) - self.assertIsNone(row.unit_price_quoted) - self.assertIsNone(row.case_price_quoted) - self.assertIsNone(row.total_price) - - # error if unknown products not allowed - self.config.setdefault('sideshow.orders.allow_unknown_products', 'false') - self.assertRaises(TypeError, handler.add_item, batch, kw, 1, enum.ORDER_UOM_UNIT) - - # local product - local = model.LocalProduct(scancode='07430500002', - description='Vinegar', - size='2oz', - unit_price_reg=2.99, - case_size=12) - self.session.add(local) - self.session.flush() - row = handler.add_item(batch, local.uuid.hex, 1, enum.ORDER_UOM_CASE) - self.session.flush() - self.session.refresh(row) - self.session.refresh(local) - self.assertIsNone(row.product_id) - self.assertIsNone(row.pending_product) - product = row.local_product - self.assertIsInstance(product, model.LocalProduct) - self.assertEqual(product.scancode, '07430500002') - self.assertIsNone(product.brand_name) + self.assertEqual(product.scancode, '07430500132') + self.assertEqual(product.brand_name, 'Bragg') self.assertEqual(product.description, 'Vinegar') - self.assertEqual(product.size, '2oz') + self.assertEqual(product.size, '32oz') self.assertEqual(product.case_size, 12) - self.assertIsNone(product.unit_cost) - self.assertEqual(product.unit_price_reg, decimal.Decimal('2.99')) - self.assertEqual(row.product_scancode, '07430500002') - self.assertIsNone(row.product_brand) - self.assertEqual(row.product_description, 'Vinegar') - self.assertEqual(row.product_size, '2oz') - self.assertEqual(row.case_size, 12) - self.assertIsNone(row.unit_cost) - self.assertEqual(row.unit_price_reg, decimal.Decimal('2.99')) - self.assertEqual(row.unit_price_quoted, decimal.Decimal('2.99')) - self.assertEqual(row.case_price_quoted, decimal.Decimal('35.88')) - self.assertEqual(row.total_price, decimal.Decimal('35.88')) + self.assertEqual(product.unit_cost, decimal.Decimal('3.99')) + self.assertEqual(product.unit_price_reg, decimal.Decimal('5.99')) + self.assertIs(product.created_by, user) - # local product, not found - mock_uuid = self.app.make_true_uuid() - self.assertRaises(ValueError, handler.add_item, - batch, mock_uuid.hex, 1, enum.ORDER_UOM_CASE) - - # external lookup not implemented - self.config.setdefault('sideshow.orders.use_local_products', 'false') - self.assertRaises(NotImplementedError, handler.add_item, - batch, '42', 1, enum.ORDER_UOM_CASE) - - def test_update_item(self): + def test_set_pending_product(self): model = self.app.model enum = self.app.enum handler = self.make_handler() @@ -278,170 +131,84 @@ class TestNewOrderBatchHandler(DataTestCase): self.session.add(batch) self.assertEqual(len(batch.rows), 0) - # start with typical pending product - kw = dict( - scancode='07430500001', - brand_name='Bragg', - description='Vinegar', - size='1oz', - case_size=12, - unit_cost=decimal.Decimal('1.99'), - unit_price_reg=decimal.Decimal('2.99'), - ) - row = handler.add_item(batch, kw, 1, enum.ORDER_UOM_CASE) - self.assertIsNone(row.product_id) - self.assertIsNone(row.local_product) - product = row.pending_product - self.assertIsInstance(product, model.PendingProduct) - self.assertEqual(product.scancode, '07430500001') - self.assertEqual(product.brand_name, 'Bragg') - self.assertEqual(product.description, 'Vinegar') - self.assertEqual(product.size, '1oz') - self.assertEqual(product.case_size, 12) - self.assertEqual(product.unit_cost, decimal.Decimal('1.99')) - self.assertEqual(product.unit_price_reg, decimal.Decimal('2.99')) - self.assertEqual(row.product_scancode, '07430500001') - self.assertEqual(row.product_brand, 'Bragg') - self.assertEqual(row.product_description, 'Vinegar') - self.assertEqual(row.product_size, '1oz') - self.assertEqual(row.case_size, 12) - self.assertEqual(row.unit_cost, decimal.Decimal('1.99')) - self.assertEqual(row.unit_price_reg, decimal.Decimal('2.99')) - self.assertEqual(row.unit_price_quoted, decimal.Decimal('2.99')) - self.assertEqual(row.case_price_quoted, decimal.Decimal('35.88')) - self.assertEqual(row.order_qty, 1) - self.assertEqual(row.order_uom, enum.ORDER_UOM_CASE) - self.assertEqual(row.total_price, decimal.Decimal('35.88')) - - # set pending, minimal - handler.update_item(row, {'description': 'Vinegar'}, 1, enum.ORDER_UOM_UNIT) - # self.session.flush() - self.assertIsNone(row.product_id) - self.assertIsNone(row.local_product) - product = row.pending_product - self.assertIsInstance(product, model.PendingProduct) - self.assertIsNone(product.scancode) - self.assertIsNone(product.brand_name) - self.assertEqual(product.description, 'Vinegar') - self.assertIsNone(product.size) - self.assertIsNone(product.case_size) - self.assertIsNone(product.unit_cost) - self.assertIsNone(product.unit_price_reg) + # start with mock product_id + row = handler.make_row(product_id=42, order_qty=1, order_uom=enum.ORDER_UOM_UNIT) + handler.add_row(batch, row) + self.session.flush() + self.assertEqual(row.product_id, 42) + self.assertIsNone(row.pending_product) self.assertIsNone(row.product_scancode) self.assertIsNone(row.product_brand) - self.assertEqual(row.product_description, 'Vinegar') + self.assertIsNone(row.product_description) self.assertIsNone(row.product_size) self.assertIsNone(row.case_size) self.assertIsNone(row.unit_cost) self.assertIsNone(row.unit_price_reg) self.assertIsNone(row.unit_price_quoted) - self.assertIsNone(row.case_price_quoted) - self.assertEqual(row.order_qty, 1) - self.assertEqual(row.order_uom, enum.ORDER_UOM_UNIT) - self.assertIsNone(row.total_price) - # start over, new row w/ local product - local = model.LocalProduct(scancode='07430500002', - description='Vinegar', - size='2oz', - unit_price_reg=3.99, - case_size=12) - self.session.add(local) + # set pending, which clears product_id + handler.set_pending_product(row, dict( + scancode='07430500132', + brand_name='Bragg', + description='Vinegar', + size='32oz', + case_size=12, + unit_cost=decimal.Decimal('3.99'), + unit_price_reg=decimal.Decimal('5.99'), + created_by=user, + )) self.session.flush() - row = handler.add_item(batch, local.uuid.hex, 1, enum.ORDER_UOM_CASE) - self.session.flush() - self.session.refresh(row) - self.session.refresh(local) self.assertIsNone(row.product_id) - self.assertIsNone(row.pending_product) - product = row.local_product - self.assertIsInstance(product, model.LocalProduct) - self.assertEqual(product.scancode, '07430500002') - self.assertIsNone(product.brand_name) - self.assertEqual(product.description, 'Vinegar') - self.assertEqual(product.size, '2oz') - self.assertEqual(product.case_size, 12) - self.assertIsNone(product.unit_cost) - self.assertEqual(product.unit_price_reg, decimal.Decimal('3.99')) - self.assertEqual(row.product_scancode, '07430500002') - self.assertIsNone(row.product_brand) - self.assertEqual(row.product_description, 'Vinegar') - self.assertEqual(row.product_size, '2oz') - self.assertEqual(row.case_size, 12) - self.assertIsNone(row.unit_cost) - self.assertEqual(row.unit_price_reg, decimal.Decimal('3.99')) - self.assertEqual(row.unit_price_quoted, decimal.Decimal('3.99')) - self.assertEqual(row.case_price_quoted, decimal.Decimal('47.88')) - self.assertEqual(row.order_qty, 1) - self.assertEqual(row.order_uom, enum.ORDER_UOM_CASE) - self.assertEqual(row.total_price, decimal.Decimal('47.88')) - - # update w/ pending product - handler.update_item(row, kw, 2, enum.ORDER_UOM_CASE) - self.assertIsNone(row.product_id) - self.assertIsNone(row.local_product) - product = row.pending_product - self.assertIsInstance(product, model.PendingProduct) - self.assertEqual(product.scancode, '07430500001') - self.assertEqual(product.brand_name, 'Bragg') - self.assertEqual(product.description, 'Vinegar') - self.assertEqual(product.size, '1oz') - self.assertEqual(product.case_size, 12) - self.assertEqual(product.unit_cost, decimal.Decimal('1.99')) - self.assertEqual(product.unit_price_reg, decimal.Decimal('2.99')) - self.assertEqual(row.product_scancode, '07430500001') + self.assertIsInstance(row.pending_product, model.PendingProduct) + self.assertEqual(row.product_scancode, '07430500132') self.assertEqual(row.product_brand, 'Bragg') self.assertEqual(row.product_description, 'Vinegar') - self.assertEqual(row.product_size, '1oz') + self.assertEqual(row.product_size, '32oz') self.assertEqual(row.case_size, 12) - self.assertEqual(row.unit_cost, decimal.Decimal('1.99')) - self.assertEqual(row.unit_price_reg, decimal.Decimal('2.99')) - self.assertEqual(row.unit_price_quoted, decimal.Decimal('2.99')) - self.assertEqual(row.case_price_quoted, decimal.Decimal('35.88')) - self.assertEqual(row.order_qty, 2) - self.assertEqual(row.order_uom, enum.ORDER_UOM_CASE) - self.assertEqual(row.total_price, decimal.Decimal('71.76')) - - # update w/ pending, error if not allowed - self.config.setdefault('sideshow.orders.allow_unknown_products', 'false') - self.assertRaises(TypeError, handler.update_item, row, kw, 1, enum.ORDER_UOM_UNIT) - - # update w/ local product - handler.update_item(row, local.uuid.hex, 1, enum.ORDER_UOM_CASE) - self.assertIsNone(row.product_id) - # nb. pending remains intact here - self.assertIsNotNone(row.pending_product) - product = row.local_product - self.assertIsInstance(product, model.LocalProduct) - self.assertEqual(product.scancode, '07430500002') - self.assertIsNone(product.brand_name) + self.assertEqual(row.unit_cost, decimal.Decimal('3.99')) + self.assertEqual(row.unit_price_reg, decimal.Decimal('5.99')) + self.assertEqual(row.unit_price_quoted, decimal.Decimal('5.99')) + self.assertEqual(row.case_price_quoted, decimal.Decimal('71.88')) + product = row.pending_product + self.assertIsInstance(product, model.PendingProduct) + self.assertEqual(product.scancode, '07430500132') + self.assertEqual(product.brand_name, 'Bragg') self.assertEqual(product.description, 'Vinegar') - self.assertEqual(product.size, '2oz') + self.assertEqual(product.size, '32oz') self.assertEqual(product.case_size, 12) - self.assertIsNone(product.unit_cost) - self.assertEqual(product.unit_price_reg, decimal.Decimal('3.99')) - self.assertEqual(row.product_scancode, '07430500002') - self.assertIsNone(row.product_brand) + self.assertEqual(product.unit_cost, decimal.Decimal('3.99')) + self.assertEqual(product.unit_price_reg, decimal.Decimal('5.99')) + self.assertIs(product.created_by, user) + + # set again to update pending + handler.set_pending_product(row, dict( + scancode='07430500116', + size='16oz', + unit_cost=decimal.Decimal('2.19'), + unit_price_reg=decimal.Decimal('3.59'), + )) + self.session.flush() + self.assertIsNone(row.product_id) + self.assertIsInstance(row.pending_product, model.PendingProduct) + self.assertEqual(row.product_scancode, '07430500116') + self.assertEqual(row.product_brand, 'Bragg') self.assertEqual(row.product_description, 'Vinegar') - self.assertEqual(row.product_size, '2oz') + self.assertEqual(row.product_size, '16oz') self.assertEqual(row.case_size, 12) - self.assertIsNone(row.unit_cost) - self.assertEqual(row.unit_price_reg, decimal.Decimal('3.99')) - self.assertEqual(row.unit_price_quoted, decimal.Decimal('3.99')) - self.assertEqual(row.case_price_quoted, decimal.Decimal('47.88')) - self.assertEqual(row.order_qty, 1) - self.assertEqual(row.order_uom, enum.ORDER_UOM_CASE) - self.assertEqual(row.total_price, decimal.Decimal('47.88')) - - # update w/ local, not found - mock_uuid = self.app.make_true_uuid() - self.assertRaises(ValueError, handler.update_item, - batch, mock_uuid.hex, 1, enum.ORDER_UOM_CASE) - - # external lookup not implemented - self.config.setdefault('sideshow.orders.use_local_products', 'false') - self.assertRaises(NotImplementedError, handler.update_item, - row, '42', 1, enum.ORDER_UOM_CASE) + self.assertEqual(row.unit_cost, decimal.Decimal('2.19')) + self.assertEqual(row.unit_price_reg, decimal.Decimal('3.59')) + self.assertEqual(row.unit_price_quoted, decimal.Decimal('3.59')) + self.assertEqual(row.case_price_quoted, decimal.Decimal('43.08')) + product = row.pending_product + self.assertIsInstance(product, model.PendingProduct) + self.assertEqual(product.scancode, '07430500116') + self.assertEqual(product.brand_name, 'Bragg') + self.assertEqual(product.description, 'Vinegar') + self.assertEqual(product.size, '16oz') + self.assertEqual(product.case_size, 12) + self.assertEqual(product.unit_cost, decimal.Decimal('2.19')) + self.assertEqual(product.unit_price_reg, decimal.Decimal('3.59')) + self.assertIs(product.created_by, user) def test_refresh_row(self): model = self.app.model @@ -543,37 +310,6 @@ class TestNewOrderBatchHandler(DataTestCase): self.assertEqual(row.case_price_quoted, decimal.Decimal('71.88')) self.assertEqual(row.total_price, decimal.Decimal('143.76')) - # refreshed from pending product (sale price) - product = model.PendingProduct(scancode='07430500132', - brand_name='Bragg', - description='Vinegar', - size='32oz', - case_size=12, - unit_cost=decimal.Decimal('3.99'), - unit_price_reg=decimal.Decimal('5.99'), - created_by=user, - status=enum.PendingProductStatus.PENDING) - row = handler.make_row(pending_product=product, order_qty=2, order_uom=enum.ORDER_UOM_CASE, - unit_price_sale=decimal.Decimal('5.19'), - sale_ends=datetime.datetime(2099, 1, 1)) - self.assertIsNone(row.status_code) - handler.add_row(batch, row) - self.assertEqual(row.status_code, row.STATUS_OK) - self.assertIsNone(row.product_id) - self.assertIs(row.pending_product, product) - self.assertEqual(row.product_scancode, '07430500132') - self.assertEqual(row.product_brand, 'Bragg') - self.assertEqual(row.product_description, 'Vinegar') - self.assertEqual(row.product_size, '32oz') - self.assertEqual(row.case_size, 12) - self.assertEqual(row.unit_cost, decimal.Decimal('3.99')) - self.assertEqual(row.unit_price_reg, decimal.Decimal('5.99')) - self.assertEqual(row.unit_price_sale, decimal.Decimal('5.19')) - self.assertEqual(row.sale_ends, datetime.datetime(2099, 1, 1)) - self.assertEqual(row.unit_price_quoted, decimal.Decimal('5.19')) - self.assertEqual(row.case_price_quoted, decimal.Decimal('62.28')) - self.assertEqual(row.total_price, decimal.Decimal('124.56')) - def test_remove_row(self): model = self.app.model enum = self.app.enum @@ -596,7 +332,7 @@ class TestNewOrderBatchHandler(DataTestCase): unit_price_reg=decimal.Decimal('5.99'), created_by=user, ) - row = handler.add_item(batch, kw, 1, enum.ORDER_UOM_CASE) + row = handler.add_pending_product(batch, kw, 1, enum.ORDER_UOM_CASE) self.session.add(row) self.session.flush() self.assertEqual(batch.row_count, 1) @@ -632,70 +368,25 @@ class TestNewOrderBatchHandler(DataTestCase): self.assertNotIn(batch, self.session) self.assertEqual(self.session.query(model.PendingCustomer).count(), 0) - # make new pending customer, assigned to batch + order - customer = model.PendingCustomer(full_name="Wilma Flintstone", + # make new pending customer + customer = model.PendingCustomer(full_name="Fred Flintstone", status=enum.PendingCustomerStatus.PENDING, created_by=user) self.session.add(customer) - batch = handler.make_batch(self.session, created_by=user, pending_customer=customer) - self.session.add(batch) - order = model.Order(order_id=77, created_by=user, pending_customer=customer) - self.session.add(order) - self.session.flush() - # deleting batch will *not* delete pending customer - self.assertIn(batch, self.session) - self.assertEqual(self.session.query(model.PendingCustomer).count(), 1) - handler.do_delete(batch, user) + # make 2 batches with same pending customer + batch1 = handler.make_batch(self.session, created_by=user, pending_customer=customer) + batch2 = handler.make_batch(self.session, created_by=user, pending_customer=customer) + self.session.add(batch1) + self.session.add(batch2) self.session.commit() - self.assertNotIn(batch, self.session) + + # deleting 1 will not delete pending customer self.assertEqual(self.session.query(model.PendingCustomer).count(), 1) - - # make new pending product, associate w/ batch + order - batch = handler.make_batch(self.session, created_by=user) - self.session.add(batch) - handler.set_customer(batch, {'full_name': "Jack Black"}) - row = handler.add_item(batch, dict( - scancode='07430500132', - brand_name='Bragg', - description='Vinegar', - size='32oz', - unit_price_reg=5.99, - ), 1, enum.ORDER_UOM_UNIT) - product = row.pending_product - order = model.Order(order_id=33, created_by=user) - item = model.OrderItem(pending_product=product, order_qty=1, order_uom=enum.ORDER_UOM_UNIT, - status_code=enum.ORDER_ITEM_STATUS_INITIATED) - order.items.append(item) - self.session.add(order) - self.session.flush() - - # deleting batch will *not* delete pending product - self.assertIn(batch, self.session) - self.assertEqual(self.session.query(model.PendingProduct).count(), 1) - handler.do_delete(batch, user) + handler.do_delete(batch1, user) self.session.commit() - self.assertNotIn(batch, self.session) self.assertEqual(self.session.query(model.PendingCustomer).count(), 1) - - # make another batch w/ same pending product - batch = handler.make_batch(self.session, created_by=user) - self.session.add(batch) - row = handler.make_row(pending_product=product, - order_qty=1, order_uom=enum.ORDER_UOM_UNIT) - handler.add_row(batch, row) - - # also delete the associated order - self.session.delete(order) - self.session.flush() - - # deleting this batch *will* delete pending product - self.assertIn(batch, self.session) - self.assertEqual(self.session.query(model.PendingProduct).count(), 1) - handler.do_delete(batch, user) - self.session.commit() - self.assertNotIn(batch, self.session) - self.assertEqual(self.session.query(model.PendingProduct).count(), 0) + self.assertIs(batch2.pending_customer, customer) def test_get_effective_rows(self): model = self.app.model @@ -746,12 +437,6 @@ class TestNewOrderBatchHandler(DataTestCase): self.assertEqual(reason, "Must assign the customer") batch.customer_id = 42 - batch.customer_name = "Fred Flintstone" - - reason = handler.why_not_execute(batch) - self.assertEqual(reason, "Customer phone number is required") - - batch.phone_number = '555-1234' reason = handler.why_not_execute(batch) self.assertEqual(reason, "Must add at least one valid item") @@ -766,206 +451,13 @@ class TestNewOrderBatchHandler(DataTestCase): unit_price_reg=decimal.Decimal('5.99'), created_by=user, ) - row = handler.add_item(batch, kw, 1, enum.ORDER_UOM_CASE) + row = handler.add_pending_product(batch, kw, 1, enum.ORDER_UOM_CASE) self.session.add(row) self.session.flush() reason = handler.why_not_execute(batch) self.assertIsNone(reason) - def test_make_local_customer(self): - model = self.app.model - enum = self.app.enum - handler = self.make_handler() - - user = model.User(username='barney') - self.session.add(user) - - # make a typical batch - batch = handler.make_batch(self.session, created_by=user) - self.session.add(batch) - handler.set_customer(batch, {'first_name': "John", 'last_name': "Doe", - 'phone_number': '555-1234'}) - row = handler.add_item(batch, dict( - scancode='07430500132', - brand_name='Bragg', - description='Vinegar', - size='32oz', - unit_price_reg=5.99, - ), 1, enum.ORDER_UOM_UNIT) - self.session.flush() - - # making local customer removes pending customer - self.assertEqual(self.session.query(model.PendingCustomer).count(), 1) - self.assertEqual(self.session.query(model.LocalCustomer).count(), 0) - self.assertIsNotNone(batch.pending_customer) - self.assertIsNone(batch.local_customer) - handler.make_local_customer(batch) - self.assertEqual(self.session.query(model.PendingCustomer).count(), 0) - self.assertEqual(self.session.query(model.LocalCustomer).count(), 1) - self.assertIsNone(batch.pending_customer) - local = batch.local_customer - self.assertIsNotNone(local) - self.assertEqual(local.first_name, "John") - self.assertEqual(local.last_name, "Doe") - self.assertEqual(local.full_name, "John Doe") - self.assertEqual(local.phone_number, '555-1234') - - # trying again does nothing - handler.make_local_customer(batch) - self.assertEqual(self.session.query(model.PendingCustomer).count(), 0) - self.assertEqual(self.session.query(model.LocalCustomer).count(), 1) - self.assertIsNone(batch.pending_customer) - local = batch.local_customer - self.assertIsNotNone(local) - self.assertEqual(local.first_name, "John") - self.assertEqual(local.last_name, "Doe") - self.assertEqual(local.full_name, "John Doe") - self.assertEqual(local.phone_number, '555-1234') - - # make another typical batch - batch = handler.make_batch(self.session, created_by=user) - self.session.add(batch) - handler.set_customer(batch, {'first_name': "Chuck", 'last_name': "Norris", - 'phone_number': '555-1234'}) - row = handler.add_item(batch, dict( - scancode='07430500132', - brand_name='Bragg', - description='Vinegar', - size='32oz', - unit_price_reg=5.99, - ), 1, enum.ORDER_UOM_UNIT) - self.session.flush() - - # should do nothing if local customers disabled - with patch.object(handler, 'use_local_customers', return_value=False): - self.assertEqual(self.session.query(model.PendingCustomer).count(), 1) - self.assertEqual(self.session.query(model.LocalCustomer).count(), 1) - self.assertIsNotNone(batch.pending_customer) - self.assertIsNone(batch.local_customer) - handler.make_local_customer(batch) - self.assertEqual(self.session.query(model.PendingCustomer).count(), 1) - self.assertEqual(self.session.query(model.LocalCustomer).count(), 1) - self.assertIsNotNone(batch.pending_customer) - self.assertIsNone(batch.local_customer) - - # but things happen by default, since local customers enabled - self.assertEqual(self.session.query(model.PendingCustomer).count(), 1) - self.assertEqual(self.session.query(model.LocalCustomer).count(), 1) - self.assertIsNotNone(batch.pending_customer) - self.assertIsNone(batch.local_customer) - handler.make_local_customer(batch) - self.assertEqual(self.session.query(model.PendingCustomer).count(), 0) - self.assertEqual(self.session.query(model.LocalCustomer).count(), 2) - self.assertIsNone(batch.pending_customer) - local = batch.local_customer - self.assertIsNotNone(local) - self.assertEqual(local.first_name, "Chuck") - self.assertEqual(local.last_name, "Norris") - self.assertEqual(local.full_name, "Chuck Norris") - self.assertEqual(local.phone_number, '555-1234') - - def test_make_local_products(self): - model = self.app.model - enum = self.app.enum - handler = self.make_handler() - - user = model.User(username='barney') - self.session.add(user) - - # make a batch w/ one each local + pending products - batch = handler.make_batch(self.session, created_by=user) - self.session.add(batch) - handler.set_customer(batch, {'full_name': "John Doe"}) - local = model.LocalProduct(scancode='07430500116', - brand_name='Bragg', - description='Vinegar', - size='16oz', - unit_price_reg=3.59) - self.session.add(local) - self.session.flush() - row1 = handler.add_item(batch, local.uuid.hex, 1, enum.ORDER_UOM_UNIT) - row2 = handler.add_item(batch, dict( - scancode='07430500132', - brand_name='Bragg', - description='Vinegar', - size='32oz', - unit_price_reg=5.99, - ), 1, enum.ORDER_UOM_UNIT) - self.session.flush() - - # making local product removes pending product - self.assertEqual(self.session.query(model.PendingProduct).count(), 1) - self.assertEqual(self.session.query(model.LocalProduct).count(), 1) - self.assertIsNotNone(row2.pending_product) - self.assertIsNone(row2.local_product) - handler.make_local_products(batch, batch.rows) - self.assertEqual(self.session.query(model.PendingProduct).count(), 0) - self.assertEqual(self.session.query(model.LocalProduct).count(), 2) - self.assertIsNone(row2.pending_product) - local = row2.local_product - self.assertIsNotNone(local) - self.assertEqual(local.scancode, '07430500132') - self.assertEqual(local.brand_name, 'Bragg') - self.assertEqual(local.description, 'Vinegar') - self.assertEqual(local.size, '32oz') - self.assertEqual(local.unit_price_reg, decimal.Decimal('5.99')) - - # trying again does nothing - handler.make_local_products(batch, batch.rows) - self.assertEqual(self.session.query(model.PendingProduct).count(), 0) - self.assertEqual(self.session.query(model.LocalProduct).count(), 2) - self.assertIsNone(row2.pending_product) - local = row2.local_product - self.assertIsNotNone(local) - self.assertEqual(local.scancode, '07430500132') - self.assertEqual(local.brand_name, 'Bragg') - self.assertEqual(local.description, 'Vinegar') - self.assertEqual(local.size, '32oz') - self.assertEqual(local.unit_price_reg, decimal.Decimal('5.99')) - - # make another typical batch - batch = handler.make_batch(self.session, created_by=user) - self.session.add(batch) - handler.set_customer(batch, {'full_name': "Chuck Norris"}) - row = handler.add_item(batch, dict( - scancode='07430500164', - brand_name='Bragg', - description='Vinegar', - size='64oz', - unit_price_reg=9.99, - ), 1, enum.ORDER_UOM_UNIT) - self.session.flush() - - # should do nothing if local products disabled - with patch.object(handler, 'use_local_products', return_value=False): - self.assertEqual(self.session.query(model.PendingProduct).count(), 1) - self.assertEqual(self.session.query(model.LocalProduct).count(), 2) - self.assertIsNotNone(row.pending_product) - self.assertIsNone(row.local_product) - handler.make_local_products(batch, batch.rows) - self.assertEqual(self.session.query(model.PendingProduct).count(), 1) - self.assertEqual(self.session.query(model.LocalProduct).count(), 2) - self.assertIsNotNone(row.pending_product) - self.assertIsNone(row.local_product) - - # but things happen by default, since local products enabled - self.assertEqual(self.session.query(model.PendingProduct).count(), 1) - self.assertEqual(self.session.query(model.LocalProduct).count(), 2) - self.assertIsNotNone(row.pending_product) - self.assertIsNone(row.local_product) - handler.make_local_products(batch, batch.rows) - self.assertEqual(self.session.query(model.PendingProduct).count(), 0) - self.assertEqual(self.session.query(model.LocalProduct).count(), 3) - self.assertIsNone(row.pending_product) - local = row.local_product - self.assertIsNotNone(local) - self.assertEqual(local.scancode, '07430500164') - self.assertEqual(local.brand_name, 'Bragg') - self.assertEqual(local.description, 'Vinegar') - self.assertEqual(local.size, '64oz') - self.assertEqual(local.unit_price_reg, decimal.Decimal('9.99')) - def test_make_new_order(self): model = self.app.model enum = self.app.enum @@ -987,7 +479,7 @@ class TestNewOrderBatchHandler(DataTestCase): unit_price_reg=decimal.Decimal('5.99'), created_by=user, ) - row = handler.add_item(batch, kw, 1, enum.ORDER_UOM_CASE) + row = handler.add_pending_product(batch, kw, 1, enum.ORDER_UOM_CASE) self.session.add(row) self.session.flush() @@ -1027,7 +519,7 @@ class TestNewOrderBatchHandler(DataTestCase): unit_price_reg=decimal.Decimal('5.99'), created_by=user, ) - row = handler.add_item(batch, kw, 1, enum.ORDER_UOM_CASE) + row = handler.add_pending_product(batch, kw, 1, enum.ORDER_UOM_CASE) self.session.add(row) self.session.flush() diff --git a/tests/db/model/test_orders.py b/tests/db/model/test_orders.py index 7169991..b0ad9f4 100644 --- a/tests/db/model/test_orders.py +++ b/tests/db/model/test_orders.py @@ -19,19 +19,6 @@ class TestOrder(DataTestCase): class TestOrderItem(DataTestCase): - def test_full_description(self): - - item = mod.OrderItem() - self.assertEqual(item.full_description, "") - - item = mod.OrderItem(product_description="Vinegar") - self.assertEqual(item.full_description, "Vinegar") - - item = mod.OrderItem(product_brand='Bragg', - product_description='Vinegar', - product_size='32oz') - self.assertEqual(item.full_description, "Bragg Vinegar 32oz") - def test_str(self): item = mod.OrderItem() @@ -40,7 +27,8 @@ class TestOrderItem(DataTestCase): item = mod.OrderItem(product_description="Vinegar") self.assertEqual(str(item), "Vinegar") - item = mod.OrderItem(product_brand='Bragg', - product_description='Vinegar', - product_size='32oz') + product = PendingProduct(brand_name="Bragg", + description="Vinegar", + size="32oz") + item = mod.OrderItem(pending_product=product) self.assertEqual(str(item), "Bragg Vinegar 32oz") diff --git a/tests/web/forms/test_schema.py b/tests/web/forms/test_schema.py index 3dd838a..38ff106 100644 --- a/tests/web/forms/test_schema.py +++ b/tests/web/forms/test_schema.py @@ -32,31 +32,6 @@ class TestOrderRef(WebTestCase): self.assertIn(f'/orders/{order.uuid}', url) -class TestLocalCustomerRef(WebTestCase): - - def test_sort_query(self): - typ = mod.LocalCustomerRef(self.request, session=self.session) - query = typ.get_query() - self.assertIsInstance(query, orm.Query) - sorted_query = typ.sort_query(query) - self.assertIsInstance(sorted_query, orm.Query) - self.assertIsNot(sorted_query, query) - - def test_get_object_url(self): - self.pyramid_config.add_route('local_customers.view', '/local/customers/{uuid}') - model = self.app.model - enum = self.app.enum - - customer = model.LocalCustomer() - self.session.add(customer) - self.session.commit() - - typ = mod.LocalCustomerRef(self.request, session=self.session) - url = typ.get_object_url(customer) - self.assertIsNotNone(url) - self.assertIn(f'/local/customers/{customer.uuid}', url) - - class TestPendingCustomerRef(WebTestCase): def test_sort_query(self): @@ -85,31 +60,6 @@ class TestPendingCustomerRef(WebTestCase): self.assertIn(f'/pending/customers/{customer.uuid}', url) -class TestLocalProductRef(WebTestCase): - - def test_sort_query(self): - typ = mod.LocalProductRef(self.request, session=self.session) - query = typ.get_query() - self.assertIsInstance(query, orm.Query) - sorted_query = typ.sort_query(query) - self.assertIsInstance(sorted_query, orm.Query) - self.assertIsNot(sorted_query, query) - - def test_get_object_url(self): - self.pyramid_config.add_route('local_products.view', '/local/products/{uuid}') - model = self.app.model - enum = self.app.enum - - product = model.LocalProduct() - self.session.add(product) - self.session.commit() - - typ = mod.LocalProductRef(self.request, session=self.session) - url = typ.get_object_url(product) - self.assertIsNotNone(url) - self.assertIn(f'/local/products/{product.uuid}', url) - - class TestPendingProductRef(WebTestCase): def test_sort_query(self): diff --git a/tests/web/test_menus.py b/tests/web/test_menus.py index 36fdc56..bff33cd 100644 --- a/tests/web/test_menus.py +++ b/tests/web/test_menus.py @@ -9,4 +9,4 @@ class TestSideshowMenuHandler(WebTestCase): def test_make_menus(self): handler = mod.SideshowMenuHandler(self.config) menus = handler.make_menus(self.request) - self.assertEqual(len(menus), 5) + self.assertEqual(len(menus), 4) diff --git a/tests/web/views/test_customers.py b/tests/web/views/test_customers.py index d68e48f..b8f1db1 100644 --- a/tests/web/views/test_customers.py +++ b/tests/web/views/test_customers.py @@ -16,114 +16,6 @@ class TestIncludeme(WebTestCase): mod.includeme(self.pyramid_config) -class TestLocalCustomerView(WebTestCase): - - def make_view(self): - return mod.LocalCustomerView(self.request) - - def test_configure_grid(self): - model = self.app.model - view = self.make_view() - grid = view.make_grid(model_class=model.LocalCustomer) - self.assertNotIn('full_name', grid.linked_columns) - view.configure_grid(grid) - self.assertIn('full_name', grid.linked_columns) - - def test_configure_form(self): - model = self.app.model - view = self.make_view() - - # creating - with patch.object(view, 'creating', new=True): - form = view.make_form(model_class=model.LocalCustomer) - view.configure_form(form) - self.assertNotIn('external_id', form) - self.assertNotIn('full_name', form) - self.assertNotIn('orders', form) - self.assertNotIn('new_order_batches', form) - - user = model.User(username='barney') - self.session.add(user) - customer = model.LocalCustomer() - self.session.add(customer) - self.session.commit() - - # viewing - with patch.object(view, 'viewing', new=True): - form = view.make_form(model_instance=customer) - view.configure_form(form) - self.assertIn('external_id', form) - self.assertIn('full_name', form) - self.assertIn('orders', form) - self.assertIn('new_order_batches', form) - - def test_make_orders_grid(self): - model = self.app.model - view = self.make_view() - - user = model.User(username='barney') - self.session.add(user) - customer = model.LocalCustomer() - self.session.add(customer) - order = model.Order(order_id=42, local_customer=customer, created_by=user) - self.session.add(order) - self.session.commit() - - # no view perm - grid = view.make_orders_grid(customer) - self.assertEqual(len(grid.actions), 0) - - # with view perm - with patch.object(self.request, 'is_root', new=True): - grid = view.make_orders_grid(customer) - self.assertEqual(len(grid.actions), 1) - self.assertEqual(grid.actions[0].key, 'view') - - def test_make_new_order_batches_grid(self): - model = self.app.model - handler = NewOrderBatchHandler(self.config) - view = self.make_view() - - user = model.User(username='barney') - self.session.add(user) - customer = model.LocalCustomer() - self.session.add(customer) - batch = handler.make_batch(self.session, local_customer=customer, created_by=user) - self.session.add(batch) - self.session.commit() - - # no view perm - grid = view.make_new_order_batches_grid(customer) - self.assertEqual(len(grid.actions), 0) - - # with view perm - with patch.object(self.request, 'is_root', new=True): - grid = view.make_new_order_batches_grid(customer) - self.assertEqual(len(grid.actions), 1) - self.assertEqual(grid.actions[0].key, 'view') - - def test_objectify(self): - model = self.app.model - view = self.make_view() - - user = model.User(username='barney') - self.session.add(user) - self.session.commit() - - with patch.object(view, 'creating', new=True): - with patch.object(self.request, 'user', new=user): - form = view.make_model_form() - with patch.object(form, 'validated', create=True, new={ - 'first_name': 'Chuck', - 'last_name': 'Norris', - }): - customer = view.objectify(form) - self.assertIsInstance(customer, model.LocalCustomer) - self.assertEqual(customer.first_name, 'Chuck') - self.assertEqual(customer.last_name, 'Norris') - self.assertEqual(customer.full_name, 'Chuck Norris') - - class TestPendingCustomerView(WebTestCase): def make_view(self): diff --git a/tests/web/views/test_orders.py b/tests/web/views/test_orders.py index ab996f1..3925832 100644 --- a/tests/web/views/test_orders.py +++ b/tests/web/views/test_orders.py @@ -13,7 +13,7 @@ from wuttaweb.forms.schema import WuttaMoney from sideshow.batch.neworder import NewOrderBatchHandler from sideshow.testing import WebTestCase from sideshow.web.views import orders as mod -from sideshow.web.forms.schema import OrderRef, PendingProductRef +from sideshow.web.forms.schema import OrderRef class TestIncludeme(WebTestCase): @@ -27,9 +27,6 @@ class TestOrderView(WebTestCase): def make_view(self): return mod.OrderView(self.request) - def make_handler(self): - return NewOrderBatchHandler(self.config) - def test_configure_grid(self): model = self.app.model view = self.make_view() @@ -43,7 +40,6 @@ class TestOrderView(WebTestCase): def test_create(self): self.pyramid_config.include('sideshow.web.views') model = self.app.model - enum = self.app.enum view = self.make_view() user = model.User(username='barney') @@ -95,7 +91,7 @@ class TestOrderView(WebTestCase): 'customer_name': 'Fred Flintstone', 'phone_number': '555-1234', 'email_address': 'fred@mailinator.com', - 'new_customer_full_name': 'Fred Flintstone', + 'new_customer_name': 'Fred Flintstone', 'new_customer_first_name': 'Fred', 'new_customer_last_name': 'Flintstone', 'new_customer_phone': '555-1234', @@ -112,40 +108,6 @@ class TestOrderView(WebTestCase): self.assertEqual(response.content_type, 'application/json') self.assertEqual(response.json_body, {'error': 'unknown form action'}) - # add item - with patch.multiple(self.request, create=True, - method='POST', - json_body={'action': 'add_item', - 'product_info': { - 'scancode': '07430500132', - 'description': 'Vinegar', - 'unit_price_reg': 5.99, - }, - 'order_qty': 1, - 'order_uom': enum.ORDER_UOM_UNIT}): - response = view.create() - self.assertIsInstance(response, Response) - self.assertEqual(response.content_type, 'application/json') - data = response.json_body - self.assertEqual(sorted(data), ['batch', 'row']) - - # add item, w/ error - with patch.object(NewOrderBatchHandler, 'add_item', side_effect=RuntimeError): - with patch.multiple(self.request, create=True, - method='POST', - json_body={'action': 'add_item', - 'product_info': { - 'scancode': '07430500116', - 'description': 'Vinegar', - 'unit_price_reg': 3.59, - }, - 'order_qty': 1, - 'order_uom': enum.ORDER_UOM_UNIT}): - response = view.create() - self.assertIsInstance(response, Response) - self.assertEqual(response.content_type, 'application/json') - self.assertEqual(response.json_body, {'error': 'RuntimeError'}) - def test_get_current_batch(self): model = self.app.model handler = NewOrderBatchHandler(self.config) @@ -175,75 +137,6 @@ class TestOrderView(WebTestCase): self.assertEqual(self.session.query(model.NewOrderBatch).count(), 1) self.assertIs(batch2, batch) - def test_customer_autocomplete(self): - model = self.app.model - view = self.make_view() - - with patch.object(view, 'Session', return_value=self.session): - - # empty results by default - self.assertEqual(view.customer_autocomplete(), []) - with patch.object(self.request, 'GET', new={'term': 'foo'}, create=True): - self.assertEqual(view.customer_autocomplete(), []) - - # add a customer - customer = model.LocalCustomer(full_name="Chuck Norris") - self.session.add(customer) - self.session.flush() - - # search for chuck finds chuck - with patch.object(self.request, 'GET', new={'term': 'chuck'}, create=True): - result = view.customer_autocomplete() - self.assertEqual(len(result), 1) - self.assertEqual(result[0], { - 'value': customer.uuid.hex, - 'label': "Chuck Norris", - }) - - # search for sally finds nothing - with patch.object(self.request, 'GET', new={'term': 'sally'}, create=True): - result = view.customer_autocomplete() - self.assertEqual(result, []) - - def test_product_autocomplete(self): - model = self.app.model - view = self.make_view() - - with patch.object(view, 'Session', return_value=self.session): - - # empty results by default - self.assertEqual(view.product_autocomplete(), []) - with patch.object(self.request, 'GET', new={'term': 'foo'}, create=True): - self.assertEqual(view.product_autocomplete(), []) - - # add a product - product = model.LocalProduct(brand_name="Bragg's", description="Vinegar") - self.session.add(product) - self.session.flush() - - # search for vinegar finds product - with patch.object(self.request, 'GET', new={'term': 'vinegar'}, create=True): - result = view.product_autocomplete() - self.assertEqual(len(result), 1) - self.assertEqual(result[0], { - 'value': product.uuid.hex, - 'label': "Bragg's Vinegar", - }) - - # search for brag finds product - with patch.object(self.request, 'GET', new={'term': 'brag'}, create=True): - result = view.product_autocomplete() - self.assertEqual(len(result), 1) - self.assertEqual(result[0], { - 'value': product.uuid.hex, - 'label': "Bragg's Vinegar", - }) - - # search for juice finds nothing - with patch.object(self.request, 'GET', new={'term': 'juice'}, create=True): - result = view.product_autocomplete() - self.assertEqual(result, []) - def test_get_pending_product_required_fields(self): model = self.app.model view = self.make_view() @@ -265,51 +158,38 @@ class TestOrderView(WebTestCase): model = self.app.model handler = NewOrderBatchHandler(self.config) view = self.make_view() - view.batch_handler = handler user = model.User(username='barney') self.session.add(user) - # with external customer - with patch.object(handler, 'use_local_customers', return_value=False): - batch = handler.make_batch(self.session, created_by=user, - customer_id=42, customer_name='Fred Flintstone', - phone_number='555-1234', email_address='fred@mailinator.com') - self.session.add(batch) - self.session.flush() - context = view.get_context_customer(batch) - self.assertEqual(context, { - 'customer_is_known': True, - 'customer_id': 42, - 'customer_name': 'Fred Flintstone', - 'phone_number': '555-1234', - 'email_address': 'fred@mailinator.com', - }) - - # with local customer - local = model.LocalCustomer(full_name="Betty Boop") - self.session.add(local) + # with true customer batch = handler.make_batch(self.session, created_by=user, - local_customer=local, customer_name='Betty Boop', - phone_number='555-8888') + customer_id=42, customer_name='Fred Flintstone', + phone_number='555-1234', email_address='fred@mailinator.com') self.session.add(batch) self.session.flush() context = view.get_context_customer(batch) self.assertEqual(context, { 'customer_is_known': True, - 'customer_id': local.uuid.hex, - 'customer_name': 'Betty Boop', - 'phone_number': '555-8888', - 'email_address': None, + 'customer_id': 42, + 'customer_name': 'Fred Flintstone', + 'phone_number': '555-1234', + 'email_address': 'fred@mailinator.com', + 'new_customer_name': None, + 'new_customer_first_name': None, + 'new_customer_last_name': None, + 'new_customer_phone': None, + 'new_customer_email': None, }) # with pending customer batch = handler.make_batch(self.session, created_by=user) self.session.add(batch) - handler.set_customer(batch, dict( + handler.set_pending_customer(batch, dict( full_name="Fred Flintstone", first_name="Fred", last_name="Flintstone", phone_number='555-1234', email_address='fred@mailinator.com', + created_by=user, )) self.session.flush() context = view.get_context_customer(batch) @@ -319,7 +199,7 @@ class TestOrderView(WebTestCase): 'customer_name': 'Fred Flintstone', 'phone_number': '555-1234', 'email_address': 'fred@mailinator.com', - 'new_customer_full_name': 'Fred Flintstone', + 'new_customer_name': 'Fred Flintstone', 'new_customer_first_name': 'Fred', 'new_customer_last_name': 'Flintstone', 'new_customer_phone': '555-1234', @@ -337,6 +217,11 @@ class TestOrderView(WebTestCase): 'customer_name': None, 'phone_number': None, 'email_address': None, + 'new_customer_name': None, + 'new_customer_first_name': None, + 'new_customer_last_name': None, + 'new_customer_phone': None, + 'new_customer_email': None, }) def test_start_over(self): @@ -383,80 +268,6 @@ class TestOrderView(WebTestCase): self.session.flush() self.assertEqual(self.session.query(model.NewOrderBatch).count(), 0) - def test_assign_customer(self): - self.pyramid_config.add_route('orders.create', '/orders/new') - model = self.app.model - handler = NewOrderBatchHandler(self.config) - view = self.make_view() - - user = model.User(username='barney') - self.session.add(user) - weirdal = model.LocalCustomer(full_name="Weird Al") - self.session.add(weirdal) - self.session.flush() - - with patch.object(view, 'batch_handler', create=True, new=handler): - with patch.object(view, 'Session', return_value=self.session): - with patch.object(self.request, 'user', new=user): - batch = view.get_current_batch() - - # normal - self.assertIsNone(batch.local_customer) - self.assertIsNone(batch.pending_customer) - context = view.assign_customer(batch, {'customer_id': weirdal.uuid.hex}) - self.assertIsNone(batch.pending_customer) - self.assertIs(batch.local_customer, weirdal) - self.assertEqual(context, { - 'customer_is_known': True, - 'customer_id': weirdal.uuid.hex, - 'customer_name': 'Weird Al', - 'phone_number': None, - 'email_address': None, - }) - - # missing customer_id - context = view.assign_customer(batch, {}) - self.assertEqual(context, {'error': "Must provide customer_id"}) - - def test_unassign_customer(self): - self.pyramid_config.add_route('orders.create', '/orders/new') - model = self.app.model - handler = NewOrderBatchHandler(self.config) - view = self.make_view() - - user = model.User(username='barney') - self.session.add(user) - self.session.flush() - - with patch.object(view, 'batch_handler', create=True, new=handler): - with patch.object(view, 'Session', return_value=self.session): - with patch.object(self.request, 'user', new=user): - batch = view.get_current_batch() - view.set_pending_customer(batch, {'first_name': 'Jack', - 'last_name': 'Black'}) - - # normal - self.assertIsNone(batch.local_customer) - self.assertIsNotNone(batch.pending_customer) - self.assertEqual(batch.customer_name, 'Jack Black') - context = view.unassign_customer(batch, {}) - # nb. pending record remains, but not used - self.assertIsNotNone(batch.pending_customer) - self.assertIsNone(batch.customer_name) - self.assertIsNone(batch.local_customer) - self.assertEqual(context, { - 'customer_is_known': True, - 'customer_id': None, - 'customer_name': None, - 'phone_number': None, - 'email_address': None, - 'new_customer_full_name': 'Jack Black', - 'new_customer_first_name': 'Jack', - 'new_customer_last_name': 'Black', - 'new_customer_phone': None, - 'new_customer_email': None, - }) - def test_set_pending_customer(self): self.pyramid_config.add_route('orders.create', '/orders/new') model = self.app.model @@ -490,58 +301,19 @@ class TestOrderView(WebTestCase): 'customer_name': 'Fred Flintstone', 'phone_number': '555-1234', 'email_address': 'fred@mailinator.com', - 'new_customer_full_name': 'Fred Flintstone', + 'new_customer_name': 'Fred Flintstone', 'new_customer_first_name': 'Fred', 'new_customer_last_name': 'Flintstone', 'new_customer_phone': '555-1234', 'new_customer_email': 'fred@mailinator.com', }) - def test_get_product_info(self): - model = self.app.model - handler = self.make_handler() - view = self.make_view() - - user = model.User(username='barney') - self.session.add(user) - local = model.LocalProduct(scancode='07430500132', - brand_name='Bragg', - description='Vinegar', - size='32oz', - case_size=12, - unit_price_reg=decimal.Decimal('5.99')) - self.session.add(local) - self.session.flush() - - with patch.object(view, 'Session', return_value=self.session): - with patch.object(view, 'batch_handler', create=True, new=handler): - with patch.object(self.request, 'user', new=user): - batch = view.get_current_batch() - - # typical, for local product - context = view.get_product_info(batch, {'product_id': local.uuid.hex}) - self.assertEqual(context['product_id'], local.uuid.hex) - self.assertEqual(context['scancode'], '07430500132') - self.assertEqual(context['brand_name'], 'Bragg') - self.assertEqual(context['description'], 'Vinegar') - self.assertEqual(context['size'], '32oz') - self.assertEqual(context['full_description'], 'Bragg Vinegar 32oz') - self.assertEqual(context['case_size'], 12) - self.assertEqual(context['unit_price_reg'], 5.99) - - # error if local product missing - mock_uuid = self.app.make_true_uuid() - context = view.get_product_info(batch, {'product_id': mock_uuid.hex}) - self.assertEqual(context, {'error': "Product not found"}) - - # error if no product_id - context = view.get_product_info(batch, {}) - self.assertEqual(context, {'error': "Must specify a product ID"}) - - # external lookup not implemented (yet) - with patch.object(handler, 'use_local_products', return_value=False): - self.assertRaises(NotImplementedError, view.get_product_info, - batch, {'product_id': '42'}) + # error + with patch.object(handler, 'set_pending_customer', side_effect=RuntimeError): + context = view.set_pending_customer(batch, data) + self.assertEqual(context, { + 'error': 'RuntimeError', + }) def test_add_item(self): model = self.app.model @@ -554,7 +326,7 @@ class TestOrderView(WebTestCase): self.session.commit() data = { - 'product_info': { + 'pending_product': { 'scancode': '07430500132', 'brand_name': 'Bragg', 'description': 'Vinegar', @@ -581,10 +353,16 @@ class TestOrderView(WebTestCase): row = batch.rows[0] self.assertIsInstance(row.pending_product, model.PendingProduct) - # external product not yet supported - with patch.object(handler, 'use_local_products', return_value=False): - with patch.dict(data, product_info='42'): - self.assertRaises(NotImplementedError, view.add_item, batch, data) + # pending w/ invalid price + with patch.dict(data['pending_product'], unit_price_reg='invalid'): + result = view.add_item(batch, data) + self.assertEqual(result, {'error': "Invalid entry for field: unit_price_reg"}) + self.session.flush() + self.assertEqual(len(batch.rows), 1) # still just the 1st row + + # true product not yet supported + with patch.dict(data, product_is_known=True): + self.assertRaises(NotImplementedError, view.add_item, batch, data) def test_update_item(self): model = self.app.model @@ -597,7 +375,7 @@ class TestOrderView(WebTestCase): self.session.commit() data = { - 'product_info': { + 'pending_product': { 'scancode': '07430500132', 'brand_name': 'Bragg', 'description': 'Vinegar', @@ -625,7 +403,7 @@ class TestOrderView(WebTestCase): # missing row uuid result = view.update_item(batch, data) - self.assertEqual(result, {'error': "Must specify row UUID"}) + self.assertEqual(result, {'error': "Must specify a row UUID"}) # row not found with patch.dict(data, uuid=self.app.make_true_uuid()): @@ -642,18 +420,16 @@ class TestOrderView(WebTestCase): result = view.update_item(batch, data) self.assertEqual(result, {'error': "Row is for wrong batch"}) + # set row for remaining tests + data['uuid'] = row.uuid + # true product not yet supported - with patch.object(handler, 'use_local_products', return_value=False): - self.assertRaises(NotImplementedError, view.update_item, batch, { - 'uuid': row.uuid, - 'product_info': '42', - 'order_qty': 1, - 'order_uom': enum.ORDER_UOM_UNIT, - }) + with patch.dict(data, product_is_known=True): + self.assertRaises(NotImplementedError, view.update_item, batch, data) # update row, pending product - with patch.dict(data, uuid=row.uuid, order_qty=2): - with patch.dict(data['product_info'], scancode='07430500116'): + with patch.dict(data, order_qty=2): + with patch.dict(data['pending_product'], scancode='07430500116'): self.assertEqual(row.product_scancode, '07430500132') self.assertEqual(row.order_qty, 1) result = view.update_item(batch, data) @@ -662,7 +438,7 @@ class TestOrderView(WebTestCase): self.assertEqual(row.order_qty, 2) self.assertEqual(row.pending_product.scancode, '07430500116') self.assertEqual(result['row']['product_scancode'], '07430500116') - self.assertEqual(result['row']['order_qty'], 2) + self.assertEqual(result['row']['order_qty'], '2') def test_delete_item(self): model = self.app.model @@ -675,7 +451,7 @@ class TestOrderView(WebTestCase): self.session.commit() data = { - 'product_info': { + 'pending_product': { 'scancode': '07430500132', 'brand_name': 'Bragg', 'description': 'Vinegar', @@ -730,7 +506,7 @@ class TestOrderView(WebTestCase): self.assertEqual(len(batch.rows), 0) self.assertEqual(batch.row_count, 0) - def test_submit_order(self): + def test_submit_new_order(self): self.pyramid_config.add_route('orders.view', '/orders/{uuid}') model = self.app.model enum = self.app.enum @@ -742,7 +518,7 @@ class TestOrderView(WebTestCase): self.session.commit() data = { - 'product_info': { + 'pending_product': { 'scancode': '07430500132', 'brand_name': 'Bragg', 'description': 'Vinegar', @@ -758,33 +534,28 @@ class TestOrderView(WebTestCase): with patch.object(view, 'Session', return_value=self.session): with patch.object(self.request, 'user', new=user): batch = view.get_current_batch() + self.session.flush() self.assertEqual(len(batch.rows), 0) # add row w/ pending product view.add_item(batch, data) - self.assertEqual(len(batch.rows), 1) + self.session.flush() row = batch.rows[0] self.assertIsInstance(row.pending_product, model.PendingProduct) self.assertEqual(row.unit_price_quoted, decimal.Decimal('5.99')) # execute not allowed yet (no customer) - result = view.submit_order(batch, {}) + result = view.submit_new_order(batch, {}) self.assertEqual(result, {'error': "Must assign the customer"}) - # execute not allowed yet (no phone number) - view.set_pending_customer(batch, {'full_name': 'John Doe'}) - result = view.submit_order(batch, {}) - self.assertEqual(result, {'error': "Customer phone number is required"}) - # submit/execute ok - view.set_pending_customer(batch, {'full_name': 'John Doe', - 'phone_number': '555-1234'}) - result = view.submit_order(batch, {}) + batch.customer_id = 42 + result = view.submit_new_order(batch, {}) self.assertEqual(sorted(result), ['next_url']) self.assertIn('/orders/', result['next_url']) # error (already executed) - result = view.submit_order(batch, {}) + result = view.submit_new_order(batch, {}) self.assertEqual(result, { 'error': f"ValueError: batch has already been executed: {batch}", }) @@ -814,8 +585,9 @@ class TestOrderView(WebTestCase): 'size': '32oz', 'unit_price_reg': 5.99, 'case_size': 12, + 'created_by': user, } - row = handler.add_item(batch, pending, 1, enum.ORDER_UOM_CASE) + row = handler.add_pending_product(batch, pending, 1, enum.ORDER_UOM_CASE) self.session.commit() data = view.normalize_batch(batch) @@ -832,15 +604,11 @@ class TestOrderView(WebTestCase): enum = self.app.enum handler = NewOrderBatchHandler(self.config) view = self.make_view() - view.batch_handler = handler user = model.User(username='barney') self.session.add(user) batch = handler.make_batch(self.session, created_by=user) self.session.add(batch) - self.session.flush() - - # add 1st row w/ pending product pending = { 'scancode': '07430500132', 'brand_name': 'Bragg', @@ -848,22 +616,19 @@ class TestOrderView(WebTestCase): 'size': '32oz', 'unit_price_reg': 5.99, 'case_size': 12, - 'vendor_name': 'Acme Warehouse', - 'vendor_item_code': '1234', + 'created_by': user, } - row1 = handler.add_item(batch, pending, 2, enum.ORDER_UOM_CASE) + row = handler.add_pending_product(batch, pending, 2, enum.ORDER_UOM_CASE) + self.session.commit() - # typical, pending product - data = view.normalize_row(row1) + # normal + data = view.normalize_row(row) self.assertIsInstance(data, dict) - self.assertEqual(data['uuid'], row1.uuid.hex) + self.assertEqual(data['uuid'], row.uuid.hex) self.assertEqual(data['sequence'], 1) - self.assertIsNone(data['product_id']) self.assertEqual(data['product_scancode'], '07430500132') - self.assertEqual(data['product_full_description'], 'Bragg Vinegar 32oz') - self.assertEqual(data['case_size'], 12) - self.assertEqual(data['vendor_name'], 'Acme Warehouse') - self.assertEqual(data['order_qty'], 2) + self.assertEqual(data['case_size'], '12') + self.assertEqual(data['order_qty'], '2') self.assertEqual(data['order_uom'], 'CS') self.assertEqual(data['order_qty_display'], '2 Cases (× 12 = 24 Units)') self.assertEqual(data['unit_price_reg'], 5.99) @@ -879,9 +644,9 @@ class TestOrderView(WebTestCase): self.assertEqual(data['total_price'], 143.76) self.assertEqual(data['total_price_display'], '$143.76') self.assertIsNone(data['special_order']) - self.assertEqual(data['status_code'], row1.STATUS_OK) + self.assertEqual(data['status_code'], row.STATUS_OK) self.assertEqual(data['pending_product'], { - 'uuid': row1.pending_product_uuid.hex, + 'uuid': row.pending_product_uuid.hex, 'scancode': '07430500132', 'brand_name': 'Bragg', 'description': 'Vinegar', @@ -889,117 +654,44 @@ class TestOrderView(WebTestCase): 'department_id': None, 'department_name': None, 'unit_price_reg': 5.99, - 'vendor_name': 'Acme Warehouse', - 'vendor_item_code': '1234', + 'vendor_name': None, + 'vendor_item_code': None, 'unit_cost': None, 'case_size': 12.0, 'notes': None, 'special_order': None, }) - # the next few tests will morph 1st row.. - # unknown case size - row1.pending_product.case_size = None - handler.refresh_row(row1) + row.pending_product.case_size = None + handler.refresh_row(row) self.session.flush() - data = view.normalize_row(row1) - self.assertIsNone(data['case_size']) + data = view.normalize_row(row) self.assertEqual(data['order_qty_display'], '2 Cases (× ?? = ?? Units)') # order by unit - row1.order_uom = enum.ORDER_UOM_UNIT - handler.refresh_row(row1) + row.order_uom = enum.ORDER_UOM_UNIT + handler.refresh_row(row) self.session.flush() - data = view.normalize_row(row1) - self.assertEqual(data['order_uom'], enum.ORDER_UOM_UNIT) + data = view.normalize_row(row) self.assertEqual(data['order_qty_display'], '2 Units') # item on sale - row1.pending_product.case_size = 12 - row1.unit_price_sale = decimal.Decimal('5.19') - row1.sale_ends = datetime.datetime(2099, 1, 5, 20, 32) - handler.refresh_row(row1) + row.pending_product.case_size = 12 + row.unit_price_sale = decimal.Decimal('5.19') + row.sale_ends = datetime.datetime(2025, 1, 5, 20, 32) + handler.refresh_row(row, now=datetime.datetime(2025, 1, 5, 19)) self.session.flush() - data = view.normalize_row(row1) + data = view.normalize_row(row) self.assertEqual(data['unit_price_sale'], 5.19) self.assertEqual(data['unit_price_sale_display'], '$5.19') - self.assertEqual(data['sale_ends'], '2099-01-05 20:32:00') - self.assertEqual(data['sale_ends_display'], '2099-01-05') + self.assertEqual(data['sale_ends'], '2025-01-05 20:32:00') + self.assertEqual(data['sale_ends_display'], '2025-01-05') self.assertEqual(data['unit_price_quoted'], 5.19) self.assertEqual(data['unit_price_quoted_display'], '$5.19') self.assertEqual(data['case_price_quoted'], 62.28) self.assertEqual(data['case_price_quoted_display'], '$62.28') - # add 2nd row w/ local product - local = model.LocalProduct(brand_name="Lay's", - description="Potato Chips", - vendor_name='Acme Distribution', - unit_price_reg=3.29) - self.session.add(local) - self.session.flush() - row2 = handler.add_item(batch, local.uuid.hex, 1, enum.ORDER_UOM_UNIT) - - # typical, local product - data = view.normalize_row(row2) - self.assertEqual(data['uuid'], row2.uuid.hex) - self.assertEqual(data['sequence'], 2) - self.assertEqual(data['product_id'], local.uuid.hex) - self.assertIsNone(data['product_scancode']) - self.assertEqual(data['product_full_description'], "Lay's Potato Chips") - self.assertIsNone(data['case_size']) - self.assertEqual(data['vendor_name'], 'Acme Distribution') - self.assertEqual(data['order_qty'], 1) - self.assertEqual(data['order_uom'], 'EA') - self.assertEqual(data['order_qty_display'], '1 Units') - self.assertEqual(data['unit_price_reg'], 3.29) - self.assertEqual(data['unit_price_reg_display'], '$3.29') - self.assertNotIn('unit_price_sale', data) - self.assertNotIn('unit_price_sale_display', data) - self.assertNotIn('sale_ends', data) - self.assertNotIn('sale_ends_display', data) - self.assertEqual(data['unit_price_quoted'], 3.29) - self.assertEqual(data['unit_price_quoted_display'], '$3.29') - self.assertIsNone(data['case_price_quoted']) - self.assertEqual(data['case_price_quoted_display'], '') - self.assertEqual(data['total_price'], 3.29) - self.assertEqual(data['total_price_display'], '$3.29') - self.assertIsNone(data['special_order']) - self.assertEqual(data['status_code'], row2.STATUS_OK) - self.assertNotIn('pending_product', data) - - # the next few tests will morph 2nd row.. - - # typical, external product - row2.product_id = '42' - with patch.object(handler, 'use_local_products', return_value=False): - data = view.normalize_row(row2) - self.assertEqual(data['uuid'], row2.uuid.hex) - self.assertEqual(data['sequence'], 2) - self.assertEqual(data['product_id'], '42') - self.assertIsNone(data['product_scancode']) - self.assertNotIn('product_full_description', data) # TODO - self.assertIsNone(data['case_size']) - self.assertNotIn('vendor_name', data) # TODO - self.assertEqual(data['order_qty'], 1) - self.assertEqual(data['order_uom'], 'EA') - self.assertEqual(data['order_qty_display'], '1 Units') - self.assertEqual(data['unit_price_reg'], 3.29) - self.assertEqual(data['unit_price_reg_display'], '$3.29') - self.assertNotIn('unit_price_sale', data) - self.assertNotIn('unit_price_sale_display', data) - self.assertNotIn('sale_ends', data) - self.assertNotIn('sale_ends_display', data) - self.assertEqual(data['unit_price_quoted'], 3.29) - self.assertEqual(data['unit_price_quoted_display'], '$3.29') - self.assertIsNone(data['case_price_quoted']) - self.assertEqual(data['case_price_quoted_display'], '') - self.assertEqual(data['total_price'], 3.29) - self.assertEqual(data['total_price_display'], '$3.29') - self.assertIsNone(data['special_order']) - self.assertEqual(data['status_code'], row2.STATUS_OK) - self.assertNotIn('pending_product', data) - def test_get_instance_title(self): model = self.app.model view = self.make_view() @@ -1023,31 +715,13 @@ class TestOrderView(WebTestCase): self.session.add(order) self.session.commit() - # viewing (no customer) + # viewing with patch.object(view, 'viewing', new=True): form = view.make_form(model_instance=order) # nb. this is to avoid include/exclude ambiguity form.remove('items') view.configure_form(form) schema = form.get_schema() - self.assertIn('pending_customer', form) - self.assertIsInstance(schema['total_price'].typ, WuttaMoney) - - # assign local customer - local = model.LocalCustomer(first_name='Jack', last_name='Black', - phone_number='555-1234') - self.session.add(local) - order.local_customer = local - self.session.flush() - - # viewing (local customer) - with patch.object(view, 'viewing', new=True): - form = view.make_form(model_instance=order) - # nb. this is to avoid include/exclude ambiguity - form.remove('items') - view.configure_form(form) - self.assertNotIn('pending_customer', form) - schema = form.get_schema() self.assertIsInstance(schema['total_price'].typ, WuttaMoney) def test_get_xref_buttons(self): @@ -1157,46 +831,6 @@ class TestOrderView(WebTestCase): url = view.get_row_action_url_view(item, 0) self.assertIn(f'/order-items/{item.uuid}', url) - def test_configure(self): - self.pyramid_config.add_route('home', '/') - self.pyramid_config.add_route('login', '/auth/login') - self.pyramid_config.add_route('orders', '/orders/') - model = self.app.model - view = self.make_view() - - with patch.object(view, 'Session', return_value=self.session): - with patch.multiple(self.config, usedb=True, preferdb=True): - - # sanity check - allowed = self.config.get_bool('sideshow.orders.allow_unknown_products', - session=self.session) - self.assertIsNone(allowed) - self.assertEqual(self.session.query(model.Setting).count(), 0) - - # fetch initial page - response = view.configure() - self.assertIsInstance(response, Response) - self.assertNotIsInstance(response, HTTPFound) - self.session.flush() - allowed = self.config.get_bool('sideshow.orders.allow_unknown_products', - session=self.session) - self.assertIsNone(allowed) - self.assertEqual(self.session.query(model.Setting).count(), 0) - - # post new settings - with patch.multiple(self.request, create=True, - method='POST', - POST={ - 'sideshow.orders.allow_unknown_products': 'true', - }): - response = view.configure() - self.assertIsInstance(response, HTTPFound) - self.session.flush() - allowed = self.config.get_bool('sideshow.orders.allow_unknown_products', - session=self.session) - self.assertTrue(allowed) - self.assertTrue(self.session.query(model.Setting).count() > 1) - class TestOrderItemView(WebTestCase): @@ -1230,18 +864,6 @@ class TestOrderItemView(WebTestCase): self.assertEqual(view.render_status_code(None, None, enum.ORDER_ITEM_STATUS_INITIATED), 'initiated') - def test_get_instance_title(self): - model = self.app.model - enum = self.app.enum - view = self.make_view() - - item = model.OrderItem(product_brand='Bragg', - product_description='Vinegar', - product_size='32oz', - status_code=enum.ORDER_ITEM_STATUS_INITIATED) - title = view.get_instance_title(item) - self.assertEqual(title, "(initiated) Bragg Vinegar 32oz") - def test_configure_form(self): model = self.app.model enum = self.app.enum @@ -1249,24 +871,12 @@ class TestOrderItemView(WebTestCase): item = model.OrderItem(status_code=enum.ORDER_ITEM_STATUS_INITIATED) - # viewing, w/ pending product + # viewing with patch.object(view, 'viewing', new=True): form = view.make_form(model_instance=item) view.configure_form(form) schema = form.get_schema() self.assertIsInstance(schema['order'].typ, OrderRef) - self.assertIn('pending_product', form) - self.assertIsInstance(schema['pending_product'].typ, PendingProductRef) - - # viewing, w/ local product - local = model.LocalProduct() - item.local_product = local - with patch.object(view, 'viewing', new=True): - form = view.make_form(model_instance=item) - view.configure_form(form) - schema = form.get_schema() - self.assertIsInstance(schema['order'].typ, OrderRef) - self.assertNotIn('pending_product', form) def test_get_xref_buttons(self): self.pyramid_config.add_route('orders.view', '/orders/{uuid}') diff --git a/tests/web/views/test_products.py b/tests/web/views/test_products.py index c9782dd..e7e61fe 100644 --- a/tests/web/views/test_products.py +++ b/tests/web/views/test_products.py @@ -16,104 +16,6 @@ class TestIncludeme(WebTestCase): mod.includeme(self.pyramid_config) -class TestLocalProductView(WebTestCase): - - def make_view(self): - return mod.LocalProductView(self.request) - - def test_configure_grid(self): - model = self.app.model - view = self.make_view() - grid = view.make_grid(model_class=model.LocalProduct) - self.assertNotIn('scancode', grid.linked_columns) - self.assertNotIn('brand_name', grid.linked_columns) - self.assertNotIn('description', grid.linked_columns) - view.configure_grid(grid) - self.assertIn('scancode', grid.linked_columns) - self.assertIn('brand_name', grid.linked_columns) - self.assertIn('description', grid.linked_columns) - - def test_configure_form(self): - model = self.app.model - view = self.make_view() - - # creating - with patch.object(view, 'creating', new=True): - form = view.make_form(model_class=model.LocalProduct) - self.assertIn('external_id', form) - view.configure_form(form) - self.assertNotIn('external_id', form) - - user = model.User(username='barney') - self.session.add(user) - product = model.LocalProduct() - self.session.add(product) - self.session.commit() - - # viewing - with patch.object(view, 'viewing', new=True): - form = view.make_form(model_instance=product) - self.assertNotIn('external_id', form.readonly_fields) - self.assertNotIn('local_products.view.orders', form.grid_vue_context) - view.configure_form(form) - self.assertIn('external_id', form.readonly_fields) - self.assertIn('local_products.view.orders', form.grid_vue_context) - - def test_make_orders_grid(self): - model = self.app.model - enum = self.app.enum - view = self.make_view() - - user = model.User(username='barney') - self.session.add(user) - order = model.Order(order_id=42, customer_id=42, created_by=user) - product = model.LocalProduct() - self.session.add(product) - item = model.OrderItem(local_product=product, - order_qty=1, order_uom=enum.ORDER_UOM_UNIT, - status_code=enum.ORDER_ITEM_STATUS_INITIATED) - order.items.append(item) - self.session.add(order) - self.session.commit() - - # no view perm - grid = view.make_orders_grid(product) - self.assertEqual(len(grid.actions), 0) - - # with view perm - with patch.object(self.request, 'is_root', new=True): - grid = view.make_orders_grid(product) - self.assertEqual(len(grid.actions), 1) - self.assertEqual(grid.actions[0].key, 'view') - - def test_make_new_order_batches_grid(self): - model = self.app.model - enum = self.app.enum - handler = NewOrderBatchHandler(self.config) - view = self.make_view() - - user = model.User(username='barney') - self.session.add(user) - batch = handler.make_batch(self.session, created_by=user) - self.session.add(batch) - product = model.LocalProduct() - self.session.add(product) - row = handler.make_row(local_product=product, - order_qty=1, order_uom=enum.ORDER_UOM_UNIT) - handler.add_row(batch, row) - self.session.commit() - - # no view perm - grid = view.make_new_order_batches_grid(product) - self.assertEqual(len(grid.actions), 0) - - # with view perm - with patch.object(self.request, 'is_root', new=True): - grid = view.make_new_order_batches_grid(product) - self.assertEqual(len(grid.actions), 1) - self.assertEqual(grid.actions[0].key, 'view') - - class TestPendingProductView(WebTestCase): def make_view(self):