Coverage for src/sideshow/web/views/orders.py: 100%

297 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-01-06 15:04 -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 Orders 

25""" 

26 

27import decimal 

28import logging 

29 

30import colander 

31from sqlalchemy import orm 

32 

33from wuttaweb.views import MasterView 

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

35 

36from sideshow.db.model import Order, OrderItem 

37from sideshow.batch.neworder import NewOrderBatchHandler 

38from sideshow.web.forms.schema import OrderRef, PendingCustomerRef, PendingProductRef 

39 

40 

41log = logging.getLogger(__name__) 

42 

43 

44class OrderView(MasterView): 

45 """ 

46 Master view for :class:`~sideshow.db.model.orders.Order`; route 

47 prefix is ``orders``. 

48 

49 Notable URLs provided by this class: 

50 

51 * ``/orders/`` 

52 * ``/orders/new`` 

53 * ``/orders/XXX`` 

54 * ``/orders/XXX/delete`` 

55 

56 Note that the "edit" view is not exposed here; user must perform 

57 various other workflow actions to modify the order. 

58 """ 

59 model_class = Order 

60 editable = False 

61 

62 labels = { 

63 'order_id': "Order ID", 

64 'store_id': "Store ID", 

65 'customer_id': "Customer ID", 

66 } 

67 

68 grid_columns = [ 

69 'order_id', 

70 'store_id', 

71 'customer_id', 

72 'customer_name', 

73 'total_price', 

74 'created', 

75 'created_by', 

76 ] 

77 

78 sort_defaults = ('order_id', 'desc') 

79 

80 form_fields = [ 

81 'order_id', 

82 'store_id', 

83 'customer_id', 

84 'pending_customer', 

85 'customer_name', 

86 'phone_number', 

87 'email_address', 

88 'total_price', 

89 'created', 

90 'created_by', 

91 ] 

92 

93 has_rows = True 

94 row_model_class = OrderItem 

95 rows_title = "Order Items" 

96 rows_sort_defaults = 'sequence' 

97 rows_viewable = True 

98 

99 row_labels = { 

100 'product_scancode': "Scancode", 

101 'product_brand': "Brand", 

102 'product_description': "Description", 

103 'product_size': "Size", 

104 'department_name': "Department", 

105 'order_uom': "Order UOM", 

106 'status_code': "Status", 

107 } 

108 

109 row_grid_columns = [ 

110 'sequence', 

111 'product_scancode', 

112 'product_brand', 

113 'product_description', 

114 'product_size', 

115 'department_name', 

116 'special_order', 

117 'order_qty', 

118 'order_uom', 

119 'total_price', 

120 'status_code', 

121 ] 

122 

123 PENDING_PRODUCT_ENTRY_FIELDS = [ 

124 'scancode', 

125 'department_id', 

126 'department_name', 

127 'brand_name', 

128 'description', 

129 'size', 

130 'vendor_name', 

131 'vendor_item_code', 

132 'unit_cost', 

133 'case_size', 

134 'unit_price_reg', 

135 ] 

136 

137 def configure_grid(self, g): 

138 """ """ 

139 super().configure_grid(g) 

140 

141 # order_id 

142 g.set_link('order_id') 

143 

144 # customer_id 

145 g.set_link('customer_id') 

146 

147 # customer_name 

148 g.set_link('customer_name') 

149 

150 # total_price 

151 g.set_renderer('total_price', g.render_currency) 

152 

153 def create(self): 

154 """ 

155 Instead of the typical "create" view, this displays a "wizard" 

156 of sorts. 

157 

158 Under the hood a 

159 :class:`~sideshow.db.model.batch.neworder.NewOrderBatch` is 

160 automatically created for the user when they first visit this 

161 page. They can select a customer, add items etc. 

162 

163 When user is finished assembling the order (i.e. populating 

164 the batch), they submit it. This of course executes the 

165 batch, which in turn creates a true 

