feat: add basic support to "resolve" a pending product
This commit is contained in:
parent
6b4bc3da10
commit
1ee398e8fb
|
@ -1056,7 +1056,7 @@ class NewOrderBatchHandler(BatchHandler):
|
|||
By default, this will call:
|
||||
|
||||
* :meth:`make_local_customer()`
|
||||
* :meth:`make_local_products()`
|
||||
* :meth:`process_pending_products()`
|
||||
* :meth:`make_new_order()`
|
||||
|
||||
And will return the new
|
||||
|
@ -1068,7 +1068,7 @@ class NewOrderBatchHandler(BatchHandler):
|
|||
"""
|
||||
rows = self.get_effective_rows(batch)
|
||||
self.make_local_customer(batch)
|
||||
self.make_local_products(batch, rows)
|
||||
self.process_pending_products(batch, rows)
|
||||
order = self.make_new_order(batch, rows, user=user, progress=progress, **kwargs)
|
||||
return order
|
||||
|
||||
|
@ -1115,46 +1115,48 @@ class NewOrderBatchHandler(BatchHandler):
|
|||
session.delete(pending)
|
||||
session.flush()
|
||||
|
||||
def make_local_products(self, batch, rows):
|
||||
def process_pending_products(self, batch, rows):
|
||||
"""
|
||||
If applicable, this converts all :term:`pending products
|
||||
<pending product>` into :term:`local products <local
|
||||
product>`.
|
||||
Process any :term:`pending products <pending product>` which
|
||||
are present in the batch.
|
||||
|
||||
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).
|
||||
If :term:`local products <local product>` are used, this will
|
||||
convert the pending products to local products.
|
||||
|
||||
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 :term:`external products <external product>` are used, this
|
||||
will update the pending product records' status to indicate
|
||||
they are ready to be resolved.
|
||||
"""
|
||||
if not self.use_local_products():
|
||||
return
|
||||
|
||||
enum = self.app.enum
|
||||
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
|
||||
if self.use_local_products():
|
||||
inspector = sa.inspect(model.LocalProduct)
|
||||
for row in rows:
|
||||
|
||||
pending = row.pending_product
|
||||
local = model.LocalProduct()
|
||||
if row.local_product or not row.pending_product:
|
||||
continue
|
||||
|
||||
for prop in inspector.column_attrs:
|
||||
if hasattr(pending, prop.key):
|
||||
setattr(local, prop.key, getattr(pending, prop.key))
|
||||
session.add(local)
|
||||
pending = row.pending_product
|
||||
local = model.LocalProduct()
|
||||
|
||||
row.local_product = local
|
||||
row.pending_product = None
|
||||
session.delete(pending)
|
||||
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)
|
||||
|
||||
else: # external products; pending should be marked 'ready'
|
||||
for row in rows:
|
||||
pending = row.pending_product
|
||||
if pending:
|
||||
pending.status = enum.PendingProductStatus.READY
|
||||
|
||||
session.flush()
|
||||
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
"""add ignored status
|
||||
|
||||
Revision ID: fd8a2527bd30
|
||||
Revises: 13af2ffbc0e0
|
||||
Create Date: 2025-02-20 12:08:27.374172
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
from alembic_postgresql_enum import TableReference
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'fd8a2527bd30'
|
||||
down_revision: Union[str, None] = '13af2ffbc0e0'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# pendingcustomerstatus
|
||||
op.sync_enum_values(
|
||||
enum_schema='public',
|
||||
enum_name='pendingcustomerstatus',
|
||||
new_values=['PENDING', 'READY', 'RESOLVED', 'IGNORED'],
|
||||
affected_columns=[TableReference(table_schema='public', table_name='sideshow_customer_pending', column_name='status')],
|
||||
enum_values_to_rename=[],
|
||||
)
|
||||
|
||||
# pendingproductstatus
|
||||
op.sync_enum_values(
|
||||
enum_schema='public',
|
||||
enum_name='pendingproductstatus',
|
||||
new_values=['PENDING', 'READY', 'RESOLVED', 'IGNORED'],
|
||||
affected_columns=[TableReference(table_schema='public', table_name='sideshow_product_pending', column_name='status')],
|
||||
enum_values_to_rename=[],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
# pendingproductstatus
|
||||
op.sync_enum_values(
|
||||
enum_schema='public',
|
||||
enum_name='pendingproductstatus',
|
||||
new_values=['PENDING', 'READY', 'RESOLVED'],
|
||||
affected_columns=[TableReference(table_schema='public', table_name='sideshow_product_pending', column_name='status')],
|
||||
enum_values_to_rename=[],
|
||||
)
|
||||
|
||||
# pendingcustomerstatus
|
||||
op.sync_enum_values(
|
||||
enum_schema='public',
|
||||
enum_name='pendingcustomerstatus',
|
||||
new_values=['PENDING', 'READY', 'RESOLVED'],
|
||||
affected_columns=[TableReference(table_schema='public', table_name='sideshow_customer_pending', column_name='status')],
|
||||
enum_values_to_rename=[],
|
||||
)
|
|
@ -185,7 +185,8 @@ class PendingProduct(ProductMixin, model.Base):
|
|||
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.
|
||||
ID of the :term:`external product` associated with this record, if
|
||||
applicable/known.
|
||||
""")
|
||||
|
||||
status = sa.Column(sa.Enum(PendingProductStatus), nullable=False, doc="""
|
||||
|
|
|
@ -91,6 +91,7 @@ class PendingCustomerStatus(Enum):
|
|||
PENDING = 'pending'
|
||||
READY = 'ready'
|
||||
RESOLVED = 'resolved'
|
||||
IGNORED = 'ignored'
|
||||
|
||||
|
||||
class PendingProductStatus(Enum):
|
||||
|
@ -101,6 +102,7 @@ class PendingProductStatus(Enum):
|
|||
PENDING = 'pending'
|
||||
READY = 'ready'
|
||||
RESOLVED = 'resolved'
|
||||
IGNORED = 'ignored'
|
||||
|
||||
|
||||
########################################
|
||||
|
|
|
@ -105,6 +105,81 @@ class OrderHandler(GenericHandler):
|
|||
enum.ORDER_ITEM_STATUS_INACTIVE):
|
||||
return 'warning'
|
||||
|
||||
def resolve_pending_product(self, pending_product, product_info, user, note=None):
|
||||
"""
|
||||
Resolve a :term:`pending product`, to reflect the given
|
||||
product info.
|
||||
|
||||
At a high level this does 2 things:
|
||||
|
||||
* update the ``pending_product``
|
||||
* find and update any related :term:`order item(s) <order item>`
|
||||
|
||||
The first step just sets
|
||||
:attr:`~sideshow.db.model.products.PendingProduct.product_id`
|
||||
from the provided info, and gives it the "resolved" status.
|
||||
Note that it does *not* update the pending product record
|
||||
further, so it will not fully "match" the product info.
|
||||
|
||||
The second step will fetch all
|
||||
:class:`~sideshow.db.model.orders.OrderItem` records which
|
||||
reference the ``pending_product`` **and** which do not yet
|
||||
have a ``product_id`` value. For each, it then updates the
|
||||
order item to contain all data from ``product_info``. And
|
||||
finally, it adds an event to the item history, indicating who
|
||||
resolved and when. (If ``note`` is specified, a *second*
|
||||
event is added for that.)
|
||||
|
||||
:param pending_product:
|
||||
:class:`~sideshow.db.model.products.PendingProduct` to be
|
||||
resolved.
|
||||
|
||||
:param product_info: Dict of product info, as obtained from
|
||||
:meth:`~sideshow.batch.neworder.NewOrderBatchHandler.get_product_info_external()`.
|
||||
|
||||
:param user:
|
||||
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
|
||||
is performing the action.
|
||||
|
||||
:param note: Optional note to be added to event history for
|
||||
related order item(s).
|
||||
"""
|
||||
enum = self.app.enum
|
||||
model = self.app.model
|
||||
session = self.app.get_session(pending_product)
|
||||
|
||||
if pending_product.status != enum.PendingProductStatus.READY:
|
||||
raise ValueError("pending product does not have 'ready' status")
|
||||
|
||||
info = product_info
|
||||
pending_product.product_id = info['product_id']
|
||||
pending_product.status = enum.PendingProductStatus.RESOLVED
|
||||
|
||||
items = session.query(model.OrderItem)\
|
||||
.filter(model.OrderItem.pending_product == pending_product)\
|
||||
.filter(model.OrderItem.product_id == None)\
|
||||
.all()
|
||||
|
||||
for item in items:
|
||||
item.product_id = info['product_id']
|
||||
item.product_scancode = info['scancode']
|
||||
item.product_brand = info['brand_name']
|
||||
item.product_description = info['description']
|
||||
item.product_size = info['size']
|
||||
item.product_weighed = info['weighed']
|
||||
item.department_id = info['department_id']
|
||||
item.department_name = info['department_name']
|
||||
item.special_order = info['special_order']
|
||||
item.vendor_name = info['vendor_name']
|
||||
item.vendor_item_code = info['vendor_item_code']
|
||||
item.case_size = info['case_size']
|
||||
item.unit_cost = info['unit_cost']
|
||||
item.unit_price_reg = info['unit_price_reg']
|
||||
|
||||
item.add_event(enum.ORDER_ITEM_EVENT_PRODUCT_RESOLVED, user)
|
||||
if note:
|
||||
item.add_event(enum.ORDER_ITEM_EVENT_NOTE_ADDED, user, note=note)
|
||||
|
||||
def process_placement(self, items, user, vendor_name=None, po_number=None, note=None):
|
||||
"""
|
||||
Process the "placement" step for the given order items.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Sideshow -- Case/Special Order Tracker
|
||||
# Copyright © 2024 Lance Edgar
|
||||
# Copyright © 2024-2025 Lance Edgar
|
||||
#
|
||||
# This file is part of Sideshow.
|
||||
#
|
||||
|
@ -33,4 +33,6 @@ class WebTestCase(base.WebTestCase):
|
|||
config = super().make_config(**kwargs)
|
||||
config.setdefault('wutta.model_spec', 'sideshow.db.model')
|
||||
config.setdefault('wutta.enum_spec', 'sideshow.enum')
|
||||
config.setdefault(f'{config.appname}.batch.neworder.handler.default_spec',
|
||||
'sideshow.batch.neworder:NewOrderBatchHandler')
|
||||
return config
|
||||
|
|
8
src/sideshow/web/templates/base.mako
Normal file
8
src/sideshow/web/templates/base.mako
Normal file
|
@ -0,0 +1,8 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="wuttaweb:templates/base.mako" />
|
||||
<%namespace file="/sideshow-components.mako" import="make_sideshow_components" />
|
||||
|
||||
<%def name="render_vue_templates()">
|
||||
${make_sideshow_components()}
|
||||
${parent.render_vue_templates()}
|
||||
</%def>
|
|
@ -207,7 +207,7 @@
|
|||
<div class="panel-block">
|
||||
<div style="width: 100%;">
|
||||
<b-field horizontal label="Product ID">
|
||||
<span>${item.product_id}</span>
|
||||
<span>${item.product_id or ''}</span>
|
||||
</b-field>
|
||||
% if not item.product_id and item.local_product:
|
||||
<b-field horizontal label="Local Product">
|
||||
|
@ -220,34 +220,34 @@
|
|||
</b-field>
|
||||
% endif
|
||||
<b-field horizontal label="Scancode">
|
||||
<span>${item.product_scancode}</span>
|
||||
<span>${item.product_scancode or ''}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Brand">
|
||||
<span>${item.product_brand}</span>
|
||||
<span>${item.product_brand or ''}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Description">
|
||||
<span>${item.product_description}</span>
|
||||
<span>${item.product_description or ''}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Size">
|
||||
<span>${item.product_size}</span>
|
||||
<span>${item.product_size or ''}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Sold by Weight">
|
||||
<span>${app.render_boolean(item.product_weighed)}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Department ID">
|
||||
<span>${item.department_id}</span>
|
||||
<span>${item.department_id or ''}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Department Name">
|
||||
<span>${item.department_name}</span>
|
||||
<span>${item.department_name or ''}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Special Order">
|
||||
<span>${app.render_boolean(item.special_order)}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Vendor Name">
|
||||
<span>${item.vendor_name}</span>
|
||||
<span>${item.vendor_name or ''}</span>
|
||||
</b-field>
|
||||
<b-field horizontal label="Vendor Item Code">
|
||||
<span>${item.vendor_item_code}</span>
|
||||
<span>${item.vendor_item_code or ''}</span>
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -523,12 +523,10 @@
|
|||
|
||||
<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" />
|
||||
<sideshow-product-lookup v-model="productID"
|
||||
ref="productLookup"
|
||||
:display="productDisplay"
|
||||
@input="productChanged" />
|
||||
</b-field>
|
||||
|
||||
<div v-if="productID">
|
||||
|
@ -1696,8 +1694,7 @@
|
|||
% endif
|
||||
this.editItemShowDialog = true
|
||||
this.$nextTick(() => {
|
||||
// this.$refs.productLookup.focus()
|
||||
this.$refs.productAutocomplete.focus()
|
||||
this.$refs.productLookup.focus()
|
||||
})
|
||||
},
|
||||
|
||||
|
|
166
src/sideshow/web/templates/pending/products/view.mako
Normal file
166
src/sideshow/web/templates/pending/products/view.mako
Normal file
|
@ -0,0 +1,166 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/master/view.mako" />
|
||||
|
||||
<%def name="tool_panels()">
|
||||
${parent.tool_panels()}
|
||||
|
||||
<wutta-tool-panel heading="Status" style="white-space: nowrap;">
|
||||
<b-field horizontal label="Current Status">
|
||||
<span>${instance.status.value}</span>
|
||||
</b-field>
|
||||
|
||||
% if instance.status.name == 'READY' and master.has_perm('resolve') and not use_local_products:
|
||||
<b-button type="is-primary"
|
||||
icon-pack="fas"
|
||||
icon-left="object-ungroup"
|
||||
@click="resolveInit()">
|
||||
Resolve Product
|
||||
</b-button>
|
||||
<b-modal :active.sync="resolveShowDialog">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
${h.form(master.get_action_url('resolve', instance), **{'@submit': 'resolveSubmitting = true'})}
|
||||
${h.csrf_token(request)}
|
||||
|
||||
<div style="display: flex; gap: 1rem;">
|
||||
|
||||
<div style="flex-grow: 1;">
|
||||
|
||||
<p class="block has-text-weight-bold">
|
||||
Please identify the corresponding External Product.
|
||||
</p>
|
||||
<p class="block">
|
||||
All related orders etc. will be updated accordingly.
|
||||
</p>
|
||||
<b-field grouped>
|
||||
<b-field label="Scancode">
|
||||
<span>${instance.scancode or ''}</span>
|
||||
</b-field>
|
||||
<b-field label="Brand">
|
||||
<span>${instance.brand_name or ''}</span>
|
||||
</b-field>
|
||||
<b-field label="Description">
|
||||
<span>${instance.description or ''}</span>
|
||||
</b-field>
|
||||
<b-field label="Size">
|
||||
<span>${instance.size or ''}</span>
|
||||
</b-field>
|
||||
</b-field>
|
||||
<b-field grouped>
|
||||
<b-field label="Vendor Name">
|
||||
<span>${instance.vendor_name or ''}</span>
|
||||
</b-field>
|
||||
<b-field label="Vendor Item Code">
|
||||
<span>${instance.vendor_item_code or ''}</span>
|
||||
</b-field>
|
||||
</b-field>
|
||||
</div>
|
||||
|
||||
<div style="flex-grow: 1;">
|
||||
<b-field label="External Product">
|
||||
<div>
|
||||
<sideshow-product-lookup v-model="resolveProductID"
|
||||
ref="productLookup" />
|
||||
${h.hidden('product_id', **{':value': 'resolveProductID'})}
|
||||
</div>
|
||||
</b-field>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<div class="buttons">
|
||||
<b-button @click="resolveShowDialog = false">
|
||||
Cancel
|
||||
</b-button>
|
||||
<b-button type="is-primary"
|
||||
native-type="submit"
|
||||
icon-pack="fas"
|
||||
icon-left="object-ungroup"
|
||||
:disabled="resolveSubmitting">
|
||||
{{ resolveSubmitting ? "Working, please wait..." : "Resolve" }}
|
||||
</b-button>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
${h.end_form()}
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
% endif
|
||||
|
||||
% if instance.status.name == 'READY' and master.has_perm('ignore') and not use_local_products:
|
||||
<b-button type="is-warning"
|
||||
icon-pack="fas"
|
||||
icon-left="ban"
|
||||
@click="ignoreShowDialog = true">
|
||||
Ignore Product
|
||||
</b-button>
|
||||
<b-modal has-modal-card
|
||||
:active.sync="ignoreShowDialog">
|
||||
<div class="modal-card">
|
||||
${h.form(master.get_action_url('ignore', instance), **{'@submit': 'ignoreSubmitting = true'})}
|
||||
${h.csrf_token(request)}
|
||||
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Ignore Product</p>
|
||||
</header>
|
||||
|
||||
<section class="modal-card-body">
|
||||
<p class="block has-text-weight-bold">
|
||||
Really ignore this product?
|
||||
</p>
|
||||
<p class="block">
|
||||
This will change the product status to "ignored"<br />
|
||||
and you will no longer be prompted to resolve it.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<footer class="modal-card-foot">
|
||||
<b-button @click="ignoreShowDialog = false">
|
||||
Cancel
|
||||
</b-button>
|
||||
<b-button type="is-warning"
|
||||
native-type="submit"
|
||||
icon-pack="fas"
|
||||
icon-left="ban"
|
||||
:disabled="ignoreSubmitting">
|
||||
{{ ignoreSubmitting ? "Working, please wait..." : "Ignore" }}
|
||||
</b-button>
|
||||
</footer>
|
||||
|
||||
${h.end_form()}
|
||||
</div>
|
||||
</b-modal>
|
||||
% endif
|
||||
</wutta-tool-panel>
|
||||
</%def>
|
||||
|
||||
<%def name="modify_vue_vars()">
|
||||
${parent.modify_vue_vars()}
|
||||
<script>
|
||||
|
||||
% if instance.status.name == 'READY' and master.has_perm('resolve') and not use_local_products:
|
||||
|
||||
ThisPageData.resolveShowDialog = false
|
||||
ThisPageData.resolveProductID = null
|
||||
ThisPageData.resolveSubmitting = false
|
||||
|
||||
ThisPage.methods.resolveInit = function() {
|
||||
this.resolveShowDialog = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.productLookup.focus()
|
||||
})
|
||||
}
|
||||
|
||||
% endif
|
||||
|
||||
% if instance.status.name == 'READY' and master.has_perm('ignore') and not use_local_products:
|
||||
|
||||
ThisPageData.ignoreShowDialog = false
|
||||
ThisPageData.ignoreSubmitting = false
|
||||
|
||||
% endif
|
||||
|
||||
</script>
|
||||
</%def>
|
53
src/sideshow/web/templates/sideshow-components.mako
Normal file
53
src/sideshow/web/templates/sideshow-components.mako
Normal file
|
@ -0,0 +1,53 @@
|
|||
|
||||
<%def name="make_sideshow_components()">
|
||||
${self.make_sideshow_product_lookup_component()}
|
||||
</%def>
|
||||
|
||||
<%def name="make_sideshow_product_lookup_component()">
|
||||
<script type="text/x-template" id="sideshow-product-lookup-template">
|
||||
<wutta-autocomplete ref="autocomplete"
|
||||
v-model="productID"
|
||||
:display="display"
|
||||
placeholder="Enter brand, description etc."
|
||||
:service-url="serviceUrl"
|
||||
@input="val => $emit('input', val)" />
|
||||
</script>
|
||||
<script>
|
||||
const SideshowProductLookup = {
|
||||
template: '#sideshow-product-lookup-template',
|
||||
|
||||
props: {
|
||||
|
||||
// this should contain the productID, or null
|
||||
// caller specifies this as `v-model`
|
||||
// component emits @input event when value changes
|
||||
value: String,
|
||||
|
||||
// caller must specify initial display string, if the
|
||||
// (v-model) value is not empty when component loads
|
||||
display: String,
|
||||
|
||||
// the url from which search results are obtained
|
||||
serviceUrl: {
|
||||
type: String,
|
||||
default: '${url('orders.product_autocomplete')}',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
productID: this.value,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
focus() {
|
||||
this.$refs.autocomplete.focus()
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
Vue.component('sideshow-product-lookup', SideshowProductLookup)
|
||||
</script>
|
||||
</%def>
|
|
@ -793,6 +793,8 @@ class OrderView(MasterView):
|
|||
'department_id': row.department_id,
|
||||
'department_name': row.department_name,
|
||||
'special_order': row.special_order,
|
||||
'vendor_name': row.vendor_name,
|
||||
'vendor_item_code': row.vendor_item_code,
|
||||
'case_size': float(row.case_size) if row.case_size is not None else None,
|
||||
'order_qty': float(row.order_qty),
|
||||
'order_uom': row.order_uom,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Sideshow -- Case/Special Order Tracker
|
||||
# Copyright © 2024 Lance Edgar
|
||||
# Copyright © 2024-2025 Lance Edgar
|
||||
#
|
||||
# This file is part of Sideshow.
|
||||
#
|
||||
|
@ -27,6 +27,7 @@ Views for Products
|
|||
from wuttaweb.views import MasterView
|
||||
from wuttaweb.forms.schema import UserRef, WuttaEnum, WuttaMoney, WuttaQuantity
|
||||
|
||||
from sideshow.enum import PendingProductStatus
|
||||
from sideshow.db.model import LocalProduct, PendingProduct
|
||||
|
||||
|
||||
|
@ -254,7 +255,12 @@ class PendingProductView(MasterView):
|
|||
'created_by',
|
||||
]
|
||||
|
||||
sort_defaults = 'scancode'
|
||||
sort_defaults = ('created', 'desc')
|
||||
|
||||
filter_defaults = {
|
||||
'status': {'active': True,
|
||||
'value': PendingProductStatus.READY.name},
|
||||
}
|
||||
|
||||
form_fields = [
|
||||
'product_id',
|
||||
|
@ -271,7 +277,6 @@ class PendingProductView(MasterView):
|
|||
'unit_price_reg',
|
||||
'special_order',
|
||||
'notes',
|
||||
'status',
|
||||
'created',
|
||||
'created_by',
|
||||
'orders',
|
||||
|
@ -291,7 +296,7 @@ class PendingProductView(MasterView):
|
|||
g.set_renderer('unit_price_reg', 'currency')
|
||||
|
||||
# status
|
||||
g.set_renderer('status', self.grid_render_enum, enum=enum.PendingProductStatus)
|
||||
g.set_enum('status', enum.PendingProductStatus)
|
||||
|
||||
# links
|
||||
g.set_link('scancode')
|
||||
|
@ -299,6 +304,12 @@ class PendingProductView(MasterView):
|
|||
g.set_link('description')
|
||||
g.set_link('size')
|
||||
|
||||
def grid_row_class(self, product, data, i):
|
||||
""" """
|
||||
enum = self.app.enum
|
||||
if product.status == enum.PendingProductStatus.IGNORED:
|
||||
return 'has-background-warning'
|
||||
|
||||
def configure_form(self, f):
|
||||
""" """
|
||||
super().configure_form(f)
|
||||
|
@ -317,13 +328,6 @@ class PendingProductView(MasterView):
|
|||
# notes
|
||||
f.set_widget('notes', 'notes')
|
||||
|
||||
# status
|
||||
if self.creating:
|
||||
f.remove('status')
|
||||
else:
|
||||
f.set_node('status', WuttaEnum(self.request, enum.PendingProductStatus))
|
||||
f.set_readonly('status')
|
||||
|
||||
# created
|
||||
if self.creating:
|
||||
f.remove('created')
|
||||
|
@ -417,6 +421,19 @@ class PendingProductView(MasterView):
|
|||
|
||||
return grid
|
||||
|
||||
def get_template_context(self, context):
|
||||
""" """
|
||||
enum = self.app.enum
|
||||
|
||||
if self.viewing:
|
||||
product = context['instance']
|
||||
if (product.status == enum.PendingProductStatus.READY
|
||||
and self.has_any_perm('resolve', 'ignore')):
|
||||
handler = self.app.get_batch_handler('neworder')
|
||||
context['use_local_products'] = handler.use_local_products()
|
||||
|
||||
return context
|
||||
|
||||
def delete_instance(self, product):
|
||||
""" """
|
||||
|
||||
|
@ -431,6 +448,98 @@ class PendingProductView(MasterView):
|
|||
# go ahead and delete per usual
|
||||
super().delete_instance(product)
|
||||
|
||||
def resolve(self):
|
||||
"""
|
||||
View to "resolve" a :term:`pending product` with the real
|
||||
:term:`external product`.
|
||||
|
||||
This view requires POST, with ``product_id`` referencing the
|
||||
desired external product.
|
||||
|
||||
It will call
|
||||
:meth:`~sideshow.batch.neworder.NewOrderBatchHandler.get_product_info_external()`
|
||||
to fetch product info, then with that it calls
|
||||
:meth:`~sideshow.orders.OrderHandler.resolve_pending_product()`
|
||||
to update related :term:`order items <order item>` etc.
|
||||
|
||||
See also :meth:`ignore()`.
|
||||
"""
|
||||
enum = self.app.enum
|
||||
session = self.Session()
|
||||
product = self.get_instance()
|
||||
|
||||
if product.status != enum.PendingProductStatus.READY:
|
||||
self.request.session.flash("pending product does not have 'ready' status!", 'error')
|
||||
return self.redirect(self.get_action_url('view', product))
|
||||
|
||||
product_id = self.request.POST.get('product_id')
|
||||
if not product_id:
|
||||
self.request.session.flash("must specify valid product_id", 'error')
|
||||
return self.redirect(self.get_action_url('view', product))
|
||||
|
||||
batch_handler = self.app.get_batch_handler('neworder')
|
||||
order_handler = self.app.get_order_handler()
|
||||
|
||||
info = batch_handler.get_product_info_external(session, product_id)
|
||||
order_handler.resolve_pending_product(product, info, self.request.user)
|
||||
|
||||
return self.redirect(self.get_action_url('view', product))
|
||||
|
||||
def ignore(self):
|
||||
"""
|
||||
View to "ignore" a :term:`pending product` so the user is no
|
||||
longer prompted to resolve it.
|
||||
|
||||
This view requires POST; it merely sets the product status to
|
||||
"ignored".
|
||||
|
||||
See also :meth:`resolve()`.
|
||||
"""
|
||||
enum = self.app.enum
|
||||
product = self.get_instance()
|
||||
|
||||
if product.status != enum.PendingProductStatus.READY:
|
||||
self.request.session.flash("pending product does not have 'ready' status!", 'error')
|
||||
return self.redirect(self.get_action_url('view', product))
|
||||
|
||||
product.status = enum.PendingProductStatus.IGNORED
|
||||
return self.redirect(self.get_action_url('view', product))
|
||||
|
||||
@classmethod
|
||||
def defaults(cls, config):
|
||||
""" """
|
||||
cls._defaults(config)
|
||||
cls._pending_product_defaults(config)
|
||||
|
||||
@classmethod
|
||||
def _pending_product_defaults(cls, config):
|
||||
route_prefix = cls.get_route_prefix()
|
||||
permission_prefix = cls.get_permission_prefix()
|
||||
instance_url_prefix = cls.get_instance_url_prefix()
|
||||
model_title = cls.get_model_title()
|
||||
|
||||
# resolve
|
||||
config.add_wutta_permission(permission_prefix,
|
||||
f'{permission_prefix}.resolve',
|
||||
f"Resolve {model_title}")
|
||||
config.add_route(f'{route_prefix}.resolve',
|
||||
f'{instance_url_prefix}/resolve',
|
||||
request_method='POST')
|
||||
config.add_view(cls, attr='resolve',
|
||||
route_name=f'{route_prefix}.resolve',
|
||||
permission=f'{permission_prefix}.resolve')
|
||||
|
||||
# ignore
|
||||
config.add_wutta_permission(permission_prefix,
|
||||
f'{permission_prefix}.ignore',
|
||||
f"Ignore {model_title}")
|
||||
config.add_route(f'{route_prefix}.ignore',
|
||||
f'{instance_url_prefix}/ignore',
|
||||
request_method='POST')
|
||||
config.add_view(cls, attr='ignore',
|
||||
route_name=f'{route_prefix}.ignore',
|
||||
permission=f'{permission_prefix}.ignore')
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
|
|
@ -1219,7 +1219,7 @@ class TestNewOrderBatchHandler(DataTestCase):
|
|||
self.assertEqual(local.full_name, "Chuck Norris")
|
||||
self.assertEqual(local.phone_number, '555-1234')
|
||||
|
||||
def test_make_local_products(self):
|
||||
def test_process_pending_products(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
handler = self.make_handler()
|
||||
|
@ -1253,7 +1253,7 @@ class TestNewOrderBatchHandler(DataTestCase):
|
|||
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)
|
||||
handler.process_pending_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)
|
||||
|
@ -1266,7 +1266,7 @@ class TestNewOrderBatchHandler(DataTestCase):
|
|||
self.assertEqual(local.unit_price_reg, decimal.Decimal('5.99'))
|
||||
|
||||
# trying again does nothing
|
||||
handler.make_local_products(batch, batch.rows)
|
||||
handler.process_pending_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)
|
||||
|
@ -1291,24 +1291,26 @@ class TestNewOrderBatchHandler(DataTestCase):
|
|||
), 1, enum.ORDER_UOM_UNIT)
|
||||
self.session.flush()
|
||||
|
||||
# should do nothing if local products disabled
|
||||
# should update status if using external products
|
||||
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.assertEqual(row.pending_product.status, enum.PendingProductStatus.PENDING)
|
||||
self.assertIsNone(row.local_product)
|
||||
handler.make_local_products(batch, batch.rows)
|
||||
handler.process_pending_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.assertEqual(row.pending_product.status, enum.PendingProductStatus.READY)
|
||||
self.assertIsNone(row.local_product)
|
||||
|
||||
# but things happen by default, since local products enabled
|
||||
# but if using local products (the default), pending is converted to local
|
||||
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)
|
||||
handler.process_pending_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)
|
||||
|
|
|
@ -70,6 +70,75 @@ class TestOrderHandler(DataTestCase):
|
|||
self.assertEqual(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_EXPIRED), 'warning')
|
||||
self.assertEqual(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_INACTIVE), 'warning')
|
||||
|
||||
def test_resolve_pending_product(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
handler = self.make_handler()
|
||||
|
||||
# sample data
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
pending = model.PendingProduct(description='vinegar', unit_price_reg=5.99,
|
||||
status=enum.PendingProductStatus.PENDING,
|
||||
created_by=user)
|
||||
self.session.add(pending)
|
||||
order = model.Order(order_id=100, customer_name="Fred Flintstone", created_by=user)
|
||||
item = model.OrderItem(pending_product=pending,
|
||||
order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_READY)
|
||||
order.items.append(item)
|
||||
self.session.add(order)
|
||||
self.session.flush()
|
||||
|
||||
info = {
|
||||
'product_id': '07430500132',
|
||||
'scancode': '07430500132',
|
||||
'brand_name': "Bragg's",
|
||||
'description': "Apple Cider Vinegar",
|
||||
'size': "32oz",
|
||||
'weighed': False,
|
||||
'department_id': None,
|
||||
'department_name': None,
|
||||
'special_order': False,
|
||||
'vendor_name': None,
|
||||
'vendor_item_code': None,
|
||||
'case_size': 12,
|
||||
'unit_cost': 2.99,
|
||||
'unit_price_reg': 5.99,
|
||||
}
|
||||
|
||||
# first try fails b/c pending status
|
||||
self.assertEqual(len(item.events), 0)
|
||||
self.assertRaises(ValueError, handler.resolve_pending_product, pending, info, user)
|
||||
|
||||
# resolves okay if ready status
|
||||
pending.status = enum.PendingProductStatus.READY
|
||||
handler.resolve_pending_product(pending, info, user)
|
||||
self.assertEqual(len(item.events), 1)
|
||||
self.assertEqual(item.events[0].type_code, enum.ORDER_ITEM_EVENT_PRODUCT_RESOLVED)
|
||||
self.assertIsNone(item.events[0].note)
|
||||
|
||||
# more sample data
|
||||
pending2 = model.PendingProduct(description='vinegar', unit_price_reg=5.99,
|
||||
status=enum.PendingProductStatus.READY,
|
||||
created_by=user)
|
||||
self.session.add(pending2)
|
||||
order2 = model.Order(order_id=101, customer_name="Wilma Flintstone", created_by=user)
|
||||
item2 = model.OrderItem(pending_product=pending2,
|
||||
order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_READY)
|
||||
order2.items.append(item2)
|
||||
self.session.add(order2)
|
||||
self.session.flush()
|
||||
|
||||
# resolve with extra note
|
||||
handler.resolve_pending_product(pending2, info, user, note='hello world')
|
||||
self.assertEqual(len(item2.events), 2)
|
||||
self.assertEqual(item2.events[0].type_code, enum.ORDER_ITEM_EVENT_PRODUCT_RESOLVED)
|
||||
self.assertIsNone(item2.events[0].note)
|
||||
self.assertEqual(item2.events[1].type_code, enum.ORDER_ITEM_EVENT_NOTE_ADDED)
|
||||
self.assertEqual(item2.events[1].note, "hello world")
|
||||
|
||||
def test_process_placement(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
|
|
|
@ -1144,6 +1144,8 @@ class TestOrderView(WebTestCase):
|
|||
row.department_id = 1
|
||||
row.department_name = "Bricks & Mortar"
|
||||
row.special_order = False
|
||||
row.vendor_name = 'Acme Distributors'
|
||||
row.vendor_item_code = '1234'
|
||||
row.case_size = None
|
||||
row.unit_cost = decimal.Decimal('599.99')
|
||||
row.unit_price_reg = decimal.Decimal('999.99')
|
||||
|
@ -1159,7 +1161,8 @@ class TestOrderView(WebTestCase):
|
|||
self.assertEqual(data['product_scancode'], '012345')
|
||||
self.assertEqual(data['product_full_description'], 'Acme Bricks 1 ton')
|
||||
self.assertIsNone(data['case_size'])
|
||||
self.assertNotIn('vendor_name', data) # TODO
|
||||
self.assertEqual(data['vendor_name'], 'Acme Distributors')
|
||||
self.assertEqual(data['vendor_item_code'], '1234')
|
||||
self.assertEqual(data['order_qty'], 1)
|
||||
self.assertEqual(data['order_uom'], 'EA')
|
||||
self.assertEqual(data['order_qty_display'], '1 Units')
|
||||
|
|
|
@ -132,6 +132,19 @@ class TestPendingProductView(WebTestCase):
|
|||
self.assertIn('brand_name', grid.linked_columns)
|
||||
self.assertIn('description', grid.linked_columns)
|
||||
|
||||
def test_grid_row_class(self):
|
||||
enum = self.app.enum
|
||||
model = self.app.model
|
||||
view = self.make_view()
|
||||
product = model.PendingProduct()
|
||||
|
||||
# null by default
|
||||
self.assertIsNone(view.grid_row_class(product, {}, 1))
|
||||
|
||||
# warning for ignored
|
||||
product.status = enum.PendingProductStatus.IGNORED
|
||||
self.assertEqual(view.grid_row_class(product, {}, 1), 'has-background-warning')
|
||||
|
||||
def test_configure_form(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
|
@ -141,7 +154,6 @@ class TestPendingProductView(WebTestCase):
|
|||
with patch.object(view, 'creating', new=True):
|
||||
form = view.make_form(model_class=model.PendingProduct)
|
||||
view.configure_form(form)
|
||||
self.assertNotIn('status', form)
|
||||
self.assertNotIn('created', form)
|
||||
self.assertNotIn('created_by', form)
|
||||
|
||||
|
@ -216,6 +228,39 @@ class TestPendingProductView(WebTestCase):
|
|||
self.assertEqual(len(grid.actions), 1)
|
||||
self.assertEqual(grid.actions[0].key, 'view')
|
||||
|
||||
def test_get_template_context(self):
|
||||
enum = self.app.enum
|
||||
model = self.app.model
|
||||
view = self.make_view()
|
||||
product = model.PendingProduct(status=enum.PendingProductStatus.PENDING)
|
||||
orig_context = {'instance': product}
|
||||
|
||||
# local setting omitted by default
|
||||
context = view.get_template_context(orig_context)
|
||||
self.assertNotIn('use_local_products', context)
|
||||
|
||||
# still omitted even though 'viewing'
|
||||
with patch.object(view, 'viewing', new=True):
|
||||
context = view.get_template_context(orig_context)
|
||||
self.assertNotIn('use_local_products', context)
|
||||
|
||||
# still omitted even though correct status
|
||||
product.status = enum.PendingProductStatus.READY
|
||||
context = view.get_template_context(orig_context)
|
||||
self.assertNotIn('use_local_products', context)
|
||||
|
||||
# no longer omitted if user has perm
|
||||
with patch.object(self.request, 'is_root', new=True):
|
||||
context = view.get_template_context(orig_context)
|
||||
self.assertIn('use_local_products', context)
|
||||
# nb. true by default
|
||||
self.assertTrue(context['use_local_products'])
|
||||
|
||||
# accurately reflects config
|
||||
self.config.setdefault('sideshow.orders.use_local_products', 'false')
|
||||
context = view.get_template_context(orig_context)
|
||||
self.assertFalse(context['use_local_products'])
|
||||
|
||||
def test_delete_instance(self):
|
||||
self.pyramid_config.add_route('pending_products.view', '/pending/products/{uuid}')
|
||||
model = self.app.model
|
||||
|
@ -259,3 +304,117 @@ class TestPendingProductView(WebTestCase):
|
|||
view.delete_instance(product)
|
||||
self.session.flush()
|
||||
self.assertEqual(self.session.query(model.PendingProduct).count(), 0)
|
||||
|
||||
def test_resolve(self):
|
||||
self.pyramid_config.add_route('pending_products.view', '/pending/products/{uuid}')
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
view = self.make_view()
|
||||
|
||||
# sample data
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
product = model.PendingProduct(status=enum.PendingProductStatus.PENDING,
|
||||
created_by=user)
|
||||
self.session.add(product)
|
||||
self.session.flush()
|
||||
|
||||
info = {
|
||||
'product_id': '07430500132',
|
||||
'scancode': '07430500132',
|
||||
'brand_name': "Bragg's",
|
||||
'description': "Apple Cider Vinegar",
|
||||
'size': "32oz",
|
||||
'weighed': False,
|
||||
'department_id': None,
|
||||
'department_name': None,
|
||||
'special_order': False,
|
||||
'vendor_name': None,
|
||||
'vendor_item_code': None,
|
||||
'case_size': 12,
|
||||
'unit_cost': 2.99,
|
||||
'unit_price_reg': 5.99,
|
||||
}
|
||||
|
||||
with patch.object(view, 'Session', return_value=self.session):
|
||||
with patch.object(self.request, 'user', new=user):
|
||||
with patch.object(self.request, 'matchdict', new={'uuid': product.uuid}):
|
||||
|
||||
# flash error if wrong status
|
||||
result = view.resolve()
|
||||
self.assertIsInstance(result, HTTPFound)
|
||||
self.assertTrue(self.request.session.peek_flash('error'))
|
||||
self.assertEqual(self.request.session.pop_flash('error'),
|
||||
["pending product does not have 'ready' status!"])
|
||||
|
||||
# flash error if product_id not specified
|
||||
product.status = enum.PendingProductStatus.READY
|
||||
result = view.resolve()
|
||||
self.assertIsInstance(result, HTTPFound)
|
||||
self.assertTrue(self.request.session.peek_flash('error'))
|
||||
self.assertEqual(self.request.session.pop_flash('error'),
|
||||
["must specify valid product_id"])
|
||||
|
||||
# more sample data
|
||||
order = model.Order(order_id=100, created_by=user,
|
||||
customer_name="Fred Flintstone")
|
||||
item = model.OrderItem(pending_product=product,
|
||||
order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_READY)
|
||||
order.items.append(item)
|
||||
self.session.add(order)
|
||||
|
||||
# product + order items updated
|
||||
self.assertIsNone(product.product_id)
|
||||
self.assertEqual(product.status, enum.PendingProductStatus.READY)
|
||||
self.assertIsNone(item.product_id)
|
||||
batch_handler = NewOrderBatchHandler(self.config)
|
||||
with patch.object(batch_handler, 'get_product_info_external',
|
||||
return_value=info):
|
||||
with patch.object(self.app, 'get_batch_handler',
|
||||
return_value=batch_handler):
|
||||
with patch.object(self.request, 'POST',
|
||||
new={'product_id': '07430500132'}):
|
||||
with patch.object(batch_handler, 'get_product_info_external',
|
||||
return_value=info):
|
||||
result = view.resolve()
|
||||
self.assertIsInstance(result, HTTPFound)
|
||||
self.assertFalse(self.request.session.peek_flash('error'))
|
||||
self.assertEqual(product.product_id, '07430500132')
|
||||
self.assertEqual(product.status, enum.PendingProductStatus.RESOLVED)
|
||||
self.assertEqual(item.product_id, '07430500132')
|
||||
|
||||
def test_ignore(self):
|
||||
self.pyramid_config.add_route('pending_products.view', '/pending/products/{uuid}')
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
view = self.make_view()
|
||||
|
||||
# sample data
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
product = model.PendingProduct(status=enum.PendingProductStatus.PENDING,
|
||||
created_by=user)
|
||||
self.session.add(product)
|
||||
self.session.flush()
|
||||
|
||||
with patch.object(view, 'Session', return_value=self.session):
|
||||
with patch.object(self.request, 'user', new=user):
|
||||
with patch.object(self.request, 'matchdict', new={'uuid': product.uuid}):
|
||||
|
||||
# flash error if wrong status
|
||||
result = view.ignore()
|
||||
self.assertIsInstance(result, HTTPFound)
|
||||
self.assertTrue(self.request.session.peek_flash('error'))
|
||||
self.assertEqual(self.request.session.pop_flash('error'),
|
||||
["pending product does not have 'ready' status!"])
|
||||
|
||||
# product updated
|
||||
product.status = enum.PendingProductStatus.READY
|
||||
self.assertIsNone(product.product_id)
|
||||
self.assertEqual(product.status, enum.PendingProductStatus.READY)
|
||||
result = view.ignore()
|
||||
self.assertIsInstance(result, HTTPFound)
|
||||
self.assertFalse(self.request.session.peek_flash('error'))
|
||||
self.assertIsNone(product.product_id)
|
||||
self.assertEqual(product.status, enum.PendingProductStatus.IGNORED)
|
||||
|
|
Loading…
Reference in a new issue