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
« 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"""
27from wuttjamaican.app import GenericHandler
30class OrderHandler(GenericHandler):
31 """
32 Base class and default implementation for the :term:`order
33 handler`.
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 """
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)
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.
52 Default logic will return something like ``"3 Cases (x 6 = 18
53 Units)"``.
55 :param order_qty: Numeric quantity.
57 :param order_uom: An order UOM constant; should be something
58 from :data:`~sideshow.enum.ORDER_UOM`.
60 :param case_size: Case size for the product, if known.
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 ``×``.
66 :returns: Display text.
67 """
68 enum = self.app.enum
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 = '×' if html else 'x'
80 return (f"{order_qty} {CS} ({times} {case_qty} = {unit_qty} {EA})")
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}"
87 def item_status_to_variant(self, status_code):
88 """
89 Return a Buefy style variant for the given status code.
91 Default logic will return ``None`` for "normal" item status,
92 but may return ``'warning'`` for some (e.g. canceled).
94 :param status_code: The status code for an order item.
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'
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.
113 At a high level this does 2 things:
115 * update the ``pending_product``
116 * find and update any related :term:`order item(s) <order item>`
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.
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.)
133 :param pending_product:
134 :class:`~sideshow.db.model.products.PendingProduct` to be
135 resolved.
137 :param product_info: Dict of product info, as obtained from
138 :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.get_product_info_external()`.
140 :param user:
141 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
142 is performing the action.
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)
151 if pending_product.status != enum.PendingProductStatus.READY:
152 raise ValueError("pending product does not have 'ready' status")
154 info = product_info
155 pending_product.product_id = info['product_id']
156 pending_product.status = enum.PendingProductStatus.RESOLVED
158 items = session.query(model.OrderItem)\
159 .filter(model.OrderItem.pending_product == pending_product)\
160 .filter(model.OrderItem.product_id == None)\
161 .all()
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']
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)
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.
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.
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.
195 :param items: Sequence of
196 :class:`~sideshow.db.model.orders.OrderItem` records.
198 :param user:
199 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User`
200 performing the action.
202 :param vendor_name: Name of the vendor to which purchase order
203 is placed, if known.
205 :param po_number: Purchase order number, if known.
207 :param note: Optional *additional* note to be attached to each
208 order item.
209 """
210 enum = self.app.enum
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}"
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
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.
229 This will update the status for each item, to indicate they
230 are "received".
232 TODO: This also should email the customer notifying their
233 items are ready for pickup etc.
235 :param items: Sequence of
236 :class:`~sideshow.db.model.orders.OrderItem` records.
238 :param user:
239 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User`
240 performing the action.
242 :param vendor_name: Name of the vendor, if known.
244 :param po_number: Purchase order number, if known.
246 :param invoice_number: Invoice number, if known.
248 :param note: Optional *additional* note to be attached to each
249 order item.
250 """
251 enum = self.app.enum
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}"
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
269 def process_reorder(self, items, user, note=None):
270 """
271 Process the "reorder" step for the given order items.
273 This will update the status for each item, to indicate they
274 are "ready" (again) for placement.
276 :param items: Sequence of
277 :class:`~sideshow.db.model.orders.OrderItem` records.
279 :param user:
280 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User`
281 performing the action.
283 :param note: Optional *additional* note to be attached to each
284 order item.
285 """
286 enum = self.app.enum
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
294 def process_contact_success(self, items, user, note=None):
295 """
296 Process the "successful contact" step for the given order
297 items.
299 This will update the status for each item, to indicate they
300 are "contacted" and awaiting delivery.
302 :param items: Sequence of
303 :class:`~sideshow.db.model.orders.OrderItem` records.
305 :param user:
306 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User`
307 performing the action.
309 :param note: Optional *additional* note to be attached to each
310 order item.
311 """
312 enum = self.app.enum
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
320 def process_contact_failure(self, items, user, note=None):
321 """
322 Process the "failed contact" step for the given order items.
324 This will update the status for each item, to indicate
325 "contact failed".
327 :param items: Sequence of
328 :class:`~sideshow.db.model.orders.OrderItem` records.
330 :param user:
331 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User`
332 performing the action.
334 :param note: Optional *additional* note to be attached to each
335 order item.
336 """
337 enum = self.app.enum
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
345 def process_delivery(self, items, user, note=None):
346 """
347 Process the "delivery" step for the given order items.
349 This will update the status for each item, to indicate they
350 are "delivered".
352 :param items: Sequence of
353 :class:`~sideshow.db.model.orders.OrderItem` records.
355 :param user:
356 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User`
357 performing the action.
359 :param note: Optional *additional* note to be attached to each
360 order item.
361 """
362 enum = self.app.enum
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
370 def process_restock(self, items, user, note=None):
371 """
372 Process the "restock" step for the given order items.
374 This will update the status for each item, to indicate they
375 are "restocked".
377 :param items: Sequence of
378 :class:`~sideshow.db.model.orders.OrderItem` records.
380 :param user:
381 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User`
382 performing the action.
384 :param note: Optional *additional* note to be attached to each
385 order item.
386 """
387 enum = self.app.enum
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