Coverage for .tox/coverage/lib/python3.11/site-packages/sideshow/web/views/products.py: 100%
146 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-09 12:58 -0600
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-09 12:58 -0600
1# -*- coding: utf-8; -*-
2################################################################################
3#
4# Sideshow -- Case/Special Order Tracker
5# Copyright © 2024 Lance Edgar
6#
7# This file is part of Sideshow.
8#
9# Sideshow is free software: you can redistribute it and/or modify it
10# under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# Sideshow is distributed in the hope that it will be useful, but
15# WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17# General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with Sideshow. If not, see <http://www.gnu.org/licenses/>.
21#
22################################################################################
23"""
24Views for Products
25"""
27from wuttaweb.views import MasterView
28from wuttaweb.forms.schema import UserRef, WuttaEnum, WuttaMoney, WuttaQuantity
30from sideshow.db.model import LocalProduct, PendingProduct
33class LocalProductView(MasterView):
34 """
35 Master view for :class:`~sideshow.db.model.products.LocalProduct`;
36 route prefix is ``local_products``.
38 Notable URLs provided by this class:
40 * ``/local/products/``
41 * ``/local/products/new``
42 * ``/local/products/XXX``
43 * ``/local/products/XXX/edit``
44 * ``/local/products/XXX/delete``
45 """
46 model_class = LocalProduct
47 model_title = "Local Product"
48 route_prefix = 'local_products'
49 url_prefix = '/local/products'
51 labels = {
52 'external_id': "External ID",
53 'department_id': "Department ID",
54 }
56 grid_columns = [
57 'scancode',
58 'brand_name',
59 'description',
60 'size',
61 'department_name',
62 'special_order',
63 'case_size',
64 'unit_cost',
65 'unit_price_reg',
66 ]
68 sort_defaults = 'scancode'
70 form_fields = [
71 'external_id',
72 'scancode',
73 'brand_name',
74 'description',
75 'size',
76 'department_id',
77 'department_name',
78 'special_order',
79 'vendor_name',
80 'vendor_item_code',
81 'case_size',
82 'unit_cost',
83 'unit_price_reg',
84 'notes',
85 'orders',
86 'new_order_batches',
87 ]
89 def configure_grid(self, g):
90 """ """
91 super().configure_grid(g)
93 # unit_cost
94 g.set_renderer('unit_cost', 'currency', scale=4)
96 # unit_price_reg
97 g.set_label('unit_price_reg', "Reg. Price", column_only=True)
98 g.set_renderer('unit_price_reg', 'currency')
100 # links
101 g.set_link('scancode')
102 g.set_link('brand_name')
103 g.set_link('description')
104 g.set_link('size')
106 def configure_form(self, f):
107 """ """
108 super().configure_form(f)
109 enum = self.app.enum
110 product = f.model_instance
112 # external_id
113 if self.creating:
114 f.remove('external_id')
115 else:
116 f.set_readonly('external_id')
118 # TODO: should not have to explicitly mark these nodes
119 # as required=False.. i guess i do for now b/c i am
120 # totally overriding the node from colanderlachemy
122 # case_size
123 f.set_node('case_size', WuttaQuantity(self.request))
124 f.set_required('case_size', False)
126 # unit_cost
127 f.set_node('unit_cost', WuttaMoney(self.request, scale=4))
128 f.set_required('unit_cost', False)
130 # unit_price_reg
131 f.set_node('unit_price_reg', WuttaMoney(self.request))
132 f.set_required('unit_price_reg', False)
134 # notes
135 f.set_widget('notes', 'notes')
137 # orders
138 if self.creating or self.editing:
139 f.remove('orders')
140 else:
141 f.set_grid('orders', self.make_orders_grid(product))
143 # new_order_batches
144 if self.creating or self.editing:
145 f.remove('new_order_batches')
146 else:
147 f.set_grid('new_order_batches', self.make_new_order_batches_grid(product))
149 def make_orders_grid(self, product):
150 """
151 Make and return the grid for the Orders field.
152 """
153 model = self.app.model
154 route_prefix = self.get_route_prefix()
156 orders = set([item.order for item in product.order_items])
157 orders = sorted(orders, key=lambda order: order.order_id)
159 grid = self.make_grid(key=f'{route_prefix}.view.orders',
160 model_class=model.Order,
161 data=orders,
162 columns=[
163 'order_id',
164 'total_price',
165 'created',
166 'created_by',
167 ],
168 labels={
169 'order_id': "Order ID",
170 },
171 renderers={
172 'total_price': 'currency',
173 })
175 if self.request.has_perm('orders.view'):
176 url = lambda order, i: self.request.route_url('orders.view', uuid=order.uuid)
177 grid.add_action('view', icon='eye', url=url)
178 grid.set_link('order_id')
180 return grid
182 def make_new_order_batches_grid(self, product):
183 """
184 Make and return the grid for the New Order Batches field.
185 """
186 model = self.app.model
187 route_prefix = self.get_route_prefix()
189 batches = set([row.batch for row in product.new_order_batch_rows])
190 batches = sorted(batches, key=lambda batch: batch.id)
192 grid = self.make_grid(key=f'{route_prefix}.view.new_order_batches',
193 model_class=model.NewOrderBatch,
194 data=batches,
195 columns=[
196 'id',
197 'total_price',
198 'created',
199 'created_by',
200 'executed',
201 ],
202 labels={
203 'id': "Batch ID",
204 'status_code': "Status",
205 },
206 renderers={
207 'id': 'batch_id',
208 })
210 if self.request.has_perm('neworder_batches.view'):
211 url = lambda batch, i: self.request.route_url('neworder_batches.view', uuid=batch.uuid)
212 grid.add_action('view', icon='eye', url=url)
213 grid.set_link('id')
215 return grid
218class PendingProductView(MasterView):
219 """
220 Master view for
221 :class:`~sideshow.db.model.products.PendingProduct`; route
222 prefix is ``pending_products``.
224 Notable URLs provided by this class:
226 * ``/pending/products/``
227 * ``/pending/products/new``
228 * ``/pending/products/XXX``
229 * ``/pending/products/XXX/edit``
230 * ``/pending/products/XXX/delete``
231 """
232 model_class = PendingProduct
233 model_title = "Pending Product"
234 route_prefix = 'pending_products'
235 url_prefix = '/pending/products'
237 labels = {
238 'product_id': "Product ID",
239 }
241 grid_columns = [
242 'scancode',
243 'department_name',
244 'brand_name',
245 'description',
246 'size',
247 'unit_cost',
248 'case_size',
249 'unit_price_reg',
250 'special_order',
251 'status',
252 'created',
253 'created_by',
254 ]
256 sort_defaults = 'scancode'
258 form_fields = [
259 'product_id',
260 'scancode',
261 'department_id',
262 'department_name',
263 'brand_name',
264 'description',
265 'size',
266 'vendor_name',
267 'vendor_item_code',
268 'unit_cost',
269 'case_size',
270 'unit_price_reg',
271 'special_order',
272 'notes',
273 'status',
274 'created',
275 'created_by',
276 'orders',
277 'new_order_batches',
278 ]
280 def configure_grid(self, g):
281 """ """
282 super().configure_grid(g)
283 enum = self.app.enum
285 # unit_cost
286 g.set_renderer('unit_cost', 'currency', scale=4)
288 # unit_price_reg
289 g.set_label('unit_price_reg', "Reg. Price", column_only=True)
290 g.set_renderer('unit_price_reg', 'currency')
292 # status
293 g.set_renderer('status', self.grid_render_enum, enum=enum.PendingProductStatus)
295 # links
296 g.set_link('scancode')
297 g.set_link('brand_name')
298 g.set_link('description')
299 g.set_link('size')
301 def configure_form(self, f):
302 """ """
303 super().configure_form(f)
304 enum = self.app.enum
305 product = f.model_instance
307 # product_id
308 if self.creating:
309 f.remove('product_id')
310 else:
311 f.set_readonly('product_id')
313 # unit_price_reg
314 f.set_node('unit_price_reg', WuttaMoney(self.request))
316 # notes
317 f.set_widget('notes', 'notes')
319 # status
320 if self.creating:
321 f.remove('status')
322 else:
323 f.set_node('status', WuttaEnum(self.request, enum.PendingProductStatus))
324 f.set_readonly('status')
326 # created
327 if self.creating:
328 f.remove('created')
329 else:
330 f.set_readonly('created')
332 # created_by
333 if self.creating:
334 f.remove('created_by')
335 else:
336 f.set_node('created_by', UserRef(self.request))
337 f.set_readonly('created_by')
339 # orders
340 if self.creating or self.editing:
341 f.remove('orders')
342 else:
343 f.set_grid('orders', self.make_orders_grid(product))
345 # new_order_batches
346 if self.creating or self.editing:
347 f.remove('new_order_batches')
348 else:
349 f.set_grid('new_order_batches', self.make_new_order_batches_grid(product))
351 def make_orders_grid(self, product):
352 """
353 Make and return the grid for the Orders field.
354 """
355 model = self.app.model
356 route_prefix = self.get_route_prefix()
358 orders = set([item.order for item in product.order_items])
359 orders = sorted(orders, key=lambda order: order.order_id)
361 grid = self.make_grid(key=f'{route_prefix}.view.orders',
362 model_class=model.Order,
363 data=orders,
364 columns=[
365 'order_id',
366 'total_price',
367 'created',
368 'created_by',
369 ],
370 labels={
371 'order_id': "Order ID",
372 },
373 renderers={
374 'total_price': 'currency',
375 })
377 if self.request.has_perm('orders.view'):
378 url = lambda order, i: self.request.route_url('orders.view', uuid=order.uuid)
379 grid.add_action('view', icon='eye', url=url)
380 grid.set_link('order_id')
382 return grid
384 def make_new_order_batches_grid(self, product):
385 """
386 Make and return the grid for the New Order Batches field.
387 """
388 model = self.app.model
389 route_prefix = self.get_route_prefix()
391 batches = set([row.batch for row in product.new_order_batch_rows])
392 batches = sorted(batches, key=lambda batch: batch.id)
394 grid = self.make_grid(key=f'{route_prefix}.view.new_order_batches',
395 model_class=model.NewOrderBatch,
396 data=batches,
397 columns=[
398 'id',
399 'total_price',
400 'created',
401 'created_by',
402 'executed',
403 ],
404 labels={
405 'id': "Batch ID",
406 'status_code': "Status",
407 },
408 renderers={
409 'id': 'batch_id',
410 })
412 if self.request.has_perm('neworder_batches.view'):
413 url = lambda batch, i: self.request.route_url('neworder_batches.view', uuid=batch.uuid)
414 grid.add_action('view', icon='eye', url=url)
415 grid.set_link('id')
417 return grid
419 def delete_instance(self, product):
420 """ """
422 # avoid deleting if still referenced by new order batch(es)
423 for row in product.new_order_batch_rows:
424 if not row.batch.executed:
425 model_title = self.get_model_title()
426 self.request.session.flash(f"Cannot delete {model_title} still attached "
427 "to New Order Batch(es)", 'warning')
428 raise self.redirect(self.get_action_url('view', product))
430 # go ahead and delete per usual
431 super().delete_instance(product)
434def defaults(config, **kwargs):
435 base = globals()
437 LocalProductView = kwargs.get('LocalProductView', base['LocalProductView'])
438 LocalProductView.defaults(config)
440 PendingProductView = kwargs.get('PendingProductView', base['PendingProductView'])
441 PendingProductView.defaults(config)
444def includeme(config):
445 defaults(config)