Coverage for .tox / coverage / lib / python3.11 / site-packages / sideshow / orders.py: 100%

115 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-15 17:10 -0600

1# -*- coding: utf-8; -*- 

2################################################################################ 

3# 

4# Sideshow -- Case/Special Order Tracker 

5# Copyright © 2024-2025 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""" 

24Sideshow Order Handler 

25""" 

26 

27from wuttjamaican.app import GenericHandler 

28 

29 

30class OrderHandler(GenericHandler): 

31 """ 

32 Base class and default implementation for the :term:`order 

33 handler`. 

34 

35 This is responsible for business logic involving customer orders 

36 after they have been first created. (The :term:`new order batch` 

37 handler is responsible for creation logic.) 

38 """ 

39 

40 def expose_store_id(self): 

41 """ 

42 Returns boolean indicating whether the ``store_id`` field 

43 should be exposed at all. This is false by default. 

44 """ 

45 return self.config.get_bool("sideshow.orders.expose_store_id", default=False) 

46 

47 def get_order_qty_uom_text(self, order_qty, order_uom, case_size=None, html=False): 

48 """ 

49 Return the display text for a given order quantity. 

50 

51 Default logic will return something like ``"3 Cases (x 6 = 18 

52 Units)"``. 

53 

54 :param order_qty: Numeric quantity. 

55 

56 :param order_uom: An order UOM constant; should be something 

57 from :data:`~sideshow.enum.ORDER_UOM`. 

58 

59 :param case_size: Case size for the product, if known. 

60 

61 :param html: Whether the return value should include any HTML. 

62 If false (the default), it will be plain text only. If 

63 true, will replace the ``x`` character with ``&times;``. 

64 

65 :returns: Display text. 

66 """ 

67 enum = self.app.enum 

68 

69 if order_uom == enum.ORDER_UOM_CASE: 

70 if case_size is None: 

71 case_qty = unit_qty = "??" 

72 else: 

73 case_qty = self.app.render_quantity(case_size) 

74 unit_qty = self.app.render_quantity(order_qty * case_size) 

75 CS = enum.ORDER_UOM[enum.ORDER_UOM_CASE] # pylint: disable=invalid-name 

76 EA = enum.ORDER_UOM[enum.ORDER_UOM_UNIT] # pylint: disable=invalid-name 

77 order_qty = self.app.render_quantity(order_qty) 

78 times = "&times;" if html else "x" 

79 return f"{order_qty} {CS} ({times} {case_qty} = {unit_qty} {EA})" 

80 

81 # units 

82 unit_qty = self.app.render_quantity(order_qty) 

83 EA = enum.ORDER_UOM[enum.ORDER_UOM_UNIT] # pylint: disable=invalid-name 

84 return f"{unit_qty} {EA}" 

85 

86 def item_status_to_variant(self, status_code): 

87 """ 

88 Return a Buefy style variant for the given status code. 

89 

90 Default logic will return ``None`` for "normal" item status, 

91 but may return ``'warning'`` for some (e.g. canceled). 

92 

93 :param status_code: The status code for an order item. 

94 

95 :returns: Style variant string (e.g. ``'warning'``) or 

96 ``None``. 

97 """ 

98 enum = self.app.enum 

99 if status_code in ( 

100 enum.ORDER_ITEM_STATUS_CANCELED, 

101 enum.ORDER_ITEM_STATUS_REFUND_PENDING, 

102 enum.ORDER_ITEM_STATUS_REFUNDED, 

103 enum.ORDER_ITEM_STATUS_RESTOCKED, 

104 enum.ORDER_ITEM_STATUS_EXPIRED, 

105 enum.ORDER_ITEM_STATUS_INACTIVE, 

106 ): 

107 return "warning" 

108 return None 

109 

110 def resolve_pending_product(self, pending_product, product_info, user, note=None): 

111 """ 

112 Resolve a :term:`pending product`, to reflect the given 

113 product info. 

114 

115 At a high level this does 2 things: 

116 

117 * update the ``pending_product`` 

118 * find and update any related :term:`order item(s) <order item>` 

119 

120 The first step just sets 

121 :attr:`~sideshow.db.model.products.PendingProduct.product_id` 

122 from the provided info, and gives it the "resolved" status. 

123 Note that it does *not* update the pending product record 

124 further, so it will not fully "match" the product info. 

125 

126 The second step will fetch all 

127 :class:`~sideshow.db.model.orders.OrderItem` records which 

128 reference the ``pending_product`` **and** which do not yet 

129 have a ``product_id`` value. For each, it then updates the 

130 order item to contain all data from ``product_info``. And 

131 finally, it adds an event to the item history, indicating who 

132 resolved and when. (If ``note`` is specified, a *second* 

133 event is added for that.) 

134 

135 :param pending_product: 

