Coverage for .tox/coverage/lib/python3.11/site-packages/sideshow/batch/neworder.py: 100%
306 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-13 13:09 -0600
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-13 13:09 -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.
47 """
48 model_class = NewOrderBatch
50 def use_local_customers(self):
51 """
52 Returns boolean indicating whether :term:`local customer`
53 accounts should be used. This is true by default, but may be
54 false for :term:`external customer` lookups.
55 """
56 return self.config.get_bool('sideshow.orders.use_local_customers',
57 default=True)
59 def use_local_products(self):
60 """
61 Returns boolean indicating whether :term:`local product`
62 records should be used. This is true by default, but may be
63 false for :term:`external product` lookups.
64 """
65 return self.config.get_bool('sideshow.orders.use_local_products',
66 default=True)
68 def allow_unknown_products(self):
69 """
70 Returns boolean indicating whether :term:`pending products
71 <pending product>` are allowed when creating an order.
73 This is true by default, so user can enter new/unknown product
74 when creating an order. This can be disabled, to force user
75 to choose existing local/external product.
76 """
77 return self.config.get_bool('sideshow.orders.allow_unknown_products',
78 default=True)
80 def autocomplete_customers_external(self, session, term, user=None):
81 """
82 Return autocomplete search results for :term:`external
83 customer` records.
85 There is no default logic here; subclass must implement.
87 :param session: Current app :term:`db session`.
89 :param term: Search term string from user input.
91 :param user:
92 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
93 is doing the search, if known.
95 :returns: List of search results; each should be a dict with
96 ``value`` and ``label`` keys.
97 """
98 raise NotImplementedError
100 def autocomplete_customers_local(self, session, term, user=None):
101 """
102 Return autocomplete search results for
103 :class:`~sideshow.db.model.customers.LocalCustomer` records.
105 :param session: Current app :term:`db session`.
107 :param term: Search term string from user input.
109 :param user:
110 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
111 is doing the search, if known.
113 :returns: List of search results; each should be a dict with
114 ``value`` and ``label`` keys.
115 """
116 model = self.app.model
118 # base query
119 query = session.query(model.LocalCustomer)
121 # filter query
122 criteria = [model.LocalCustomer.full_name.ilike(f'%{word}%')
123 for word in term.split()]
124 query = query.filter(sa.and_(*criteria))
126 # sort query
127 query = query.order_by(model.LocalCustomer.full_name)
129 # get data
130 # TODO: need max_results option
131 customers = query.all()
133 # get results
134 def result(customer):
135 return {'value': customer.uuid.hex,
136 'label': customer.full_name}
137 return [result(c) for c in customers]
139 def set_customer(self, batch, customer_info, user=None):
140 """
141 Set/update customer info for the batch.
143 This will first set one of the following:
145 * :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.customer_id`
146 * :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.local_customer`
147 * :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer`
149 Note that a new
150 :class:`~sideshow.db.model.customers.PendingCustomer` record
151 is created if necessary.
153 And then it will update customer-related attributes via one of:
155 * :meth:`refresh_batch_from_external_customer()`
156 * :meth:`refresh_batch_from_local_customer()`
157 * :meth:`refresh_batch_from_pending_customer()`
159 Note that ``customer_info`` may be ``None``, which will cause
160 customer attributes to be set to ``None`` also.
162 :param batch:
163 :class:`~sideshow.db.model.batch.neworder.NewOrderBatch` to
164 update.
166 :param customer_info: Customer ID string, or dict of
167 :class:`~sideshow.db.model.customers.PendingCustomer` data,
168 or ``None`` to clear the customer info.
170 :param user:
171 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
172 is performing the action. This is used to set
173 :attr:`~sideshow.db.model.customers.PendingCustomer.created_by`
174 on the pending customer, if applicable. If not specified,
175 the batch creator is assumed.
176 """
177 model = self.app.model
178 enum = self.app.enum
179 session = self.app.get_session(batch)
180 use_local = self.use_local_customers()
182 # set customer info
183 if isinstance(customer_info, str):
184 if use_local:
186 # local_customer
187 customer = session.get(model.LocalCustomer, customer_info)
188 if not customer:
189 raise ValueError("local customer not found")
190 batch.local_customer = customer
191 self.refresh_batch_from_local_customer(batch)
193 else: # external customer_id
194 batch.customer_id = customer_info
195 self.refresh_batch_from_external_customer(batch)
197 elif customer_info:
199 # pending_customer
200 batch.customer_id = None
201 batch.local_customer = None
202 customer = batch.pending_customer
203 if not customer:
204 customer = model.PendingCustomer(status=enum.PendingCustomerStatus.PENDING,
205 created_by=user or batch.created_by)
206 session.add(customer)
207 batch.pending_customer = customer
208 fields = [
209 'full_name',
210 'first_name',
211 'last_name',
212 'phone_number',
213 'email_address',
214 ]
215 for key in fields:
216 setattr(customer, key, customer_info.get(key))
217 if 'full_name' not in customer_info:
218 customer.full_name = self.app.make_full_name(customer.first_name,
219 customer.last_name)
220 self.refresh_batch_from_pending_customer(batch)
222 else:
224 # null
225 batch.customer_id = None
226 batch.local_customer = None
227 batch.customer_name = None
228 batch.phone_number = None
229 batch.email_address = None
231 session.flush()
233 def refresh_batch_from_external_customer(self, batch):
234 """
235 Update customer-related attributes on the batch, from its
236 :term:`external customer` record.
238 This is called automatically from :meth:`set_customer()`.
240 There is no default logic here; subclass must implement.
241 """
242 raise NotImplementedError
244 def refresh_batch_from_local_customer(self, batch):
245 """
246 Update customer-related attributes on the batch, from its
247 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.local_customer`
248 record.
250 This is called automatically from :meth:`set_customer()`.
251 """
252 customer = batch.local_customer
253 batch.customer_name = customer.full_name
254 batch.phone_number = customer.phone_number
255 batch.email_address = customer.email_address
257 def refresh_batch_from_pending_customer(self, batch):
258 """
259 Update customer-related attributes on the batch, from its
260 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer`
261 record.
263 This is called automatically from :meth:`set_customer()`.
264 """
265 customer = batch.pending_customer
266 batch.customer_name = customer.full_name
267 batch.phone_number = customer.phone_number
268 batch.email_address = customer.email_address
270 def autocomplete_products_external(self, session, term, user=None):
271 """
272 Return autocomplete search results for :term:`external
273 product` records.
275 There is no default logic here; subclass must implement.
277 :param session: Current app :term:`db session`.
279 :param term: Search term string from user input.
281 :param user:
282 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
283 is doing the search, if known.
285 :returns: List of search results; each should be a dict with
286 ``value`` and ``label`` keys.
287 """
288 raise NotImplementedError
290 def autocomplete_products_local(self, session, term, user=None):
291 """
292 Return autocomplete search results for
293 :class:`~sideshow.db.model.products.LocalProduct` records.
295 :param session: Current app :term:`db session`.
297 :param term: Search term string from user input.
299 :param user:
300 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
301 is doing the search, if known.
303 :returns: List of search results; each should be a dict with
304 ``value`` and ``label`` keys.
305 """
306 model = self.app.model
308 # base query
309 query = session.query(model.LocalProduct)
311 # filter query
312 criteria = []
313 for word in term.split():
314 criteria.append(sa.or_(
315 model.LocalProduct.brand_name.ilike(f'%{word}%'),
316 model.LocalProduct.description.ilike(f'%{word}%')))
317 query = query.filter(sa.and_(*criteria))
319 # sort query
320 query = query.order_by(model.LocalProduct.brand_name,
321 model.LocalProduct.description)
323 # get data
324 # TODO: need max_results option
325 products = query.all()
327 # get results
328 def result(product):
329 return {'value': product.uuid.hex,
330 'label': product.full_description}
331 return [result(c) for c in products]
333 def get_product_info_external(self, session, product_id, user=None):
334 """
335 Returns basic info for an :term:`external product` as pertains
336 to ordering.
338 When user has located a product via search, and must then
339 choose order quantity and UOM based on case size, pricing
340 etc., this method is called to retrieve the product info.
342 There is no default logic here; subclass must implement.
344 :param session: Current app :term:`db session`.
346 :param product_id: Product ID string for which to retrieve
347 info.
349 :param user:
350 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
351 is performing the action, if known.
353 :returns: Dict of product info. Should raise error instead of
354 returning ``None`` if product not found.
356 This method should only be called after a product has been
357 identified via autocomplete/search lookup; therefore the
358 ``product_id`` should be valid, and the caller can expect this
359 method to *always* return a dict. If for some reason the
360 product cannot be found here, an error should be raised.
362 The dict should contain as much product info as is available
363 and needed; if some are missing it should not cause too much
364 trouble in the app. Here is a basic example::
366 def get_product_info_external(self, session, product_id, user=None):
367 ext_model = get_external_model()
368 ext_session = make_external_session()
370 ext_product = ext_session.get(ext_model.Product, product_id)
371 if not ext_product:
372 ext_session.close()
373 raise ValueError(f"external product not found: {product_id}")
375 info = {
376 'product_id': product_id,
377 'scancode': product.scancode,
378 'brand_name': product.brand_name,
379 'description': product.description,
380 'size': product.size,
381 'weighed': product.sold_by_weight,
382 'special_order': False,
383 'department_id': str(product.department_number),
384 'department_name': product.department_name,
385 'case_size': product.case_size,
386 'unit_price_reg': product.unit_price_reg,
387 'vendor_name': product.vendor_name,
388 'vendor_item_code': product.vendor_item_code,
389 }
391 ext_session.close()
392 return info
393 """
394 raise NotImplementedError
396 def get_product_info_local(self, session, uuid, user=None):
397 """
398 Returns basic info for a
399 :class:`~sideshow.db.model.products.LocalProduct` as pertains
400 to ordering.
402 When user has located a product via search, and must then
403 choose order quantity and UOM based on case size, pricing
404 etc., this method is called to retrieve the product info.
406 See :meth:`get_product_info_external()` for more explanation.
407 """
408 model = self.app.model
409 product = session.get(model.LocalProduct, uuid)
410 if not product:
411 raise ValueError(f"Local Product not found: {uuid}")
413 return {
414 'product_id': product.uuid.hex,
415 'scancode': product.scancode,
416 'brand_name': product.brand_name,
417 'description': product.description,
418 'size': product.size,
419 'full_description': product.full_description,
420 'weighed': product.weighed,
421 'special_order': product.special_order,
422 'department_id': product.department_id,
423 'department_name': product.department_name,
424 'case_size': product.case_size,
425 'unit_price_reg': product.unit_price_reg,
426 'vendor_name': product.vendor_name,
427 'vendor_item_code': product.vendor_item_code,
428 }
430 def add_item(self, batch, product_info, order_qty, order_uom, user=None):
431 """
432 Add a new item/row to the batch, for given product and quantity.
434 See also :meth:`update_item()`.
436 :param batch:
437 :class:`~sideshow.db.model.batch.neworder.NewOrderBatch` to
438 update.
440 :param product_info: Product ID string, or dict of
441 :class:`~sideshow.db.model.products.PendingProduct` data.
443 :param order_qty:
444 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_qty`
445 value for the new row.
447 :param order_uom:
448 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_uom`
449 value for the new row.
451 :param user:
452 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
453 is performing the action. This is used to set
454 :attr:`~sideshow.db.model.products.PendingProduct.created_by`
455 on the pending product, if applicable. If not specified,
456 the batch creator is assumed.
458 :returns:
459 :class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
460 instance.
461 """
462 model = self.app.model
463 enum = self.app.enum
464 session = self.app.get_session(batch)
465 use_local = self.use_local_products()
466 row = self.make_row()
468 # set product info
469 if isinstance(product_info, str):
470 if use_local:
472 # local_product
473 local = session.get(model.LocalProduct, product_info)
474 if not local:
475 raise ValueError("local product not found")
476 row.local_product = local
478 else: # external product_id
479 row.product_id = product_info
481 else:
482 # pending_product
483 if not self.allow_unknown_products():
484 raise TypeError("unknown/pending product not allowed for new orders")
485 row.product_id = None
486 row.local_product = None
487 pending = model.PendingProduct(status=enum.PendingProductStatus.PENDING,
488 created_by=user or batch.created_by)
489 fields = [
490 'scancode',
491 'brand_name',
492 'description',
493 'size',
494 'weighed',
495 'department_id',
496 'department_name',
497 'special_order',
498 'vendor_name',
499 'vendor_item_code',
500 'case_size',
501 'unit_cost',
502 'unit_price_reg',
503 'notes',
504 ]
505 for key in fields:
506 setattr(pending, key, product_info.get(key))
508 # nb. this may convert float to decimal etc.
509 session.add(pending)
510 session.flush()
511 session.refresh(pending)
512 row.pending_product = pending
514 # set order info
515 row.order_qty = order_qty
516 row.order_uom = order_uom
518 # add row to batch
519 self.add_row(batch, row)
520 session.flush()
521 return row
523 def update_item(self, row, product_info, order_qty, order_uom, user=None):
524 """
525 Update an item/row, per given product and quantity.
527 See also :meth:`add_item()`.
529 :param row:
530 :class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
531 to update.
533 :param product_info: Product ID string, or dict of
534 :class:`~sideshow.db.model.products.PendingProduct` data.
536 :param order_qty: New
537 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_qty`
538 value for the row.
540 :param order_uom: New
541 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_uom`
542 value for the row.
544 :param user:
545 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
546 is performing the action. This is used to set
547 :attr:`~sideshow.db.model.products.PendingProduct.created_by`
548 on the pending product, if applicable. If not specified,
549 the batch creator is assumed.
550 """
551 model = self.app.model
552 enum = self.app.enum
553 session = self.app.get_session(row)
554 use_local = self.use_local_products()
556 # set product info
557 if isinstance(product_info, str):
558 if use_local:
560 # local_product
561 local = session.get(model.LocalProduct, product_info)
562 if not local:
563 raise ValueError("local product not found")
564 row.local_product = local
566 else: # external product_id
567 row.product_id = product_info
569 else:
570 # pending_product
571 if not self.allow_unknown_products():
572 raise TypeError("unknown/pending product not allowed for new orders")
573 row.product_id = None
574 row.local_product = None
575 pending = row.pending_product
576 if not pending:
577 pending = model.PendingProduct(status=enum.PendingProductStatus.PENDING,
578 created_by=user or row.batch.created_by)
579 session.add(pending)
580 row.pending_product = pending
581 fields = [
582 'scancode',
583 'brand_name',
584 'description',
585 'size',
586 'weighed',
587 'department_id',
588 'department_name',
589 'special_order',
590 'vendor_name',
591 'vendor_item_code',
592 'case_size',
593 'unit_cost',
594 'unit_price_reg',
595 'notes',
596 ]
597 for key in fields:
598 setattr(pending, key, product_info.get(key))
600 # nb. this may convert float to decimal etc.
601 session.flush()
602 session.refresh(pending)
604 # set order info
605 row.order_qty = order_qty
606 row.order_uom = order_uom
608 # nb. this may convert float to decimal etc.
609 session.flush()
610 session.refresh(row)
612 # refresh per new info
613 self.refresh_row(row)
615 def refresh_row(self, row):
616 """
617 Refresh data for the row. This is called when adding a new
618 row to the batch, or anytime the row is updated (e.g. when
619 changing order quantity).
621 This calls one of the following to update product-related
622 attributes:
624 * :meth:`refresh_row_from_external_product()`
625 * :meth:`refresh_row_from_local_product()`
626 * :meth:`refresh_row_from_pending_product()`
628 It then re-calculates the row's
629 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.total_price`
630 and updates the batch accordingly.
632 It also sets the row
633 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.status_code`.
634 """
635 enum = self.app.enum
636 row.status_code = None
637 row.status_text = None
639 # ensure product
640 if not row.product_id and not row.local_product and not row.pending_product:
641 row.status_code = row.STATUS_MISSING_PRODUCT
642 return
644 # ensure order qty/uom
645 if not row.order_qty or not row.order_uom:
646 row.status_code = row.STATUS_MISSING_ORDER_QTY
647 return
649 # update product attrs on row
650 if row.product_id:
651 self.refresh_row_from_external_product(row)
652 elif row.local_product:
653 self.refresh_row_from_local_product(row)
654 else:
655 self.refresh_row_from_pending_product(row)
657 # we need to know if total price changes
658 old_total = row.total_price
660 # update quoted price
661 row.unit_price_quoted = None
662 row.case_price_quoted = None
663 if row.unit_price_sale is not None and (
664 not row.sale_ends
665 or row.sale_ends > datetime.datetime.now()):
666 row.unit_price_quoted = row.unit_price_sale
667 else:
668 row.unit_price_quoted = row.unit_price_reg
669 if row.unit_price_quoted is not None and row.case_size:
670 row.case_price_quoted = row.unit_price_quoted * row.case_size
672 # update row total price
673 row.total_price = None
674 if row.order_uom == enum.ORDER_UOM_CASE:
675 if row.unit_price_quoted is not None and row.case_size is not None:
676 row.total_price = row.unit_price_quoted * row.case_size * row.order_qty
677 else: # ORDER_UOM_UNIT (or similar)
678 if row.unit_price_quoted is not None:
679 row.total_price = row.unit_price_quoted * row.order_qty
680 if row.total_price is not None:
681 row.total_price = decimal.Decimal(f'{row.total_price:0.2f}')
683 # update batch if total price changed
684 if row.total_price != old_total:
685 batch = row.batch
686 batch.total_price = ((batch.total_price or 0)
687 + (row.total_price or 0)
688 - (old_total or 0))
690 # all ok
691 row.status_code = row.STATUS_OK
693 def refresh_row_from_local_product(self, row):
694 """
695 Update product-related attributes on the row, from its
696 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.local_product`
697 record.
699 This is called automatically from :meth:`refresh_row()`.
700 """
701 product = row.local_product
702 row.product_scancode = product.scancode
703 row.product_brand = product.brand_name
704 row.product_description = product.description
705 row.product_size = product.size
706 row.product_weighed = product.weighed
707 row.department_id = product.department_id
708 row.department_name = product.department_name
709 row.special_order = product.special_order
710 row.case_size = product.case_size
711 row.unit_cost = product.unit_cost
712 row.unit_price_reg = product.unit_price_reg
714 def refresh_row_from_pending_product(self, row):
715 """
716 Update product-related attributes on the row, from its
717 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.pending_product`
718 record.
720 This is called automatically from :meth:`refresh_row()`.
721 """
722 product = row.pending_product
723 row.product_scancode = product.scancode
724 row.product_brand = product.brand_name
725 row.product_description = product.description
726 row.product_size = product.size
727 row.product_weighed = product.weighed
728 row.department_id = product.department_id
729 row.department_name = product.department_name
730 row.special_order = product.special_order
731 row.case_size = product.case_size
732 row.unit_cost = product.unit_cost
733 row.unit_price_reg = product.unit_price_reg
735 def refresh_row_from_external_product(self, row):
736 """
737 Update product-related attributes on the row, from its
738 :term:`external product` record indicated by
739 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.product_id`.
741 This is called automatically from :meth:`refresh_row()`.
743 There is no default logic here; subclass must implement as
744 needed.
745 """
746 raise NotImplementedError
748 def remove_row(self, row):
749 """
750 Remove a row from its batch.
752 This also will update the batch
753 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.total_price`
754 accordingly.
755 """
756 if row.total_price:
757 batch = row.batch
758 batch.total_price = (batch.total_price or 0) - row.total_price
760 super().remove_row(row)
762 def do_delete(self, batch, user, **kwargs):
763 """
764 Delete a batch completely.
766 If the batch has :term:`pending customer` or :term:`pending
767 product` records, they are also deleted - unless still
768 referenced by some order(s).
769 """
770 session = self.app.get_session(batch)
772 # maybe delete pending customer
773 customer = batch.pending_customer
774 if customer and not customer.orders:
775 session.delete(customer)
777 # maybe delete pending products
778 for row in batch.rows:
779 product = row.pending_product
780 if product and not product.order_items:
781 session.delete(product)
783 # continue with normal deletion
784 super().do_delete(batch, user, **kwargs)
786 def why_not_execute(self, batch, **kwargs):
787 """
788 By default this checks to ensure the batch has a customer with
789 phone number, and at least one item.
790 """
791 if not batch.customer_name:
792 return "Must assign the customer"
794 if not batch.phone_number:
795 return "Customer phone number is required"
797 rows = self.get_effective_rows(batch)
798 if not rows:
799 return "Must add at least one valid item"
801 def get_effective_rows(self, batch):
802 """
803 Only rows with
804 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.STATUS_OK`
805 are "effective" - i.e. rows with other status codes will not
806 be created as proper order items.
807 """
808 return [row for row in batch.rows
809 if row.status_code == row.STATUS_OK]
811 def execute(self, batch, user=None, progress=None, **kwargs):
812 """
813 Execute the batch; this should make a proper :term:`order`.
815 By default, this will call:
817 * :meth:`make_local_customer()`
818 * :meth:`make_local_products()`
819 * :meth:`make_new_order()`
821 And will return the new
822 :class:`~sideshow.db.model.orders.Order` instance.
824 Note that callers should use
825 :meth:`~wuttjamaican:wuttjamaican.batch.BatchHandler.do_execute()`
826 instead, which calls this method automatically.
827 """
828 rows = self.get_effective_rows(batch)
829 self.make_local_customer(batch)
830 self.make_local_products(batch, rows)
831 order = self.make_new_order(batch, rows, user=user, progress=progress, **kwargs)
832 return order
834 def make_local_customer(self, batch):
835 """
836 If applicable, this converts the batch :term:`pending
837 customer` into a :term:`local customer`.
839 This is called automatically from :meth:`execute()`.
841 This logic will happen only if :meth:`use_local_customers()`
842 returns true, and the batch has pending instead of local
843 customer (so far).
845 It will create a new
846 :class:`~sideshow.db.model.customers.LocalCustomer` record and
847 populate it from the batch
848 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer`.
849 The latter is then deleted.
850 """
851 if not self.use_local_customers():
852 return
854 # nothing to do if no pending customer
855 pending = batch.pending_customer
856 if not pending:
857 return
859 session = self.app.get_session(batch)
861 # maybe convert pending to local customer
862 if not batch.local_customer:
863 model = self.app.model
864 inspector = sa.inspect(model.LocalCustomer)
865 local = model.LocalCustomer()
866 for prop in inspector.column_attrs:
867 if hasattr(pending, prop.key):
868 setattr(local, prop.key, getattr(pending, prop.key))
869 session.add(local)
870 batch.local_customer = local
872 # remove pending customer
873 batch.pending_customer = None
874 session.delete(pending)
875 session.flush()
877 def make_local_products(self, batch, rows):
878 """
879 If applicable, this converts all :term:`pending products
880 <pending product>` into :term:`local products <local
881 product>`.
883 This is called automatically from :meth:`execute()`.
885 This logic will happen only if :meth:`use_local_products()`
886 returns true, and the batch has pending instead of local items
887 (so far).
889 For each affected row, it will create a new
890 :class:`~sideshow.db.model.products.LocalProduct` record and
891 populate it from the row
892 :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.pending_product`.
893 The latter is then deleted.
894 """
895 if not self.use_local_products():
896 return
898 model = self.app.model
899 session = self.app.get_session(batch)
900 inspector = sa.inspect(model.LocalProduct)
901 for row in rows:
903 if row.local_product or not row.pending_product:
904 continue
906 pending = row.pending_product
907 local = model.LocalProduct()
909 for prop in inspector.column_attrs:
910 if hasattr(pending, prop.key):
911 setattr(local, prop.key, getattr(pending, prop.key))
912 session.add(local)
914 row.local_product = local
915 row.pending_product = None
916 session.delete(pending)
918 session.flush()
920 def make_new_order(self, batch, rows, user=None, progress=None, **kwargs):
921 """
922 Create a new :term:`order` from the batch data.
924 This is called automatically from :meth:`execute()`.
926 :param batch:
927 :class:`~sideshow.db.model.batch.neworder.NewOrderBatch`
928 instance.
930 :param rows: List of effective rows for the batch, i.e. which
931 rows should be converted to :term:`order items <order
932 item>`.
934 :returns: :class:`~sideshow.db.model.orders.Order` instance.
935 """
936 model = self.app.model
937 enum = self.app.enum
938 session = self.app.get_session(batch)
940 batch_fields = [
941 'store_id',
942 'customer_id',
943 'local_customer',
944 'pending_customer',
945 'customer_name',
946 'phone_number',
947 'email_address',
948 'total_price',
949 ]
951 row_fields = [
952 'product_id',
953 'local_product',
954 'pending_product',
955 'product_scancode',
956 'product_brand',
957 'product_description',
958 'product_size',
959 'product_weighed',
960 'department_id',
961 'department_name',
962 'case_size',
963 'order_qty',
964 'order_uom',
965 'unit_cost',
966 'unit_price_quoted',
967 'case_price_quoted',
968 'unit_price_reg',
969 'unit_price_sale',
970 'sale_ends',
971 # 'discount_percent',
972 'total_price',
973 'special_order',
974 ]
976 # make order
977 kw = dict([(field, getattr(batch, field))
978 for field in batch_fields])
979 kw['order_id'] = batch.id
980 kw['created_by'] = user
981 order = model.Order(**kw)
982 session.add(order)
983 session.flush()
985 def convert(row, i):
987 # make order item
988 kw = dict([(field, getattr(row, field))
989 for field in row_fields])
990 item = model.OrderItem(**kw)
991 order.items.append(item)
993 # set item status
994 item.status_code = enum.ORDER_ITEM_STATUS_INITIATED
996 self.app.progress_loop(convert, rows, progress,
997 message="Converting batch rows to order items")
998 session.flush()
999 return order