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

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""" 

26 

27from wuttaweb.views import MasterView 

28from wuttaweb.forms.schema import UserRef, WuttaEnum, WuttaMoney, WuttaQuantity 

29 

30from sideshow.db.model import LocalProduct, PendingProduct 

31 

32 

33class LocalProductView(MasterView): 

34 """ 

35 Master view for :class:`~sideshow.db.model.products.LocalProduct`; 

36 route prefix is ``local_products``. 

37 

38 Notable URLs provided by this class: 

39 

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' 

50 

51 labels = { 

52 'external_id': "External ID", 

53 'department_id': "Department ID", 

54 } 

55 

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 ] 

67 

68 sort_defaults = 'scancode' 

69 

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 ] 

88 

89 def configure_grid(self, g): 

90 """ """ 

91 super().configure_grid(g) 

92 

93 # unit_cost 

94 g.set_renderer('unit_cost', 'currency', scale=4) 

95 

96 # unit_price_reg 

97 g.set_label('unit_price_reg', "Reg. Price", column_only=True) 

98 g.set_renderer('unit_price_reg', 'currency') 

99 

100 # links 

101 g.set_link('scancode') 

102 g.set_link('brand_name') 

103 g.set_link('description') 

104 g.set_link('size') 

105 

106 def configure_form(self, f): 

107 """ """ 

108 super().configure_form(f) 

109 enum = self.app.enum 

110 product = f.model_instance 

111 

112 # external_id 

113 if self.creating: 

114 f.remove('external_id') 

115 else: 

116 f.set_readonly('external_id') 

117 

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 

121 

122 # case_size 

123 f.set_node('case_size', WuttaQuantity(self.request)) 

124 f.set_required('case_size', False) 

125 

126 # unit_cost 

127 f.set_node('unit_cost', WuttaMoney(self.request, scale=4)) 

128 f.set_required('unit_cost', False) 

129 

130 # unit_price_reg 

131 f.set_node('unit_price_reg', WuttaMoney(self.request)) 

132 f.set_required('unit_price_reg', False) 

133 

134 # notes 

135 f.set_widget('notes', 'notes') 

136 

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)) 

142 

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)) 

148 

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() 

155 

156 orders = set([item.order for item in product.order_items]) 

157 orders = sorted(orders, key=lambda order: order.order_id) 

158 

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 }) 

174 

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') 

179 

180 return grid 

181 

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() 

188 

189 batches = set([row.batch for row in product.new_order_batch_rows]) 

190 batches = sorted(batches, key=lambda batch: batch.id) 

191 

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 }) 

209 

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') 

214 

215 return grid 

216 

217 

218class PendingProductView(MasterView): 

219 """ 

220 Master view for 

221 :class:`~sideshow.db.model.products.PendingProduct`; route 

222 prefix is ``pending_products``. 

223 

224 Notable URLs provided by this class: 

225 

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' 

236 

237 labels = { 

238 'product_id': "Product ID", 

239 } 

240 

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 ] 

255 

256 sort_defaults = 'scancode' 

257 

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 ] 

279 

280 def configure_grid(self, g): 

281 """ """ 

282 super().configure_grid(g) 

283 enum = self.app.enum 

284 

285 # unit_cost 

286 g.set_renderer('unit_cost', 'currency', scale=4) 

287 

288 # unit_price_reg 

289 g.set_label('unit_price_reg', "Reg. Price", column_only=True) 

290 g.set_renderer('unit_price_reg', 'currency') 

291 

292 # status 

293 g.set_renderer('status', self.grid_render_enum, enum=enum.PendingProductStatus) 

294 

295 # links 

296 g.set_link('scancode') 

297 g.set_link('brand_name') 

298 g.set_link('description') 

299 g.set_link('size') 

300 

301 def configure_form(self, f): 

302 """ """ 

303 super().configure_form(f) 

304 enum = self.app.enum 

305 product = f.model_instance 

306 

307 # product_id 

308 if self.creating: 

309 f.remove('product_id') 

310 else: 

311 f.set_readonly('product_id') 

312 

313 # unit_price_reg 

314 f.set_node('unit_price_reg', WuttaMoney(self.request)) 

315 

316 # notes 

317 f.set_widget('notes', 'notes') 

318 

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') 

325 

326 # created 

327 if self.creating: 

328 f.remove('created') 

329 else: 

330 f.set_readonly('created') 

331 

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') 

338 

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)) 

344 

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)) 

350 

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() 

357 

358 orders = set([item.order for item in product.order_items]) 

359 orders = sorted(orders, key=lambda order: order.order_id) 

360 

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 }) 

376 

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') 

381 

382 return grid 

383 

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() 

390 

391 batches = set([row.batch for row in product.new_order_batch_rows]) 

392 batches = sorted(batches, key=lambda batch: batch.id) 

393 

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 }) 

411 

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') 

416 

417 return grid 

418 

419 def delete_instance(self, product): 

420 """ """ 

421 

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)) 

429 

430 # go ahead and delete per usual 

431 super().delete_instance(product) 

432 

433 

434def defaults(config, **kwargs): 

435 base = globals() 

436 

437 LocalProductView = kwargs.get('LocalProductView', base['LocalProductView']) 

438 LocalProductView.defaults(config) 

439 

440 PendingProductView = kwargs.get('PendingProductView', base['PendingProductView']) 

441 PendingProductView.defaults(config) 

442 

443 

444def includeme(config): 

445 defaults(config)