136 :class:`~sideshow.db.model.products.PendingProduct` to be 

137 resolved. 

138 

139 :param product_info: Dict of product info, as obtained from 

140 :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.get_product_info_external()`. 

141 

142 :param user: 

143 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who 

144 is performing the action. 

145 

146 :param note: Optional note to be added to event history for 

147 related order item(s). 

148 """ 

149 enum = self.app.enum 

150 model = self.app.model 

151 session = self.app.get_session(pending_product) 

152 

153 if pending_product.status != enum.PendingProductStatus.READY: 

154 raise ValueError("pending product does not have 'ready' status") 

155 

156 info = product_info 

157 pending_product.product_id = info["product_id"] 

158 pending_product.status = enum.PendingProductStatus.RESOLVED 

159 

160 items = ( 

161 session.query(model.OrderItem) 

162 .filter(model.OrderItem.pending_product == pending_product) 

163 .filter( 

164 model.OrderItem.product_id # pylint: disable=singleton-comparison 

165 == None 

166 ) 

167 .all() 

168 ) 

169 

170 for item in items: 

171 item.product_id = info["product_id"] 

172 item.product_scancode = info["scancode"] 

173 item.product_brand = info["brand_name"] 

174 item.product_description = info["description"] 

175 item.product_size = info["size"] 

176 item.product_weighed = info["weighed"] 

177 item.department_id = info["department_id"] 

178 item.department_name = info["department_name"] 

179 item.special_order = info["special_order"] 

180 item.vendor_name = info["vendor_name"] 

181 item.vendor_item_code = info["vendor_item_code"] 

182 item.case_size = info["case_size"] 

183 item.unit_cost = info["unit_cost"] 

184 item.unit_price_reg = info["unit_price_reg"] 

185 

186 item.add_event(enum.ORDER_ITEM_EVENT_PRODUCT_RESOLVED, user) 

187 if note: 

188 item.add_event(enum.ORDER_ITEM_EVENT_NOTE_ADDED, user, note=note) 

189 

190 def process_placement( # pylint: disable=too-many-arguments,too-many-positional-arguments 

191 self, items, user, vendor_name=None, po_number=None, note=None 

192 ): 

193 """ 

194 Process the "placement" step for the given order items. 

195 

196 This may eventually do something involving an *actual* 

197 purchase order, or at least a minimal representation of one, 

198 but for now it does not. 

199 

200 Instead, this will simply update each item to indicate its new 

201 status. A note will be attached to indicate the vendor and/or 

202 PO number, if provided. 

203 

204 :param items: Sequence of 

205 :class:`~sideshow.db.model.orders.OrderItem` records. 

206 

207 :param user: 

208 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` 

209 performing the action. 

210 

211 :param vendor_name: Name of the vendor to which purchase order 

212 is placed, if known. 

213 

214 :param po_number: Purchase order number, if known. 

215 

216 :param note: Optional *additional* note to be attached to each 

217 order item. 

218 """ 

219 enum = self.app.enum 

220 

221 placed = None 

222 if vendor_name: 

223 placed = f"PO {po_number or ''} for vendor {vendor_name}" 

224 elif po_number: 

225 placed = f"PO {po_number}" 

226 

227 for item in items: 

228 item.add_event(enum.ORDER_ITEM_EVENT_PLACED, user, note=placed) 

229 if note: 

230 item.add_event(enum.ORDER_ITEM_EVENT_NOTE_ADDED, user, note=note) 

231 item.status_code = enum.ORDER_ITEM_STATUS_PLACED 

232 

233 def process_receiving( # pylint: disable=too-many-arguments,too-many-positional-arguments 

234 self, 

235 items, 

236 user, 

237 vendor_name=None, 

238 invoice_number=None, 

239 po_number=None, 

240 note=None, 

241 ): 

242 """ 

243 Process the "receiving" step for the given order items. 

244 

245 This will update the status for each item, to indicate they 

246 are "received". 

247 

248 TODO: This also should email the customer notifying their 

249 items are ready for pickup etc. 

250 

251 :param items: Sequence of 

252 :class:`~sideshow.db.model.orders.OrderItem` records. 

253 

254 :param user: 