166 :class:`~sideshow.db.model.orders.Order`, and user is 

167 redirected to the "view order" page. 

168 """ 

169 enum = self.app.enum 

170 self.creating = True 

171 self.batch_handler = NewOrderBatchHandler(self.config) 

172 batch = self.get_current_batch() 

173 

174 context = self.get_context_customer(batch) 

175 

176 if self.request.method == 'POST': 

177 

178 # first we check for traditional form post 

179 action = self.request.POST.get('action') 

180 post_actions = [ 

181 'start_over', 

182 'cancel_order', 

183 ] 

184 if action in post_actions: 

185 return getattr(self, action)(batch) 

186 

187 # okay then, we'll assume newer JSON-style post params 

188 data = dict(self.request.json_body) 

189 action = data.pop('action') 

190 json_actions = [ 

191 # 'assign_contact', 

192 # 'unassign_contact', 

193 # 'update_phone_number', 

194 # 'update_email_address', 

195 'set_pending_customer', 

196 # 'get_customer_info', 

197 # # 'set_customer_data', 

198 # 'get_product_info', 

199 # 'get_past_items', 

200 'add_item', 

201 'update_item', 

202 'delete_item', 

203 'submit_new_order', 

204 ] 

205 if action in json_actions: 

206 result = getattr(self, action)(batch, data) 

207 return self.json_response(result) 

208 

209 return self.json_response({'error': "unknown form action"}) 

210 

211 context.update({ 

212 'batch': batch, 

213 'normalized_batch': self.normalize_batch(batch), 

214 'order_items': [self.normalize_row(row) 

215 for row in batch.rows], 

216 

217 'allow_unknown_product': True, # TODO 

218 'default_uom_choices': self.get_default_uom_choices(), 

219 'default_uom': None, # TODO? 

220 'pending_product_required_fields': self.get_pending_product_required_fields(), 

221 }) 

222 return self.render_to_response('create', context) 

223 

224 def get_current_batch(self): 

225 """ 

226 Returns the current batch for the current user. 

227 

228 This looks for a new order batch which was created by the 

229 user, but not yet executed. If none is found, a new batch is 

230 created. 

231 

232 :returns: 

233 :class:`~sideshow.db.model.batch.neworder.NewOrderBatch` 

234 instance 

235 """ 

236 model = self.app.model 

237 session = self.Session() 

238 

239 user = self.request.user 

240 if not user: 

241 raise self.forbidden() 

242 

243 try: 

244 # there should be at most *one* new batch per user 

245 batch = session.query(model.NewOrderBatch)\ 

246 .filter(model.NewOrderBatch.created_by == user)\ 

247 .filter(model.NewOrderBatch.executed == None)\ 

248 .one() 

249 

250 except orm.exc.NoResultFound: 

251 # no batch yet for this user, so make one 

252 batch = self.batch_handler.make_batch(session, created_by=user) 

253 session.add(batch) 

254 session.flush() 

255 

256 return batch 

257 

258 def get_pending_product_required_fields(self): 

259 """ """ 

260 required = [] 

261 for field in self.PENDING_PRODUCT_ENTRY_FIELDS: 

262 require = self.config.get_bool( 

263 f'sideshow.orders.unknown_product.fields.{field}.required') 

264 if require is None and field == 'description': 

265 require = True 

266 if require: 

267 required.append(field) 

268 return required 

269 

270 def start_over(self, batch): 

271 """ 

272 This will delete the user's current batch, then redirect user 

273 back to "Create Order" page, which in turn will auto-create a 

274 new batch for them. 

275 

276 This is a "batch action" method which may be called from 

277 :meth:`create()`. 

278 """ 

279 # drop current batch 

280 self.batch_handler.do_delete(batch, self.request.user) 

281 self.Session.flush() 

282 

283 # send back to "create order" which makes new batch 

284 route_prefix = self.get_route_prefix() 

285 url = self.request.route_url(f'{route_prefix}.create') 

286 return self.redirect(url) 

287 

288 def cancel_order(self, batch): 

289 """ 

