Coverage for src/sideshow/web/views/products.py: 0%
146 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-20 09:03 -0600
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-20 09:03 -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 'department_id': "Department ID",
239 'product_id': "Product ID",
240 }
242 grid_columns = [
243 'scancode',
244 'department_name',
245 'brand_name',
246 'description',
247 'size',
248 'unit_cost',
249 'case_size',
250 'unit_price_reg',
251 'special_order',
252 'status',
253 'created',
254 'created_by',
255 ]
257 sort_defaults = 'scancode'
259 form_fields = [
260 'product_id',
261 'scancode',
262 'department_id',
263 'department_name',
264 'brand_name',
265 'description',
266 'size',
267 'vendor_name',
268 'vendor_item_code',
269 'unit_cost',
270 'case_size',
271 'unit_price_reg',
272 'special_order',
273 'notes',
274 'status',
275 'created',
276 'created_by',
277 'orders',
278 'new_order_batches',
279 ]
281 def configure_grid(self, g):
282 """ """
283 super().configure_grid(g)
284 enum = self.app.enum
286 # unit_cost
287 g.set_renderer('unit_cost', 'currency', scale=4)
289 # unit_price_reg
290 g.set_label('unit_price_reg', "Reg. Price", column_only=True)
291 g.set_renderer('unit_price_reg', 'currency')
293 # status
294 g.set_renderer('status', self.grid_render_enum, enum=enum.PendingProductStatus)
296 # links
297 g.set_link('scancode')
298 g.set_link('brand_name')
299 g.set_link('description')
300 g.set_link('size')
302 def configure_form(self, f):
303 """ """
304 super().configure_form(f)
305 enum = self.app.enum
306 product = f.model_instance
308 # product_id
309 if self.creating:
310 f.remove('product_id')
311 else:
312 f.set_readonly('product_id')
314 # unit_price_reg
315 f.set_node('unit_price_reg', WuttaMoney(self.request))
317 # notes
318 f.set_widget('notes', 'notes')
320 # status
321 if self.creating:
322 f.remove('status')
323 else:
324 f.set_node('status', WuttaEnum(self.request, enum.PendingProductStatus))
325 f.set_readonly('status')
327 # created
328 if self.creating:
329 f.remove('created')
330 else:
331 f.set_readonly('created')
333 # created_by
334 if self.creating:
335 f.remove('created_by')
336 else:
337 f.set_node('created_by', UserRef(self.request))
338 f.set_readonly('created_by')
340 # orders
341 if self.creating or self.editing:
342 f.remove('orders')
343 else:
344 f.set_grid('orders', self.make_orders_grid(product))
346 # new_order_batches
347 if self.creating or self.editing:
348 f.remove('new_order_batches')
349 else:
350 f.set_grid('new_order_batches', self.make_new_order_batches_grid(product))
352 def make_orders_grid(self, product):
353 """
354 Make and return the grid for the Orders field.
355 """
356 model = self.app.model
357 route_prefix = self.get_route_prefix()
359 orders = set([item.order for item in product.order_items])
360 orders = sorted(orders, key=lambda order: order.order_id)
362 grid = self.make_grid(key=f'{route_prefix}.view.orders',
363 model_class=model.Order,
364 data=orders,
365 columns=[
366 'order_id',
367 'total_price',
368 'created',
369 'created_by',
370 ],
371 labels={
372 'order_id': "Order ID",
373 },
374 renderers={
375 'total_price': 'currency',
376 })
378 if self.request.has_perm('orders.view'):
379 url = lambda order, i: self.request.route_url('orders.view', uuid=order.uuid)
380 grid.add_action('view', icon='eye', url=url)
381 grid.set_link('order_id')
383 return grid
385 def make_new_order_batches_grid(self, product):
386 """
387 Make and return the grid for the New Order Batches field.
388 """
389 model = self.app.model
390 route_prefix = self.get_route_prefix()
392 batches = set([row.batch for row in product.new_order_batch_rows])
393 batches = sorted(batches, key=lambda batch: batch.id)
395 grid = self.make_grid(key=f'{route_prefix}.view.new_order_batches',
396 model_class=model.NewOrderBatch,
397 data=batches,
398 columns=[
399 'id',
400 'total_price',
401 'created',
402 'created_by',
403 'executed',
404 ],
405 labels={
406 'id': "Batch ID",
407 'status_code': "Status",
408 },
409 renderers={
410 'id': 'batch_id',
411 })
413 if self.request.has_perm('neworder_batches.view'):
414 url = lambda batch, i: self.request.route_url('neworder_batches.view', uuid=batch.uuid)
415 grid.add_action('view', icon='eye', url=url)
416 grid.set_link('id')
418 return grid
420 def delete_instance(self, product):
421 """ """
423 # avoid deleting if still referenced by new order batch(es)
424 for row in product.new_order_batch_rows:
425 if not row.batch.executed:
426 model_title = self.get_model_title()
427 self.request.session.flash(f"Cannot delete {model_title} still attached "
428 "to New Order Batch(es)", 'warning')
429 raise self.redirect(self.get_action_url('view', product))
431 # go ahead and delete per usual
432 super().delete_instance(product)
435def defaults(config, **kwargs):
436 base = globals()
438 LocalProductView = kwargs.get('LocalProductView', base['LocalProductView'])
439 LocalProductView.defaults(config)
441 PendingProductView = kwargs.get('PendingProductView', base['PendingProductView'])
442 PendingProductView.defaults(config)
445def includeme(config):
446 defaults(config)