Source code for fpiweb.support.BoxManagement

"""
BoxManagement.py - Manage box creating, filling, moving and emptying.

Error messages from this module are prefixed by 1nn, e.g. "101 - blah blah..."
"""

__author__ = 'Travis Risner'
__project__ = "Food-Pantry-Inventory"
__creation_date__ = "01/11/2020"

# "${Copyright.py}"
from typing import Optional, Union

from django.db import transaction
from django.utils.timezone import now

from fpiweb.constants import \
    InvalidValueError, \
    InvalidActionAttemptedError, \
    CURRENT_YEAR
from fpiweb.models import \
    Box, \
    Pallet, \
    PalletBox, \
    Location, \
    Product, \
    BoxType, \
    BoxNumber, \
    Constraints
from fpiweb.support.BoxActivity import BoxActivityClass


[docs]class BoxManagementClass: """ BoxManagementClass - Manage db changes for box changes. The public API's validate parameter values. The private methods perform the actual actions. """ def __init__(self): self.pallet: Optional[Pallet] = None self.pallet_box: Optional[PalletBox] = None self.box: Optional[Box] = None self.box_type: Optional[BoxType] = None self.location: Optional[Location] = None self.product: Optional[Product] = None self.exp_year: Optional[int] = None self.exp_mo_start: Optional[int] = None self.exp_mo_end: Optional[int] = None self.activity = BoxActivityClass()
[docs] def box_new(self, box_number: str, box_type: Union[str, int, BoxType]) -> Box: """ Add a new empty box to the inventory system. If successful it adds an activity record for an empty box and returns the box record just created. Requirements: * Box number is valid, unique, and not previously assigned * Box type is valid Exceptions: 101 - Box number supplied is not in the valid format ('BOXnnnn') 102 - Box number is not unique 102 - Box type is not valid :param box_number: in the form of 'BOXnnnnn' :param box_type: a valid box type code, BoxType record or record id :return: the newly created box record """ # box number validation if not BoxNumber.validate(box_number): raise InvalidValueError(f'101 - Box number of "{box_number}" is ' f'improperly formatted or missing') box_exists_qs = Box.objects.filter(box_number=box_number) if len(box_exists_qs) > 0: raise InvalidActionAttemptedError( f'102 - Creating a new box {box_number} failed because it' f'already exists') # box type validation - either code, id or record if type(box_type) == BoxType: self.box_type = box_type elif type(box_type) == int: try: self.box_type = BoxType.objects.get(pk=box_type) except BoxType.DoesNotExist: raise InvalidValueError( f'102 - Box type id of "{box_type}" is invalid') else: try: self.box_type = BoxType.objects.get(box_type_code=box_type) except BoxType.DoesNotExist: raise InvalidValueError( f'102 - Box type code of "{box_type}" is invalid') self._new_box(box_number, self.box_type) return self.box
[docs] def box_fill(self, *, box: Union[Box, int], location: Union[Location, int], product: Union[Product, int], exp_year: int, exp_mo_start: int = 0, exp_mo_end: int = 0 ): """ Fill an individual box with product and add to the inventory. If the box is not empty, an activity record will empty the box of its previous contents and a new activity record will note the new contents profiled. If successful, it will return the box just filled. Requirements: * Box record has not been modified * All required fields are valid * Optional month start and end, if specified, bracket one or more months Exceptions: 111 - Attempting to fill a box that does not exist 112 - location supplied is not valid 113 - the product supplied is not valid 114 - the expiration year, start month, and/or end month are not valid or are out of range :param box: Box record or id of a box already in the system :param location: Target location record or ID :param product: Target product record or ID :param exp_year: year (current year +/- 10) :param exp_mo_start: 1 - 12 if specified - usually beginning quarter :param exp_mo_end: 1-12 if specified - usually ending quarter :return: box record after modifications """ if type(box) == Box: self.box = box else: try: self.box = Box.objects.select_related('box_type').get(pk=box) except Box.DoesNotExist: raise InvalidActionAttemptedError( f'111 - Attempting to fill a box that does not exist. ID ' f'given was "{box}"') if type(location) == Location: self.location = location else: try: self.location = Location.objects.get(pk=location) except Location.DoesNotExist: raise InvalidValueError( f'112 - Location with ID "{location}" not found') if type(product) == Product: self.product = product else: try: self.product = Product.objects.get(pk=product) except Product.DoesNotExist: raise InvalidValueError( f'113 - Product with ID "{product}" not found') # presume the date information is true until proven otherwise expiration_info_valid = True years_ahead_list = Constraints.get_values( Constraints.FUTURE_EXP_YEAR_LIMIT) years_ahead = years_ahead_list[0] future_exp_year_limit = CURRENT_YEAR + years_ahead if exp_year < CURRENT_YEAR or exp_year > future_exp_year_limit: expiration_info_valid = False else: self.exp_year = exp_year # both valid months or zero or null if (exp_mo_start is None) or exp_mo_start == 0: self.exp_mo_start = 0 elif 1 <= exp_mo_start <= 12: self.exp_mo_start = exp_mo_start else: expiration_info_valid = False if (exp_mo_end is None) or exp_mo_end == 0: self.exp_mo_end = 0 elif 1 <= exp_mo_end <= 12: self.exp_mo_end = exp_mo_end else: expiration_info_valid = False # end must be greater than or equal to start if self.exp_mo_end < self.exp_mo_start: expiration_info_valid = False # did it pass the gauntlet? if not expiration_info_valid: raise InvalidValueError( f'114 - Expiration date information of {exp_mo_start} -' f' {exp_mo_end} - {exp_year} was not valid') self._fill_box() return self.box
[docs] def box_move(self, box: Union[Box, int], location: Union[Location, int]): """ Move an individual box in the inventory. The activity record for this box will be changed to show the new location. The old location will be dropped from the activity record. If successful, the box record will be returned. Requirements: * Box is filled * Location is valid Exceptions: 121 - Box not in system 122 - Cannot move an empty box 123 - Location is not valid :param box: :param location: :return: """ if type(box) == Box: self.box = box else: try: self.box = Box.objects.get(pk=box).select_related('box_type') except Box.DoesNotExist: raise InvalidActionAttemptedError( f'121 - Attempting to move a box that does not exist. ID ' f'given was "{box}"') if not self.box.product: raise InvalidActionAttemptedError( f'122 - Attempting to move an empty box') if type(location) == Location: self.location = location else: try: self.location = Location.objects.get(pk=location) except Location.DoesNotExist: raise InvalidValueError( f'123 - Location with ID "{location}" not found') self._move_box() return self.box
[docs] def box_consume(self, box: Union[Box, int]) -> Box: """ Consume (e.g. empty) a box. The box will be marked empty, the activity record will be updated, and the box will be returned. Requirements: * The box must not be empty when passed in. Exception: 131 - Box not in system 132 - The box was already empty :param box: :return: the box record freshly emptied """ if type(box) == Box: self.box = box else: try: self.box = Box.objects.select_related('box_type').get(pk=box) except Box.DoesNotExist: raise InvalidActionAttemptedError( f'131 - Attempting to move a box that does not exist. ID ' f'given was "{box}"') if not self.box.product: raise InvalidActionAttemptedError( f'132 - Attempting to consume the contents of an empty box') self._consume_box() return self.box
[docs] def pallet_finish(self, pallet: Union[Pallet, int, str]): """ Finish the processing of a pallet of boxes into inventory. Each box of the pallet will be processed. Nothing will be returned. Note - a pallet is still considered valid even if there are no boxes associated with it. Requirements: * A valid pallet record, pallet name, or ID * A pallet status indicating if the boxes have just been filled or are being moved to a new location Exception: 161 - An invalid pallet ID was passed in 162 - An invalid pallet name was passed in 166 - The pallet has an invalid location :param pallet: :return: """ if type(pallet) == Pallet: pallet_rec = pallet elif type(pallet) == int: try: pallet_rec = Pallet.objects.get(pk=pallet) except Pallet.DoesNotExist: raise InvalidValueError( f'161 - A pallet with ID: {pallet} does not exist') else: try: pallet_rec = Pallet.objects.get(name=pallet) except Pallet.DoesNotExist: raise InvalidValueError( f'162 - A pallet with the name "{pallet}" does not ' f'currently exist') try: location = Location.objects.get(pk=pallet_rec.location.id) except Location.DoesNotExist: raise InvalidValueError( f'166 - The location of "{pallet_rec.location}" does not ' f'exist') # TODO Mar 19 2020 travis - temporary fix pallet_status = pallet.pallet_status if pallet_status is None or pallet_status.strip() == '': pallet_status = Pallet.FILL pallet_boxes = PalletBox.objects.filter(pallet=pallet_rec) # transfer info and delete the pallet and its boxes in one trans with transaction.atomic(): if pallet_status == Pallet.FILL: # transfer the information to the real boxes for pallet_box in pallet_boxes: box = pallet_box.box product = pallet_box.product exp_year = pallet_box.exp_year exp_mo_start = pallet_box.exp_month_start exp_mo_end = pallet_box.exp_month_end self.box_fill( box=box, location=location, product=product, exp_year=exp_year, exp_mo_start=exp_mo_start, exp_mo_end=exp_mo_end, ) else: # move or merge the boxes to the new location for pallet_box in pallet_boxes: box = pallet_box.box self.box_move( box=box, location=location, ) # delete the pallet boxes for this pallet en mass pallet_boxes.delete() # now delete the pallet itself pallet_rec.delete() return
def _new_box(self, box_number: str, box_type: BoxType): """ Add a new, uniquely numbered box to the inventory system. :param box_number: :param box_type: :return: """ with transaction.atomic(): self.box = Box.objects.create(box_number=box_number, box_type=box_type, quantity=box_type.box_type_qty) self.activity.box_new(self.box.id) return def _fill_box(self): """ Fill a supposedly empty box and record activity for the event. :return: """ with transaction.atomic(): self.box.location = self.location self.box.product = self.product self.box.exp_year = self.exp_year self.box.exp_month_start = self.exp_mo_start self.box.exp_month_end = self.exp_mo_end self.box.date_filled = now() self.box.quantity = self.box.box_type.box_type_qty self.box.save() self.activity.box_fill(self.box.id) return def _move_box(self): """ Move a filled box to a new location. :return: """ with transaction.atomic(): self.box.location = self.location self.box.save() self.activity.box_move(self.box.id) return def _consume_box(self): """ Mark the box as empty. :return: """ # We are passing the box on to the activity class unchanged because # that method needs to see the former contents. This is so that if # someone has emptied the box and refilled it with something else, # the previous contents need to be consumed as well as the contents # just emptied. That method will clear out the box before returning. self.activity.box_empty(self.box.id) # since we didn't modify the box ourself, we have a stale copy of # the box record. Refresh it. box_id = self.box.id try: self.box = Box.objects.get(pk=box_id) except Box.DoesNotExist as exc: self.activity._report_internal_error(exc, f'Box with ID {id} disappeared after consume processing') return
if __name__ == "__main__": box_mgmt = BoxManagementClass() # EOF