255 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` 

256 performing the action. 

257 

258 :param vendor_name: Name of the vendor, if known. 

259 

260 :param po_number: Purchase order number, if known. 

261 

262 :param invoice_number: Invoice number, if known. 

263 

264 :param note: Optional *additional* note to be attached to each 

265 order item. 

266 """ 

267 enum = self.app.enum 

268 

269 received = None 

270 if invoice_number and po_number and vendor_name: 

271 received = ( 

272 f"invoice {invoice_number} (PO {po_number}) from vendor {vendor_name}" 

273 ) 

274 elif invoice_number and vendor_name: 

275 received = f"invoice {invoice_number} from vendor {vendor_name}" 

276 elif po_number and vendor_name: 

277 received = f"PO {po_number} from vendor {vendor_name}" 

278 elif vendor_name: 

279 received = f"from vendor {vendor_name}" 

280 

281 for item in items: 

282 item.add_event(enum.ORDER_ITEM_EVENT_RECEIVED, user, note=received) 

283 if note: 

284 item.add_event(enum.ORDER_ITEM_EVENT_NOTE_ADDED, user, note=note) 

285 item.status_code = enum.ORDER_ITEM_STATUS_RECEIVED 

286 

287 def process_reorder(self, items, user, note=None): 

288 """ 

289 Process the "reorder" step for the given order items. 

290 

291 This will update the status for each item, to indicate they 

292 are "ready" (again) for placement. 

293 

294 :param items: Sequence of 

295 :class:`~sideshow.db.model.orders.OrderItem` records. 

296 

297 :param user: 

298 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` 

299 performing the action. 

300 

301 :param note: Optional *additional* note to be attached to each 

302 order item. 

303 """ 

304 enum = self.app.enum 

305 

306 for item in items: 

307 item.add_event(enum.ORDER_ITEM_EVENT_REORDER, user) 

308 if note: 

309 item.add_event(enum.ORDER_ITEM_EVENT_NOTE_ADDED, user, note=note) 

310 item.status_code = enum.ORDER_ITEM_STATUS_READY 

311 

312 def process_contact_success(self, items, user, note=None): 

313 """ 

314 Process the "successful contact" step for the given order 

315 items. 

316 

317 This will update the status for each item, to indicate they 

318 are "contacted" and awaiting delivery. 

319 

320 :param items: Sequence of 

321 :class:`~sideshow.db.model.orders.OrderItem` records. 

322 

323 :param user: 

324 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` 

325 performing the action. 

326 

327 :param note: Optional *additional* note to be attached to each 

328 order item. 

329 """ 

330 enum = self.app.enum 

331 

332 for item in items: 

333 item.add_event(enum.ORDER_ITEM_EVENT_CONTACTED, user) 

334 if note: 

335 item.add_event(enum.ORDER_ITEM_EVENT_NOTE_ADDED, user, note=note) 

336 item.status_code = enum.ORDER_ITEM_STATUS_CONTACTED 

337 

338 def process_contact_failure(self, items, user, note=None): 

339 """ 

340 Process the "failed contact" step for the given order items. 

341 

342 This will update the status for each item, to indicate 

343 "contact failed". 

344 

345 :param items: Sequence of 

346 :class:`~sideshow.db.model.orders.OrderItem` records. 

347 

348 :param user: 

349 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` 

350 performing the action. 

351 

352 :param note: Optional *additional* note to be attached to each 

353 order item. 

354 """ 

355 enum = self.app.enum 

356 

357 for item in items: 

358 item.add_event(enum.ORDER_ITEM_EVENT_CONTACT_FAILED, user) 

359 if note: 

360 item.add_event(enum.ORDER_ITEM_EVENT_NOTE_ADDED, user, note=note) 

361 item.status_code = enum.ORDER_ITEM_STATUS_CONTACT_FAILED 

362 

363 def process_delivery(self, items, user, note=None): 

364 """ 

365 Process the "delivery" step for the given order items. 

366 

367 This will update the status for each item, to indicate they 

368 are "delivered". 

369 

370 :param items: Sequence of 

371 :class:`~sideshow.db.model.orders.OrderItem` records. 

372 

373 :param user: 

374 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` 

375 performing the action. 

376 

377 :param note: Optional *additional* note to be attached to each 

378 order item. 

379 """ 

380 enum = self.app.enum 

381 

382 for item in items: 

383 item.add_event(enum.ORDER_ITEM_EVENT_DELIVERED, user) 

384 if note: 

385 item.add_event(enum.ORDER_ITEM_EVENT_NOTE_ADDED, user, note=note) 

386 item.status_code = enum.ORDER_ITEM_STATUS_DELIVERED 

387 

388 def process_restock(self, items, user, note=None): 

389 """ 

390 Process the "restock" step for the given order items. 

391 

392 This will update the status for each item, to indicate they 

393 are "restocked". 

394 

395 :param items: Sequence of 

396 :class:`~sideshow.db.model.orders.OrderItem` records. 

397 

398 :param user: 

399 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` 

400 performing the action. 

401 

402 :param note: Optional *additional* note to be attached to each 

403 order item. 

404 """ 

405 enum = self.app.enum 

406 

407 for item in items: 

408 item.add_event(enum.ORDER_ITEM_EVENT_RESTOCKED, user) 

409 if note: 

410 item.add_event(enum.ORDER_ITEM_EVENT_NOTE_ADDED, user, note=note) 

411 item.status_code = enum.ORDER_ITEM_STATUS_RESTOCKED