def _create_item_transfers(self, sum_item_amounts, error_items=[]): """ Create ItemTransfers to alter inventory amounts """ transfers = [] for item, amount in sum_item_amounts.items(): try: amount_symbol = '{:~}'.format(amount['amount']).split(' ')[1] measure = AmountMeasure.objects.get(symbol=amount_symbol) amount['amount'] = amount['amount'].magnitude except: measure = AmountMeasure.objects.get(symbol='item') # Look up to see if there is a matching ItemTransfer already and then # use this instead try: transfer = ItemTransfer.objects.get( item=item, barcode=amount.get('barcode', None), coordinates=amount.get('coordinates', None)) # At this point need to subtract amount from available in existing # transfer! Need to mark in some way not completed though so can put # back if the trasnfer is cancelled transfer.amount_to_take = amount['amount'] except (ObjectDoesNotExist, MultipleObjectsReturned): transfer = ItemTransfer(item=item, barcode=amount.get('barcode', None), coordinates=amount.get( 'coordinates', None), amount_taken=amount['amount'], amount_measure=measure) if item in error_items: transfer.is_valid = False transfers.append(transfer) return transfers
def _create_item_transfers(self, sum_item_amounts, error_items=[]): """ Create ItemTransfers to alter inventory amounts """ transfers = [] for item, amount in sum_item_amounts.items(): try: amount_symbol = '{:~}'.format(amount['amount']).split(' ')[1] measure = AmountMeasure.objects.get(symbol=amount_symbol) amount['amount'] = amount['amount'].magnitude except: measure = AmountMeasure.objects.get(symbol='item') # Look up to see if there is a matching ItemTransfer already and then # use this instead try: transfer = ItemTransfer.objects.get(item=item, barcode=amount.get('barcode', None), coordinates=amount.get('coordinates', None)) # At this point need to subtract amount from available in existing # transfer! Need to mark in some way not completed though so can put # back if the trasnfer is cancelled transfer.amount_to_take = amount['amount'] except (ObjectDoesNotExist, MultipleObjectsReturned): transfer = ItemTransfer( item=item, barcode=amount.get('barcode', None), coordinates=amount.get('coordinates', None), amount_taken=amount['amount'], amount_measure=measure) if item in error_items: transfer.is_valid = False transfers.append(transfer) return transfers
def _update_products(self, task_id, products, activeworkflow, request, retry=False): """ Utility function to update products at end of task. """ task_list = activeworkflow.workflow.order.split(',') for p in products: data_entries = DataEntry.objects.filter(run_identifier=p.run_identifier) created_from_items = [] for entry in data_entries: created_from_items.append(entry.item) if retry: entry.state = 'failed' else: entry.state = 'succeeded' entry.save() # Make outputs # This requires an ItemTransfer to also be created for output in data_entries[0].data['output_fields']: item_type = ItemType.objects.get(name=output['lookup_type']) measure = AmountMeasure.objects.get(symbol=output['measure']) location = Location.objects.get(name='Lab') output_item = Item( name='{} {}'.format(p.product_identifier(), output['lookup_type']), identifier='{}OPT{}'.format(p.product_identifier(), datetime.datetime.now()), item_type=item_type, in_inventory=True, amount_available=float(output['amount']), amount_measure=measure, location=location, added_by=request.user, ) output_item.save() output_item.created_from.add(*created_from_items) # We want to add it to history but say it is # and addition rather than a subtraction tsf = ItemTransfer( item=output_item, amount_taken=float(output['amount']), amount_measure=measure, run_identifier=p.run_identifier, is_addition=True, ) tsf.save() p.run_identifier = '' p.task_in_progress = False p.save() if not retry: if p.current_task + 1 < len(task_list): p.current_task += 1 p.save() else: p.delete()
def start_task(self, request, pk=None): """ Start a task creating data entry and removing from inventory. Takes in task data, serializes for use in the data entry and takes the relevant amounts from the inventory. Can also act in preview mode and just return the info required for the task. ### query_params - _is_preview_: Don't actually start the task, just return the task information. ### query data - _task_ (**required**): A JSON representation of the task to be serialized - _products_ (**required**): A list of product ID's that are a part of the task - _input_files_: A list of file contents to be used as input for the task """ task_data = json.loads(self.request.data.get('task', None)) task_serializer = TaskValuesSerializer(data=task_data) product_data = json.loads(self.request.data.get('products', [])) file_data = self.request.data.getlist('input_files', []) is_preview = request.query_params.get('is_preview', False) active_workflow = self.get_object() uuid = uuid4() ureg = UnitRegistry() # This takes each input file and merges all rows with # matching identifiers into a single dict. # This will overwrite duplicated entries with the last # file processed, which is why values should only be # defined in a single file. input_file_data = {} for f in file_data: try: ft = FileTemplate.objects.get(name=f.name) except: pass else: parsed_file = ft.read(TextIOWrapper(f.file, encoding=f.charset)) if parsed_file: for key, row in parsed_file.items(): if key not in input_file_data: input_file_data[key] = {} input_file_data[key].update(row) else: return Response( {'message': 'Input file "{}" has incorrect headers/format'.format(f.name)}, status=400) products = Product.objects.filter(id__in=[p['product'] for p in product_data]) if task_serializer.is_valid(raise_exception=True) and len(products) > 0: # A dict of items and amounts required from inputs required_amounts = {} input_items = [] fields_from_file = [] data_items = {} # Look for items for use as inputs to the task and add the amounts that # are required to the list for prd in products: items = Item.objects.filter( products__id=prd.id, item_type__name=task_data['product_input']) if items.count() == 0: return Response( {'message': '{}: {} does not contain any items to use as input'.format( prd.product_identifier, prd.name)}, status=400) input_items.extend(items) # Add items from Product's to the list for itm in items: identifier = frozenset([prd.product_identifier, itm.identifier]) data_items[identifier] = { 'data': task_serializer.data, 'product': prd, 'item': itm } self.update_item_amounts(itm.identifier, task_serializer.data['product_input_amount'], task_serializer.data['product_input_measure'], required_amounts, ureg) # Look through any input files for matching identifiers and add their amounts # to the list for key, row in input_file_data.items(): for header, value in row.items(): # If the header has identifier/amount combo then # is aplicable to an item in the product # if not, it is applicable TO the product exists = False matched_headers = [] label = header.rstrip(' identifier').rstrip(' amount') # Look for _amount + _identifier combos and math them up if header.endswith('identifier') and header not in matched_headers: identifier = value amount_field_name = '{} amount'.format(label) if amount_field_name in row.keys(): amount = row[amount_field_name] matched_headers.extend([header, amount_field_name]) exists = True elif header.endswith('amount') and header not in matched_headers: identifier_field_name = '{} identifier'.format(label) if identifier_field_name in row.keys(): identifier = row[identifier_field_name] amount = value matched_headers.extend([header, identifier_field_name]) exists = True elif identifier_field_name not in row.keys() and header in row.keys(): # This value is directly for an amount on the item # rather than another input identifier = key[1] amount = value label = header.replace(' ', '_') measure_label = label.replace('amount', 'measure') matched_headers.append(header) exists = True if exists: # Look for the matching pairs try: data_entry_field = next((fld for fld in data_items[key]['data'][ 'input_fields'] if fld['label'] == label)) data_entry_field['amount'] = amount measure = data_entry_field['measure'] except StopIteration: data_items[key]['data'][label] = amount measure = data_items[key]['data'][measure_label] # Try to get the item from the inventory and add to list try: ti = Item.objects.get(identifier=identifier) except ObjectDoesNotExist: return Response( {'message': '{} does not exist in the inventory'.format(identifier)}, status=400) if ti not in input_items: input_items.append(ti) self.update_item_amounts(ti.identifier, amount, measure, required_amounts, ureg) # Record this field has data from a file and does not need to # be processed again fields_from_file.append(label) # Now just read through the fields left and fill in any more details for itm in task_serializer.data['input_fields']: # If we already have set the value in a file we don't want to overwrite it if itm['label'] not in fields_from_file and itm['from_input_file'] is False: try: ti = Item.objects.get(identifier=itm['inventory_identifier']) except ObjectDoesNotExist: return Response({'message': '{} does not exist in the inventory'.format( itm['inventory_identifier'])}, status=400) if ti not in input_items: input_items.append(ti) self.update_item_amounts(ti.identifier, itm['amount'], itm[ 'measure'], required_amounts, ureg) # But if it's suposed to have been from a file and is still here, return an error elif itm['label'] not in fields_from_file and itm['from_input_file'] is True: return Response({'message': 'The value for field "{}" is not present in file' .format(itm['label'])}, status=400) # input checks preview_data = [] valid_amounts = True amount_error_messages = [] for item in input_items: # Check enough of each item is avilable. # First, translate to a known amount (if possible) or just be presented as # a raw number try: available = item.amount_available * ureg(item.amount_measure.symbol) except UndefinedUnitError: available = item.amount_available required = required_amounts[item.identifier] # The actual check if available < required: missing = (available - required_amounts[item.identifier]) * -1 valid_amounts = False amount_error_messages.append( 'Inventory item {} ({}) is short of amount by {}'.format( item.identifier, item.name, missing)) # If it's a preview then just serialize the amount, don't actually # do anything with it if is_preview: amount = required_amounts[item.identifier] amount_symbol = '{:~}'.format(amount).split(' ')[1] item_transfer = ItemTransfer( item=item, amount_taken=amount.magnitude, amount_measure=AmountMeasure.objects.get(symbol=amount_symbol) ) sit = ItemTransferPreviewSerializer(item_transfer) preview_data.append(sit.data) if not is_preview: # If valid, continue to create item transfers if valid_amounts: for item in input_items: # Create item transfers amount = required_amounts[item.identifier] amount_symbol = '{:~}'.format(amount).split(' ')[1] item_transfer = ItemTransfer( item=item, run_identifier=uuid, amount_taken=amount.magnitude, amount_measure=AmountMeasure.objects.get(symbol=amount_symbol) ) item_transfer.save() # Alter amounts in DB to corrospond to the amount taken new_amount = available - required_amounts[item.identifier] item.amount_available = new_amount.magnitude item.save() else: return Response({'message': '\n'.join(amount_error_messages)}, status=400) # make data entries to record the values of the task task = TaskTemplate.objects.get(pk=task_data['id']) for key, data in data_items.items(): entry = DataEntry( run_identifier=uuid, product=data['product'], item=data['item'], created_by=self.request.user, state='active', data=data['data'], workflow=active_workflow.workflow, task=task ) entry.save() # update products that task started active = WorkflowProduct.objects.get(product=data['product']) active.task_in_progress = True active.run_identifier = uuid active.save() return Response({'message': 'Task started'}) return Response(preview_data) return Response({'message': 'You must provide task data and products'}, status=400)