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

114 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-16 07:16 -0500

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

46 default=False) 

47 

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

49 """ 

50 Return the display text for a given order quantity. 

51 

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

53 Units)"``. 

54 

55 :param order_qty: Numeric quantity. 

56 

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

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

59 

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

61 

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

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

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

65 

66 :returns: Display text. 

67 """ 

68 enum = self.app.enum 

69 

70 if order_uom == enum.ORDER_UOM_CASE: 

71 if case_size is None: 

72 case_qty = unit_qty = '??' 

73 else: 

74 case_qty = self.app.render_quantity(case_size) 

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

76 CS = enum.ORDER_UOM[enum.ORDER_UOM_CASE] 

77 EA = enum.ORDER_UOM[enum.ORDER_UOM_UNIT] 

78 order_qty = self.app.render_quantity(order_qty) 

79 times = '&times;' if html else 'x' 

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

81 

82 # units 

83 unit_qty = self.app.render_quantity(order_qty) 

84 EA = enum.ORDER_UOM[enum.ORDER_UOM_UNIT] 

85 return f"{unit_qty} {EA}" 

86 

87 def item_status_to_variant(self, status_code): 

88 """ 

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

90 

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

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

93 

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

95 

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

97 ``None``. 

98 """ 

99 enum = self.app.enum 

100 if status_code in (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 return 'warning' 

107 

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

109 """ 

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

111 product info. 

112 

113 At a high level this does 2 things: 

114 

115 * update the ``pending_product`` 

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

117 

118 The first step just sets 

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

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

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

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

123 

124 The second step will fetch all 

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

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

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

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

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

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

131 event is added for that.) 

132 

133 :param pending_product: 

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

135 resolved. 

136 

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

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

139 

140 :param user: 

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

142 is performing the action. 

143 

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

145 related order item(s). 

146 """ 

147 enum = self.app.enum 

148 model = self.app.model 

149 session = self.app.get_session(pending_product) 

150 

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

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

153 

154 info = product_info 

155 pending_product.product_id = info['product_id'] 

156 pending_product.status = enum.PendingProductStatus.RESOLVED 

157 

158 items = session.query(model.OrderItem)\ 

159 .filter(model.OrderItem.pending_product == pending_product)\ 

160 .filter(model.OrderItem.product_id == None)\ 

161 .all() 

162 

163 for item in items: 

164 item.product_id = info['product_id'] 

165 item.product_scancode = info['scancode'] 

166 item.product_brand = info['brand_name'] 

167 item.product_description = info['description'] 

168 item.product_size = info['size'] 

169 item.product_weighed = info['weighed'] 

170 item.department_id = info['department_id'] 

171 item.department_name = info['department_name'] 

172 item.special_order = info['special_order'] 

173 item.vendor_name = info['vendor_name'] 

174 item.vendor_item_code = info['vendor_item_code'] 

175 item.case_size = info['case_size'] 

176 item.unit_cost = info['unit_cost'] 

177 item.unit_price_reg = info['unit_price_reg'] 

178 

179 item.add_event(enum.ORDER_ITEM_EVENT_PRODUCT_RESOLVED, user) 

180 if note: 

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

182 

183 def process_placement(self, items, user, vendor_name=None, po_number=None, note=None): 

184 """ 

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

186 

187 This may eventually do something involving an *actual* 

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

189 but for now it does not. 

190 

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

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

193 PO number, if provided. 

194 

195 :param items: Sequence of 

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

197 

198 :param user: 

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

200 performing the action. 

201 

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

203 is placed, if known. 

204 

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

206 

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

208 order item. 

209 """ 

210 enum = self.app.enum 

211 

212 placed = None 

213 if vendor_name: 

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

215 elif po_number: 

216 placed = f"PO {po_number}" 

217 

218 for item in items: 

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

220 if note: 

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

222 item.status_code = enum.ORDER_ITEM_STATUS_PLACED 

223 

224 def process_receiving(self, items, user, vendor_name=None, 

225 invoice_number=None, po_number=None, note=None): 

226 """ 

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

228 

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

230 are "received". 

231 

232 TODO: This also should email the customer notifying their 

233 items are ready for pickup etc. 

234 

235 :param items: Sequence of 

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

237 

238 :param user: 

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

240 performing the action. 

241 

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

243 

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

245 

246 :param invoice_number: Invoice number, if known. 

247 

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

249 order item. 

250 """ 

251 enum = self.app.enum 

252 

253 received = None 

254 if invoice_number and po_number and vendor_name: 

255 received = f"invoice {invoice_number} (PO {po_number}) from vendor {vendor_name}" 

256 elif invoice_number and vendor_name: 

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

258 elif po_number and vendor_name: 

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

260 elif vendor_name: 

261 received = f"from vendor {vendor_name}" 

262 

263 for item in items: 

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

265 if note: 

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

267 item.status_code = enum.ORDER_ITEM_STATUS_RECEIVED 

268 

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

270 """ 

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

272 

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

274 are "ready" (again) for placement. 

275 

276 :param items: Sequence of 

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

278 

279 :param user: 

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

281 performing the action. 

282 

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

284 order item. 

285 """ 

286 enum = self.app.enum 

287 

288 for item in items: 

289 item.add_event(enum.ORDER_ITEM_EVENT_REORDER, user) 

290 if note: 

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

292 item.status_code = enum.ORDER_ITEM_STATUS_READY 

293 

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

295 """ 

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

297 items. 

298 

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

300 are "contacted" and awaiting delivery. 

301 

302 :param items: Sequence of 

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

304 

305 :param user: 

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

307 performing the action. 

308 

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

310 order item. 

311 """ 

312 enum = self.app.enum 

313 

314 for item in items: 

315 item.add_event(enum.ORDER_ITEM_EVENT_CONTACTED, user) 

316 if note: 

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

318 item.status_code = enum.ORDER_ITEM_STATUS_CONTACTED 

319 

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

321 """ 

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

323 

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

325 "contact failed". 

326 

327 :param items: Sequence of 

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

329 

330 :param user: 

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

332 performing the action. 

333 

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

335 order item. 

336 """ 

337 enum = self.app.enum 

338 

339 for item in items: 

340 item.add_event(enum.ORDER_ITEM_EVENT_CONTACT_FAILED, user) 

341 if note: 

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

343 item.status_code = enum.ORDER_ITEM_STATUS_CONTACT_FAILED 

344 

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

346 """ 

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

348 

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

350 are "delivered". 

351 

352 :param items: Sequence of 

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

354 

355 :param user: 

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

357 performing the action. 

358 

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

360 order item. 

361 """ 

362 enum = self.app.enum 

363 

364 for item in items: 

365 item.add_event(enum.ORDER_ITEM_EVENT_DELIVERED, user) 

366 if note: 

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

368 item.status_code = enum.ORDER_ITEM_STATUS_DELIVERED 

369 

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

371 """ 

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

373 

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

375 are "restocked". 

376 

377 :param items: Sequence of 

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

379 

380 :param user: 

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

382 performing the action. 

383 

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

385 order item. 

386 """ 

387 enum = self.app.enum 

388 

389 for item in items: 

390 item.add_event(enum.ORDER_ITEM_EVENT_RESTOCKED, user) 

391 if note: 

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

393 item.status_code = enum.ORDER_ITEM_STATUS_RESTOCKED