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

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 'department_id': "Department ID", 

239 'product_id': "Product ID", 

240 } 

241 

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 ] 

256 

257 sort_defaults = 'scancode' 

258 

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 ] 

280 

281 def configure_grid(self, g): 

282 """ """ 

283 super().configure_grid(g) 

284 enum = self.app.enum 

285 

286 # unit_cost 

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

288 

289 # unit_price_reg 

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

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

292 

293 # status 

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

295 

296 # links 

297 g.set_link('scancode') 

298 g.set_link('brand_name') 

299 g.set_link('description') 

300 g.set_link('size') 

301 

302 def configure_form(self, f): 

303 """ """ 

304 super().configure_form(f) 

305 enum = self.app.enum 

306 product = f.model_instance 

307 

308 # product_id 

309 if self.creating: 

310 f.remove('product_id') 

311 else: 

312 f.set_readonly('product_id') 

313 

314 # unit_price_reg 

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

316 

317 # notes 

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

319 

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

326 

327 # created 

328 if self.creating: 

329 f.remove('created') 

330 else: 

331 f.set_readonly('created') 

332 

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

339 

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

345 

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

351 

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

358 

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

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

361 

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

377 

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

382 

383 return grid 

384 

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

391 

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

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

394 

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

412 

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

417 

418 return grid 

419 

420 def delete_instance(self, product): 

421 """ """ 

422 

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

430 

431 # go ahead and delete per usual 

432 super().delete_instance(product) 

433 

434 

435def defaults(config, **kwargs): 

436 base = globals() 

437 

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

439 LocalProductView.defaults(config) 

440 

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

442 PendingProductView.defaults(config) 

443 

444 

445def includeme(config): 

446 defaults(config)