290 This will delete the user's current batch, then redirect user 

291 back to "List Orders" page. 

292 

293 This is a "batch action" method which may be called from 

294 :meth:`create()`. 

295 """ 

296 self.batch_handler.do_delete(batch, self.request.user) 

297 self.Session.flush() 

298 

299 # set flash msg just to be more obvious 

300 self.request.session.flash("New order has been deleted.") 

301 

302 # send user back to orders list, w/ no new batch generated 

303 url = self.get_index_url() 

304 return self.redirect(url) 

305 

306 def get_context_customer(self, batch): 

307 """ """ 

308 context = { 

309 'customer_id': batch.customer_id, 

310 'customer_name': batch.customer_name, 

311 'phone_number': batch.phone_number, 

312 'email_address': batch.email_address, 

313 'new_customer_name': None, 

314 'new_customer_first_name': None, 

315 'new_customer_last_name': None, 

316 'new_customer_phone': None, 

317 'new_customer_email': None, 

318 } 

319 

320 pending = batch.pending_customer 

321 if pending: 

322 context.update({ 

323 'new_customer_first_name': pending.first_name, 

324 'new_customer_last_name': pending.last_name, 

325 'new_customer_name': pending.full_name, 

326 'new_customer_phone': pending.phone_number, 

327 'new_customer_email': pending.email_address, 

328 }) 

329 

330 # figure out if customer is "known" from user's perspective. 

331 # if we have an ID then it's definitely known, otherwise if we 

332 # have a pending customer then it's definitely *not* known, 

333 # but if no pending customer yet then we can still "assume" it 

334 # is known, by default, until user specifies otherwise. 

335 if batch.customer_id: 

336 context['customer_is_known'] = True 

337 else: 

338 context['customer_is_known'] = not pending 

339 

340 return context 

341 

342 def set_pending_customer(self, batch, data): 

343 """ 

344 This will set/update the batch pending customer info. 

345 

346 This calls 

