Coverage for src/sideshow/batch/neworder.py: 92%
311 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-23 16:52 -0600
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-23 16:52 -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"""
24New Order Batch Handler
25"""
27import datetime
28import decimal
30import sqlalchemy as sa
32from wuttjamaican.batch import BatchHandler
34from sideshow.db.model import NewOrderBatch
37class NewOrderBatchHandler(BatchHandler):
38 """
39 The :term:`batch handler` for :term:`new order batches <new order
40 batch>`.
42 This is responsible for business logic around the creation of new
43 :term:`orders <order>`. A
44 :class:`~sideshow.db.model.batch.neworder.NewOrderBatch` tracks
45 all user input until they "submit" (execute) at which point an
46 :class:`~sideshow.db.model.orders.Order` is created.
48 After the batch has executed the :term:`order handler` takes over
49 responsibility for the rest of the order lifecycle.
50 """
51 model_class = NewOrderBatch
53 def use_local_customers(self):
54 """
55 Returns boolean indicating whether :term:`local customer`
56 accounts should be used. This is true by default, but may be
57 false for :term:`external customer` lookups.
58 """
59 return self.config.get_bool('sideshow.orders.use_local_customers',
60 default=True)
62 def use_local_products(self):
63 """
64 Returns boolean indicating whether :term:`local product`
65 records should be used. This is true by default, but may be
66 false for :term:`external product` lookups.
67 """
68 return self.config.get_bool('sideshow.orders.use_local_products',
69 default=True)
71 def allow_unknown_products(self):
72 """
73 Returns boolean indicating whether :term:`pending products
74 <pending product>` are allowed when creating an order.
76 This is true by default, so user can enter new/unknown product
77 when creating an order. This can be disabled, to force user
78 to choose existing local/external product.
79 """
80 return self.config.get_bool('sideshow.orders.allow_unknown_products',
81 default=True)
83 def autocomplete_customers_external(self, session, term, user=None):
84 """
85 Return autocomplete search results for :term:`external
86 customer` records.
88 There is no default logic here; subclass must implement.
90 :param session: Current app :term:`db session`.
92 :param term: Search term string from user input.
94 :param user:
95 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
96 is doing the search, if known.
98 :returns: List of search results; each should be a dict with
99 ``value`` and ``label`` keys.
100 """
101 raise NotImplementedError
103 def autocomplete_customers_local(self, session, term, user=None):
104 """
105 Return autocomplete search results for
106 :class:`~sideshow.db.model.customers.LocalCustomer` records.
108 :param session: Current app :term:`db session`.
110 :param term: Search term string from user input.
112 :param user:
113 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
114 is doing the search, if known.
116 :returns: List of search results; each should be a dict with
117 ``value`` and ``label`` keys.
118 """
119 model = self.app.model
121 # base query
122 query = session.query(model.LocalCustomer)
124 # filter query
125 criteria = [model.LocalCustomer.full_name.ilike(f'%{word}%')
126 for word in term.split()]
127 query = query.filter(sa.and_(*criteria))
129 # sort query
130 query = query.order_by(model.LocalCustomer.full_name)
132 # get data
133 # TODO: need max_results option
134 customers = query.all()
136 # get results
137 def result(customer):
138 return {'value': customer.uuid.hex,
139 'label': customer.full_name}
140 return [result(c) for c in customers]
142 def set_customer(self, batch, customer_info, user=None):
143 """
144 Set/update customer info for the batch.
146 This will first set one of the following:
148 * :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.customer_id`
149 * :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.local_customer`
150 * :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer`
152 Note that a new
153 :class:`~sideshow.db.model.customers.PendingCustomer` record
154 is created if necessary.
156 And then it will update customer-related attributes via one of:
158 * :meth:`refresh_batch_from_external_customer()`
159 * :meth:`refresh_batch_from_local_customer()`
160 * :meth:`refresh_batch_from_pending_customer()`
162 Note that ``customer_info`` may be ``None``, which will cause
163 customer attributes to be set to ``None`` also.
165 :param batch:
166 :class:`~sideshow.db.model.batch.neworder.NewOrderBatch` to
167 update.
169 :param customer_info: Customer ID string, or dict of
170 :class:`~sideshow.db.model.customers.PendingCustomer` data,
171 or ``None`` to clear the customer info.
173 :param user:
174 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
175 is performing the action. This is used to set
176 :attr:`~sideshow.db.model.customers.PendingCustomer.created_by`
177 on the pending customer, if applicable. If not specified,
178 the batch creator is assumed.
179 """
180 model = self.app.model
181 enum = self.app.enum
182 session = self.app.get_session(batch)
183 use_local = self.use_local_customers()
185 # set customer info
186 if isinstance(customer_info, str):
187 if use_local:
189 # local_customer
190 customer = session.get(model.LocalCustomer, customer_info)
191 if not customer:
192 raise ValueError("local customer not found")
193 batch.local_customer = customer
194 self.refresh_batch_from_local_customer(batch)
196 else: # external customer_id
197 batch.customer_id = customer_info
198 self.refresh_batch_from_external_customer(batch)
200 elif customer_info:
202 # pending_customer
203 batch.customer_id = None
204 batch.local_customer = None
205 customer = batch.pending_customer
206 if not customer:
207 customer = model.PendingCustomer(status=enum.PendingCustomerStatus.PENDING,
208 created_by=user or batch.created_by)
209 session.add(customer)
210 batch.pending_customer = customer
211 fields = [
212 'full_name',
213 'first_name',
214 'last_name',
215 'phone_number',
216 'email_address',
217 ]
218 for key in fields:
219 setattr(customer, key, customer_info.get(key))
220 if 'full_name' not in customer_info:
221 customer.full_name = self.app.make_full_name(customer.first_name,
222 customer.last_name)
223 self.refresh_batch_from_pending_customer(batch)
225 else:
227 # null
228 batch.customer_id = None
229 batch.local_customer = None
230 batch.customer_name = None
231 batch.phone_number = None
232 batch.email_address = None
234 session.flush()
236 def refresh_batch_from_external_customer(self, batch):
237 """
238 Update customer-related attributes on the batch, from its
239 :term:`external customer` record.
241 This is called automatically from :meth:`set_customer()`.
243 There is no default logic here; subclass must implement.
244 """
245 raise NotImplementedError
247 def refresh_batch_from_local_customer(self, batch):
248 """
249 Update customer-related attributes on the batch, from its
250 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.local_customer`
251 record.
253 This is called automatically from :meth:`set_customer()`.
254 """
255 customer = batch.local_customer
256 batch.customer_name = customer.full_name
257 batch.phone_number = customer.phone_number
258 batch.email_address = customer.email_address
260 def refresh_batch_from_pending_customer(self, batch):
261 """
262 Update customer-related attributes on the batch, from its
263 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer`
264 record.
266 This is called automatically from :meth:`set_customer()`.
267 """
268 customer = batch.pending_customer
269 batch.customer_name = customer.full_name
270 batch.phone_number = customer.phone_number
271 batch.email_address = customer.email_address
273 def autocomplete_products_external(self, session, term, user=None):
274 """
275 Return autocomplete search results for :term:`external
276 product` records.
278 There is no default logic here; subclass must implement.
280 :param session: Current app :term:`db session`.
282 :param term: Search term string from user input.
284 :param user:
285 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
286 is doing the search, if known.
288 :returns: List of search results; each should be a dict with
289 ``value`` and ``label`` keys.
290 """
291 raise NotImplementedError
293 def autocomplete_products_local(self, session, term, user=None):
294 """
295 Return autocomplete search results for
296 :class:`~sideshow.db.model.products.LocalProduct` records.
298 :param session: Current app :term:`db session`.
300 :param term: Search term string from user input.
302 :param user:
303 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
304 is doing the search, if known.
306 :returns: List of search results; each should be a dict with
307 ``value`` and ``label`` keys.
308 """
309 model = self.app.model
311 # base query
312 query = session.query(model.LocalProduct)
314 # filter query
315 criteria = []
316 for word in term.split():
317 criteria.append(sa.or_(
318 model.LocalProduct.brand_name.ilike(f'%{word}%'),
319 model.LocalProduct.description.ilike(f'%{word}%')))
320 query = query.filter(sa.and_(*criteria))
322 # sort query
323 query = query.order_by(model.LocalProduct.brand_name,
324 model.LocalProduct.description)
326 # get data
327 # TODO: need max_results option
328 products = query.all()
330 # get results
331 def result(product):
332 return {'value': product.uuid.hex,
333 'label': product.full_description}
334 return [result(c) for c in products]
336 def get_product_info_external(self, session, product_id, user=None):
337 """
338 Returns basic info for an :term:`external product` as pertains
339 to ordering.
341 When user has located a product via search, and must then
342 choose order quantity and UOM based on case size, pricing
343 etc., this method is called to retrieve the product info.
345 There is no default logic here; subclass must implement.
347 :param session: Current app :term:`db session`.
349 :param product_id: Product ID string for which to retrieve
350 info.
352 :param user:
353 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
354 is performing the action, if known.
356 :returns: Dict of product info. Should raise error instead of
357 returning ``None`` if product not found.
359 This method should only be called after a product has been
360 identified via autocomplete/search lookup; therefore the
361 ``product_id`` should be valid, and the caller can expect this
362 method to *always* return a dict. If for some reason the
363 product cannot be found here, an error should be raised.
365 The dict should contain as much product info as is available
366 and needed; if some are missing it should not cause too much
367 trouble in the app. Here is a basic example::
369 def get_product_info_external(self, session, product_id, user=None):
370 ext_model = get_external_model()
371 ext_session = make_external_session()
373 ext_product = ext_session.get(ext_model.Product, product_id)
374 if not ext_product:
375 ext_session.close()
376 raise ValueError(f"external product not found: {product_id}")
378 info = {
379 'product_id': product_id,
380 'scancode': product.scancode,
381 'brand_name': product.brand_name,
382 'description': product.description,
383 'size': product.size,
384 'weighed': product.sold_by_weight,
385 'special_order': False,
386 'department_id': str(product.department_number),
387 'department_name': product.department_name,
388 'case_size': product.case_size,
389 'unit_price_reg': product.unit_price_reg,
390 'vendor_name': product.vendor_name,
391 'vendor_item_code': product.vendor_item_code,
392 }
394 ext_session.close()
395 return info
396 """
397 raise NotImplementedError
399 def get_product_info_local(self, session, uuid, user=None):
400 """
401 Returns basic info for a
402 :class:`~sideshow.db.model.products.LocalProduct` as pertains
403 to ordering.
405 When user has located a product via search, and must then
406 choose order quantity and UOM based on case size, pricing
407 etc., this method is called to retrieve the product info.
409 See :meth:`get_product_info_external()` for more explanation.
410 """
411 model = self.app.model
412 product = session.get(model.LocalProduct, uuid)
413 if not product:
414 raise ValueError(f"Local Product not found: {uuid}")
416 return {
417 'product_id': product.uuid.hex,
418 'scancode': product.scancode,
419 'brand_name': product.brand_name,
420 'description': product.description,
421 'size': product.size,
422 'full_description': product.full_description,
423 'weighed': product.weighed,
424 'special_order': product.special_order,
425 'department_id': product.department_id,
426 'department_name': product.department_name,
427 'case_size': product.case_size,
428 'unit_price_reg': product.unit_price_reg,
429 'vendor_name': product.vendor_name,
430 'vendor_item_code': product.vendor_item_code,
431 }
433 def add_item(self, batch, product_info, order_qty, order_uom, user=None):
434 """
435 Add a new item/row to the batch, for given product and quantity.
437 See also :meth:`update_item()`.
439 :param batch:
440 :class:`~sideshow.db.model.batch.neworder.NewOrderBatch` to
441 update.
443 :param product_info: Product ID string, or dict of
444 :class:`~sideshow.db.model.products.PendingProduct` data.
446 :param order_qty:
447 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_qty`
448 value for the new row.
450 :param order_uom:
451 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_uom`
452 value for the new row.
454 :param user:
455 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
456 is performing the action. This is used to set
457 :attr:`~sideshow.db.model.products.PendingProduct.created_by`
458 on the pending product, if applicable. If not specified,
459 the batch creator is assumed.
461 :returns:
462 :class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
463 instance.
464 """
465 model = self.app.model
466 enum = self.app.enum
467 session = self.app.get_session(batch)
468 use_local = self.use_local_products()
469 row = self.make_row()
471 # set product info
472 if isinstance(product_info, str):
473 if use_local:
475 # local_product
476 local = session.get(model.LocalProduct, product_info)
477 if not local:
478 raise ValueError("local product not found")
479 row.local_product = local
481 else: # external product_id
482 row.product_id = product_info
484 else:
485 # pending_product
486 if not self.allow_unknown_products():
487 raise TypeError("unknown/pending product not allowed for new orders")
488 row.product_id = None
489 row.local_product = None
490 pending = model.PendingProduct(status=enum.PendingProductStatus.PENDING,
491 created_by=user or batch.created_by)
492 fields = [
493 'scancode',
494 'brand_name',
495 'description',
496 'size',
497 'weighed',
498 'department_id',
499 'department_name',
500 'special_order',
501 'vendor_name',
502 'vendor_item_code',
503 'case_size',
504 'unit_cost',
505 'unit_price_reg',
506 'notes',
507 ]
508 for key in fields:
509 setattr(pending, key, product_info.get(key))
511 # nb. this may convert float to decimal etc.
512 session.add(pending)
513 session.flush()
514 session.refresh(pending)
515 row.pending_product = pending
517 # set order info
518 row.order_qty = order_qty
519 row.order_uom = order_uom
521 # add row to batch
522 self.add_row(batch, row)
523 session.flush()
524 return row
526 def update_item(self, row, product_info, order_qty, order_uom, user=None):
527 """
528 Update an item/row, per given product and quantity.
530 See also :meth:`add_item()`.
532 :param row:
533 :class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
534 to update.
536 :param product_info: Product ID string, or dict of
537 :class:`~sideshow.db.model.products.PendingProduct` data.
539 :param order_qty: New
540 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_qty`
541 value for the row.
543 :param order_uom: New
544 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_uom`
545 value for the row.
547 :param user:
548 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
549 is performing the action. This is used to set
550 :attr:`~sideshow.db.model.products.PendingProduct.created_by`
551 on the pending product, if applicable. If not specified,
552 the batch creator is assumed.
553 """
554 model = self.app.model
555 enum = self.app.enum
556 session = self.app.get_session(row)
557 use_local = self.use_local_products()
559 # set product info
560 if isinstance(product_info, str):
561 if use_local:
563 # local_product
564 local = session.get(model.LocalProduct, product_info)
565 if not local:
566 raise ValueError("local product not found")
567 row.local_product = local
569 else: # external product_id
570 row.product_id = product_info
572 else:
573 # pending_product
574 if not self.allow_unknown_products():
575 raise TypeError("unknown/pending product not allowed for new orders")
576 row.product_id = None
577 row.local_product = None
578 pending = row.pending_product
579 if not pending:
580 pending = model.PendingProduct(status=enum.PendingProductStatus.PENDING,
581 created_by=user or row.batch.created_by)
582 session.add(pending)
583 row.pending_product = pending
584 fields = [
585 'scancode',
586 'brand_name',
587 'description',
588 'size',
589 'weighed',
590 'department_id',
591 'department_name',
592 'special_order',
593 'vendor_name',
594 'vendor_item_code',
595 'case_size',
596 'unit_cost',
597 'unit_price_reg',
598 'notes',
599 ]
600 for key in fields:
601 setattr(pending, key, product_info.get(key))
603 # nb. this may convert float to decimal etc.
604 session.flush()
605 session.refresh(pending)
607 # set order info
608 row.order_qty = order_qty
609 row.order_uom = order_uom
611 # nb. this may convert float to decimal etc.
612 session.flush()
613 session.refresh(row)
615 # refresh per new info
616 self.refresh_row(row)
618 def refresh_row(self, row):
619 """
620 Refresh data for the row. This is called when adding a new
621 row to the batch, or anytime the row is updated (e.g. when
622 changing order quantity).
624 This calls one of the following to update product-related
625 attributes:
627 * :meth:`refresh_row_from_external_product()`
628 * :meth:`refresh_row_from_local_product()`
629 * :meth:`refresh_row_from_pending_product()`
631 It then re-calculates the row's
632 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.total_price`
633 and updates the batch accordingly.
635 It also sets the row
636 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.status_code`.
637 """
638 enum = self.app.enum
639 row.status_code = None
640 row.status_text = None
642 # ensure product
643 if not row.product_id and not row.local_product and not row.pending_product:
644 row.status_code = row.STATUS_MISSING_PRODUCT
645 return
647 # ensure order qty/uom
648 if not row.order_qty or not row.order_uom:
649 row.status_code = row.STATUS_MISSING_ORDER_QTY
650 return
652 # update product attrs on row
653 if row.product_id:
654 self.refresh_row_from_external_product(row)
655 elif row.local_product:
656 self.refresh_row_from_local_product(row)
657 else:
658 self.refresh_row_from_pending_product(row)
660 # we need to know if total price changes
661 old_total = row.total_price
663 # update quoted price
664 row.unit_price_quoted = None
665 row.case_price_quoted = None
666 if row.unit_price_sale is not None and (
667 not row.sale_ends
668 or row.sale_ends > datetime.datetime.now()):
669 row.unit_price_quoted = row.unit_price_sale
670 else:
671 row.unit_price_quoted = row.unit_price_reg
672 if row.unit_price_quoted is not None and row.case_size:
673 row.case_price_quoted = row.unit_price_quoted * row.case_size
675 # update row total price
676 row.total_price = None
677 if row.order_uom == enum.ORDER_UOM_CASE:
678 if row.unit_price_quoted is not None and row.case_size is not None:
679 row.total_price = row.unit_price_quoted * row.case_size * row.order_qty
680 else: # ORDER_UOM_UNIT (or similar)
681 if row.unit_price_quoted is not None:
682 row.total_price = row.unit_price_quoted * row.order_qty
683 if row.total_price is not None:
684 row.total_price = decimal.Decimal(f'{row.total_price:0.2f}')
686 # update batch if total price changed
687 if row.total_price != old_total:
688 batch = row.batch
689 batch.total_price = ((batch.total_price or 0)
690 + (row.total_price or 0)
691 - (old_total or 0))
693 # all ok
694 row.status_code = row.STATUS_OK
696 def refresh_row_from_local_product(self, row):
697 """
698 Update product-related attributes on the row, from its
699 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.local_product`
700 record.
702 This is called automatically from :meth:`refresh_row()`.
703 """
704 product = row.local_product
705 row.product_scancode = product.scancode
706 row.product_brand = product.brand_name
707 row.product_description = product.description
708 row.product_size = product.size
709 row.product_weighed = product.weighed
710 row.department_id = product.department_id
711 row.department_name = product.department_name
712 row.special_order = product.special_order
713 row.case_size = product.case_size
714 row.unit_cost = product.unit_cost
715 row.unit_price_reg = product.unit_price_reg
717 def refresh_row_from_pending_product(self, row):
718 """
719 Update product-related attributes on the row, from its
720 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.pending_product`
721 record.
723 This is called automatically from :meth:`refresh_row()`.
724 """
725 product = row.pending_product
726 row.product_scancode = product.scancode
727 row.product_brand = product.brand_name
728 row.product_description = product.description
729 row.product_size = product.size
730 row.product_weighed = product.weighed
731 row.department_id = product.department_id
732 row.department_name = product.department_name
733 row.special_order = product.special_order
734 row.case_size = product.case_size
735 row.unit_cost = product.unit_cost
736 row.unit_price_reg = product.unit_price_reg
738 def refresh_row_from_external_product(self, row):
739 """
740 Update product-related attributes on the row, from its
741 :term:`external product` record indicated by
742 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.product_id`.
744 This is called automatically from :meth:`refresh_row()`.
746 There is no default logic here; subclass must implement as
747 needed.
748 """
749 raise NotImplementedError
751 def remove_row(self, row):
752 """
753 Remove a row from its batch.
755 This also will update the batch
756 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.total_price`
757 accordingly.
758 """
759 if row.total_price:
760 batch = row.batch
761 batch.total_price = (batch.total_price or 0) - row.total_price
763 super().remove_row(row)
765 def do_delete(self, batch, user, **kwargs):
766 """
767 Delete a batch completely.
769 If the batch has :term:`pending customer` or :term:`pending
770 product` records, they are also deleted - unless still
771 referenced by some order(s).
772 """
773 session = self.app.get_session(batch)
775 # maybe delete pending customer
776 customer = batch.pending_customer
777 if customer and not customer.orders:
778 session.delete(customer)
780 # maybe delete pending products
781 for row in batch.rows:
782 product = row.pending_product
783 if product and not product.order_items:
784 session.delete(product)
786 # continue with normal deletion
787 super().do_delete(batch, user, **kwargs)
789 def why_not_execute(self, batch, **kwargs):
790 """
791 By default this checks to ensure the batch has a customer with
792 phone number, and at least one item.
793 """
794 if not batch.customer_name:
795 return "Must assign the customer"
797 if not batch.phone_number:
798 return "Customer phone number is required"
800 rows = self.get_effective_rows(batch)
801 if not rows:
802 return "Must add at least one valid item"
804 def get_effective_rows(self, batch):
805 """
806 Only rows with
807 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.STATUS_OK`
808 are "effective" - i.e. rows with other status codes will not
809 be created as proper order items.
810 """
811 return [row for row in batch.rows
812 if row.status_code == row.STATUS_OK]
814 def execute(self, batch, user=None, progress=None, **kwargs):
815 """
816 Execute the batch; this should make a proper :term:`order`.
818 By default, this will call:
820 * :meth:`make_local_customer()`
821 * :meth:`make_local_products()`
822 * :meth:`make_new_order()`
824 And will return the new
825 :class:`~sideshow.db.model.orders.Order` instance.
827 Note that callers should use
828 :meth:`~wuttjamaican:wuttjamaican.batch.BatchHandler.do_execute()`
829 instead, which calls this method automatically.
830 """
831 rows = self.get_effective_rows(batch)
832 self.make_local_customer(batch)
833 self.make_local_products(batch, rows)
834 order = self.make_new_order(batch, rows, user=user, progress=progress, **kwargs)
835 return order
837 def make_local_customer(self, batch):
838 """
839 If applicable, this converts the batch :term:`pending
840 customer` into a :term:`local customer`.
842 This is called automatically from :meth:`execute()`.
844 This logic will happen only if :meth:`use_local_customers()`
845 returns true, and the batch has pending instead of local
846 customer (so far).
848 It will create a new
849 :class:`~sideshow.db.model.customers.LocalCustomer` record and
850 populate it from the batch
851 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer`.
852 The latter is then deleted.
853 """
854 if not self.use_local_customers():
855 return
857 # nothing to do if no pending customer
858 pending = batch.pending_customer
859 if not pending:
860 return
862 session = self.app.get_session(batch)
864 # maybe convert pending to local customer
865 if not batch.local_customer:
866 model = self.app.model
867 inspector = sa.inspect(model.LocalCustomer)
868 local = model.LocalCustomer()
869 for prop in inspector.column_attrs:
870 if hasattr(pending, prop.key):
871 setattr(local, prop.key, getattr(pending, prop.key))
872 session.add(local)
873 batch.local_customer = local
875 # remove pending customer
876 batch.pending_customer = None
877 session.delete(pending)
878 session.flush()
880 def make_local_products(self, batch, rows):
881 """
882 If applicable, this converts all :term:`pending products
883 <pending product>` into :term:`local products <local
884 product>`.
886 This is called automatically from :meth:`execute()`.
888 This logic will happen only if :meth:`use_local_products()`
889 returns true, and the batch has pending instead of local items
890 (so far).
892 For each affected row, it will create a new
893 :class:`~sideshow.db.model.products.LocalProduct` record and
894 populate it from the row
895 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.pending_product`.
896 The latter is then deleted.
897 """
898 if not self.use_local_products():
899 return
901 model = self.app.model
902 session = self.app.get_session(batch)
903 inspector = sa.inspect(model.LocalProduct)
904 for row in rows:
906 if row.local_product or not row.pending_product:
907 continue
909 pending = row.pending_product
910 local = model.LocalProduct()
912 for prop in inspector.column_attrs:
913 if hasattr(pending, prop.key):
914 setattr(local, prop.key, getattr(pending, prop.key))
915 session.add(local)
917 row.local_product = local
918 row.pending_product = None
919 session.delete(pending)
921 session.flush()
923 def make_new_order(self, batch, rows, user=None, progress=None, **kwargs):
924 """
925 Create a new :term:`order` from the batch data.
927 This is called automatically from :meth:`execute()`.
929 :param batch:
930 :class:`~sideshow.db.model.batch.neworder.NewOrderBatch`
931 instance.
933 :param rows: List of effective rows for the batch, i.e. which
934 rows should be converted to :term:`order items <order
935 item>`.
937 :returns: :class:`~sideshow.db.model.orders.Order` instance.
938 """
939 model = self.app.model
940 enum = self.app.enum
941 session = self.app.get_session(batch)
943 batch_fields = [
944 'store_id',
945 'customer_id',
946 'local_customer',
947 'pending_customer',
948 'customer_name',
949 'phone_number',
950 'email_address',
951 'total_price',
952 ]
954 row_fields = [
955 'product_id',
956 'local_product',
957 'pending_product',
958 'product_scancode',
959 'product_brand',
960 'product_description',
961 'product_size',
962 'product_weighed',
963 'department_id',
964 'department_name',
965 'case_size',
966 'order_qty',
967 'order_uom',
968 'unit_cost',
969 'unit_price_quoted',
970 'case_price_quoted',
971 'unit_price_reg',
972 'unit_price_sale',
973 'sale_ends',
974 # 'discount_percent',
975 'total_price',
976 'special_order',
977 ]
979 # make order
980 kw = dict([(field, getattr(batch, field))
981 for field in batch_fields])
982 kw['order_id'] = batch.id
983 kw['created_by'] = user
984 order = model.Order(**kw)
985 session.add(order)
986 session.flush()
988 def convert(row, i):
990 # make order item
991 kw = dict([(field, getattr(row, field))
992 for field in row_fields])
993 item = model.OrderItem(**kw)
994 order.items.append(item)
996 # set item status
997 self.set_initial_item_status(item, user)
999 self.app.progress_loop(convert, rows, progress,
1000 message="Converting batch rows to order items")
1001 session.flush()
1002 return order
1004 def set_initial_item_status(self, item, user, **kwargs):
1005 """
1006 Set the initial status and attach event(s) for the given item.
1008 This is called from :meth:`make_new_order()` for each item
1009 after it is added to the order.
1011 Default logic will set status to
1012 :data:`~sideshow.enum.ORDER_ITEM_STATUS_READY` and attach 2
1013 events:
1015 * :data:`~sideshow.enum.ORDER_ITEM_EVENT_INITIATED`
1016 * :data:`~sideshow.enum.ORDER_ITEM_EVENT_READY`
1018 :param item: :class:`~sideshow.db.model.orders.OrderItem`
1019 being added to the new order.
1021 :param user:
1022 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
1023 is performing the action.
1024 """
1025 enum = self.app.enum
1026 item.add_event(enum.ORDER_ITEM_EVENT_INITIATED, user)
1027 item.add_event(enum.ORDER_ITEM_EVENT_READY, user)
1028 item.status_code = enum.ORDER_ITEM_STATUS_READY