347 :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.set_pending_customer()` 

348 for the heavy lifting. 

349 

350 This is a "batch action" method which may be called from 

351 :meth:`create()`. 

352 """ 

353 data['created_by'] = self.request.user 

354 try: 

355 self.batch_handler.set_pending_customer(batch, data) 

356 except Exception as error: 

357 return {'error': self.app.render_error(error)} 

358 

359 self.Session.flush() 

360 context = self.get_context_customer(batch) 

361 return context 

362 

363 def add_item(self, batch, data): 

364 """ 

365 This adds a row to the user's current new order batch. 

366 

367 This is a "batch action" method which may be called from 

368 :meth:`create()`. 

369 """ 

370 order_qty = decimal.Decimal(data.get('order_qty') or '0') 

371 order_uom = data['order_uom'] 

372 

373 if data.get('product_is_known'): 

374 raise NotImplementedError 

375 

376 else: # unknown product; add pending 

377 pending = data['pending_product'] 

378 

379 for field in ('unit_cost', 'unit_price_reg', 'case_size'): 

380 if field in pending: 

381 try: 

382 pending[field] = decimal.Decimal(pending[field]) 

383 except decimal.InvalidOperation: 

384 return {'error': f"Invalid entry for field: {field}"} 

385 

386 pending['created_by'] = self.request.user 

387 row = self.batch_handler.add_pending_product(batch, pending, 

388 order_qty, order_uom) 

389 

390 return {'batch': self.normalize_batch(batch), 

391 'row': self.normalize_row(row)} 

392 

393 def update_item(self, batch, data): 

394 """ 

395 This updates a row in the user's current new order batch. 

396 

397 This is a "batch action" method which may be called from 

398 :meth:`create()`. 

399 """ 

400 model = self.app.model 

401 enum = self.app.enum 

402 session = self.Session() 

403 

404 uuid = data.get('uuid') 

405 if not uuid: 

406 return {'error': "Must specify a row UUID"} 

407 

408 row = session.get(model.NewOrderBatchRow, uuid) 

409 if not row: 

410 return {'error': "Row not found"} 

411 

412 if row.batch is not batch: 

413 return {'error': "Row is for wrong batch"} 

414 

415 order_qty = decimal.Decimal(data.get('order_qty') or '0') 

416 order_uom = data['order_uom'] 

417 

418 if data.get('product_is_known'): 

419 raise NotImplementedError 

420 

421 else: # pending product 

422 

423 # set these first, since row will be refreshed below 

424 row.order_qty = order_qty 

425 row.order_uom = order_uom 

426 

427 # nb. this will refresh the row 

428 self.batch_handler.set_pending_product(row, data['pending_product']) 

429 

430 return {'batch': self.normalize_batch(batch), 

431 'row': self.normalize_row(row)} 

432 

433 def delete_item(self, batch, data): 

434 """ 

435 This deletes a row from the user's current new order batch. 

436 

437 This is a "batch action" method which may be called from 

438 :meth:`create()`. 

439 """ 

440 model = self.app.model 

441 session = self.app.get_session(batch) 

442 

443 uuid = data.get('uuid') 

444 if not uuid: 

445 return {'error': "Must specify a row UUID"} 

446 

447 row = session.get(model.NewOrderBatchRow, uuid) 

448 if not row: 

449 return {'error': "Row not found"} 

450 

451 if row.batch is not batch: 

452 return {'error': "Row is for wrong batch"} 

453 

454 self.batch_handler.do_remove_row(row) 

455 session.flush() 

456 return {'batch': self.normalize_batch(batch)} 

457 

458 def submit_new_order(self, batch, data): 

459 """ 

460 This submits the user's current new order batch, hence 

461 executing the batch and creating the true order. 

462 

463 This is a "batch action" method which may be called from 

464 :meth:`create()`. 

465 """ 

466 user = self.request.user 

467 reason = self.batch_handler.why_not_execute(batch, user=user) 

468 if reason: 

469 return {'error': reason} 

470 

471 try: 

472 order = self.batch_handler.do_execute(batch, user) 

473 except Exception as error: 

474 log.warning("failed to execute new order batch: %s", batch, 

475 exc_info=True) 

476 return {'error': self.app.render_error(error)} 

477 

478 return { 

479 'next_url': self.get_action_url('view', order), 

480 } 

481 

482 def normalize_batch(self, batch): 

483 """ """ 

484 return { 

485 'uuid': batch.uuid.hex, 

486 'total_price': str(batch.total_price or 0), 

487 'total_price_display': self.app.render_currency(batch.total_price), 

488 'status_code': batch.status_code, 

489 'status_text': batch.status_text, 

490 } 

491 

492 def get_default_uom_choices(self): 

493 """ """ 

494 enum = self.app.enum 

495 return [{'key': key, 'value': val} 

496 for key, val in enum.ORDER_UOM.items()] 

497 

498 def normalize_row(self, row): 

499 """ """ 

500 enum = self.app.enum 

501 

502 data = { 

503 'uuid': row.uuid.hex, 

504 'sequence': row.sequence, 

505 'product_scancode': row.product_scancode, 

506 'product_brand': row.product_brand, 

507 'product_description': row.product_description, 

508 'product_size': row.product_size, 

509 'product_weighed': row.product_weighed, 

510 'department_display': row.department_name, 

511 'special_order': row.special_order, 

512 'case_size': self.app.render_quantity(row.case_size), 

513 'order_qty': self.app.render_quantity(row.order_qty), 

514 'order_uom': row.order_uom, 

515 'order_uom_choices': self.get_default_uom_choices(), 

516 'unit_price_quoted': float(row.unit_price_quoted) if row.unit_price_quoted is not None else None, 

517 'unit_price_quoted_display': self.app.render_currency(row.unit_price_quoted), 

518 'case_price_quoted': float(row.case_price_quoted) if row.case_price_quoted is not None else None, 

519 'case_price_quoted_display': self.app.render_currency(row.case_price_quoted), 

520 'total_price': float(row.total_price) if row.total_price is not None else None, 

521 'total_price_display': self.app.render_currency(row.total_price), 

522 'status_code': row.status_code, 

523 'status_text': row.status_text, 

524 } 

525 

526 if row.unit_price_reg: 

527 data['unit_price_reg'] = float(row.unit_price_reg) 

528 data['unit_price_reg_display'] = self.app.render_currency(row.unit_price_reg) 

529 

530 if row.unit_price_sale: 

531 data['unit_price_sale'] = float(row.unit_price_sale) 

532 data['unit_price_sale_display'] = self.app.render_currency(row.unit_price_sale) 

533 if row.sale_ends: 

534 sale_ends = row.sale_ends 

535 data['sale_ends'] = str(row.sale_ends) 

536 data['sale_ends_display'] = self.app.render_date(row.sale_ends) 

537 

538 # if row.unit_price_sale and row.unit_price_quoted == row.unit_price_sale: 

539 # data['pricing_reflects_sale'] = True 

540 

541 # TODO 

542 if row.pending_product: 

543 data['product_full_description'] = row.pending_product.full_description 

544 # else: 

545 # data['product_full_description'] = row.product_description 

546 

547 # if row.pending_product: 

548 # data['vendor_display'] = row.pending_product.vendor_name 

549 

550 if row.pending_product: 

551 pending = row.pending_product 

552 # data['vendor_display'] = pending.vendor_name 

553 data['pending_product'] = { 

554 'uuid': pending.uuid.hex, 

555 'scancode': pending.scancode, 

556 'brand_name': pending.brand_name, 

557 'description': pending.description, 

558 'size': pending.size, 

559 'department_id': pending.department_id, 

560 'department_name': pending.department_name, 

561 'unit_price_reg': float(pending.unit_price_reg) if pending.unit_price_reg is not None else None, 

562 'vendor_name': pending.vendor_name, 

563 'vendor_item_code': pending.vendor_item_code, 

564 'unit_cost': float(pending.unit_cost) if pending.unit_cost is not None else None, 

565 'case_size': float(pending.case_size) if pending.case_size is not None else None, 

566 'notes': pending.notes, 

567 'special_order': pending.special_order, 

568 } 

569 

570 # TODO: remove this 

571 data['product_key'] = row.product_scancode 

572 

573 # display text for order qty/uom 

574 if row.order_uom == enum.ORDER_UOM_CASE: 

575 if row.case_size is None: 

576 case_qty = unit_qty = '??' 

577 else: 

578 case_qty = data['case_size'] 

579 unit_qty = self.app.render_quantity(row.order_qty * row.case_size) 

580 CS = enum.ORDER_UOM[enum.ORDER_UOM_CASE] 

581 EA = enum.ORDER_UOM[enum.ORDER_UOM_UNIT] 

582 data['order_qty_display'] = (f"{data['order_qty']} {CS} " 

583 f"(&times; {case_qty} = {unit_qty} {EA})") 

584 else: 

585 unit_qty = self.app.render_quantity(row.order_qty) 

586 EA = enum.ORDER_UOM[enum.ORDER_UOM_UNIT] 

587 data['order_qty_display'] = f"{unit_qty} {EA}" 

588 

589 return data 

590 

591 def get_instance_title(self, order): 

592 """ """ 

593 return f"#{order.order_id} for {order.customer_name}" 

594 

595 def configure_form(self, f): 

596 """ """ 

597 super().configure_form(f) 

598 

599 # pending_customer 

600 f.set_node('pending_customer', PendingCustomerRef(self.request)) 

601 

602 # total_price 

603 f.set_node('total_price', WuttaMoney(self.request)) 

604 

605 # created_by 

606 f.set_node('created_by', UserRef(self.request)) 

607 f.set_readonly('created_by') 

608 

609 def get_xref_buttons(self, order): 

610 """ """ 

611 buttons = super().get_xref_buttons(order) 

612 model = self.app.model 

613 session = self.Session() 

614 

615 if self.request.has_perm('neworder_batches.view'): 

616 batch = session.query(model.NewOrderBatch)\ 

617 .filter(model.NewOrderBatch.id == order.order_id)\ 

618 .first() 

619 if batch: 

620 url = self.request.route_url('neworder_batches.view', uuid=batch.uuid) 

621 buttons.append( 

622 self.make_button("View the Batch", primary=True, icon_left='eye', url=url)) 

623 

624 return buttons 

625 

626 def get_row_grid_data(self, order): 

627 """ """ 

628 model = self.app.model 

629 session = self.Session() 

630 return session.query(model.OrderItem)\ 

631 .filter(model.OrderItem.order == order) 

632 

633 def configure_row_grid(self, g): 

634 """ """ 

635 super().configure_row_grid(g) 

636 enum = self.app.enum 

637 

638 # sequence 

639 g.set_label('sequence', "Seq.", column_only=True) 

640 g.set_link('sequence') 

641 

642 # product_scancode 

643 g.set_link('product_scancode') 

644 

645 # product_brand 

646 g.set_link('product_brand') 

647 

648 # product_description 

649 g.set_link('product_description') 

650 

651 # product_size 

652 g.set_link('product_size') 

653 

654 # TODO 

655 # order_uom 

656 #g.set_renderer('order_uom', self.grid_render_enum, enum=enum.OrderUOM) 

657 

658 # total_price 

659 g.set_renderer('total_price', g.render_currency) 

660 

661 # status_code 

662 g.set_renderer('status_code', self.render_status_code) 

663 

664 def render_status_code(self, item, key, value): 

665 """ """ 

666 enum = self.app.enum 

667 return enum.ORDER_ITEM_STATUS[value] 

668 

669 def get_row_action_url_view(self, item, i): 

670 """ """ 

671 return self.request.route_url('order_items.view', uuid=item.uuid) 

672 

673 

674class OrderItemView(MasterView): 

675 """ 

676 Master view for :class:`~sideshow.db.model.orders.OrderItem`; 

677 route prefix is ``order_items``. 

678 

679 Notable URLs provided by this class: 

680 

681 * ``/order-items/`` 

682 * ``/order-items/XXX`` 

683 

684 Note that this does not expose create, edit or delete. The user 

685 must perform various other workflow actions to modify the item. 

686 """ 

687 model_class = OrderItem 

688 model_title = "Order Item" 

689 route_prefix = 'order_items' 

690 url_prefix = '/order-items' 

691 creatable = False 

692 editable = False 

693 deletable = False 

694 

695 labels = { 

696 'order_id': "Order ID", 

697 'product_id': "Product ID", 

698 'product_scancode': "Scancode", 

699 'product_brand': "Brand", 

700 'product_description': "Description", 

701 'product_size': "Size", 

702 'department_name': "Department", 

703 'order_uom': "Order UOM", 

704 'status_code': "Status", 

705 } 

706 

707 grid_columns = [ 

708 'order_id', 

709 'customer_name', 

710 # 'sequence', 

711 'product_scancode', 

712 'product_brand', 

713 'product_description', 

714 'product_size', 

715 'department_name', 

716 'special_order', 

717 'order_qty', 

718 'order_uom', 

719 'total_price', 

720 'status_code', 

721 ] 

722 

723 sort_defaults = ('order_id', 'desc') 

724 

725 form_fields = [ 

726 'order', 

727 # 'customer_name', 

728 'sequence', 

729 'product_id', 

730 'pending_product', 

731 'product_scancode', 

732 'product_brand', 

733 'product_description', 

734 'product_size', 

735 'product_weighed', 

736 'department_id', 

737 'department_name', 

738 'special_order', 

739 'order_qty', 

740 'order_uom', 

741 'case_size', 

742 'unit_cost', 

743 'unit_price_reg', 

744 'unit_price_sale', 

745 'sale_ends', 

746 'unit_price_quoted', 

747 'case_price_quoted', 

748 'discount_percent', 

749 'total_price', 

750 'status_code', 

751 'paid_amount', 

752 'payment_transaction_number', 

753 ] 

754 

755 def get_query(self, session=None): 

756 """ """ 

757 query = super().get_query(session=session) 

758 model = self.app.model 

759 return query.join(model.Order) 

760 

761 def configure_grid(self, g): 

762 """ """ 

763 super().configure_grid(g) 

764 model = self.app.model 

765 # enum = self.app.enum 

766 

767 # order_id 

768 g.set_sorter('order_id', model.Order.order_id) 

769 g.set_renderer('order_id', self.render_order_id) 

770 g.set_link('order_id') 

771 

772 # customer_name 

773 g.set_label('customer_name', "Customer", column_only=True) 

774 

775 # # sequence 

776 # g.set_label('sequence', "Seq.", column_only=True) 

777 

778 # product_scancode 

779 g.set_link('product_scancode') 

780 

781 # product_brand 

782 g.set_link('product_brand') 

783 

784 # product_description 

785 g.set_link('product_description') 

786 

787 # product_size 

788 g.set_link('product_size') 

789 

790 # order_uom 

791 # TODO 

792 #g.set_renderer('order_uom', self.grid_render_enum, enum=enum.OrderUOM) 

793 

794 # total_price 

795 g.set_renderer('total_price', g.render_currency) 

796 

797 # status_code 

798 g.set_renderer('status_code', self.render_status_code) 

799 

800 def render_order_id(self, item, key, value): 

801 """ """ 

802 return item.order.order_id 

803 

804 def render_status_code(self, item, key, value): 

805 """ """ 

806 enum = self.app.enum 

807 return enum.ORDER_ITEM_STATUS[value] 

808 

809 def configure_form(self, f): 

810 """ """ 

811 super().configure_form(f) 

812 enum = self.app.enum 

813 

814 # order 

815 f.set_node('order', OrderRef(self.request)) 

816 

817 # pending_product 

818 f.set_node('pending_product', PendingProductRef(self.request)) 

819 

820 # order_qty 

821 f.set_node('order_qty', WuttaQuantity(self.request)) 

822 

823 # order_uom 

824 # TODO 

825 #f.set_node('order_uom', WuttaEnum(self.request, enum.OrderUOM)) 

826 

827 # case_size 

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

829 

830 # unit_price_quoted 

831 f.set_node('unit_price_quoted', WuttaMoney(self.request)) 

832 

833 # case_price_quoted 

834 f.set_node('case_price_quoted', WuttaMoney(self.request)) 

835 

836 # total_price 

837 f.set_node('total_price', WuttaMoney(self.request)) 

838 

839 # paid_amount 

840 f.set_node('paid_amount', WuttaMoney(self.request)) 

841 

842 def get_xref_buttons(self, item): 

843 """ """ 

844 buttons = super().get_xref_buttons(item) 

845 model = self.app.model 

846 

847 if self.request.has_perm('orders.view'): 

848 url = self.request.route_url('orders.view', uuid=item.order_uuid) 

849 buttons.append( 

850 self.make_button("View the Order", primary=True, icon_left='eye', url=url)) 

851 

852 return buttons 

853 

854 

855def defaults(config, **kwargs): 

856 base = globals() 

857 

858 OrderView = kwargs.get('OrderView', base['OrderView']) 

859 OrderView.defaults(config) 

860 

861 OrderItemView = kwargs.get('OrderItemView', base['OrderItemView']) 

862 OrderItemView.defaults(config) 

863 

864 

865def includeme(config): 

866 defaults(config)