from simpy import Environment, Event, Resource, FilterStore, PriorityResource, AnyOf, AllOf
import numpy as np
from abc import abstractmethod
from typing import List, Dict, Tuple
import math
from dataclasses import make_dataclass, dataclass, field
from collections import Counter, defaultdict
import pandas as pd
from functools import partial, wraps
from anytree import Node, RenderTree, find, findall
import uuid
import warnings

warnings.simplefilter("error", RuntimeWarning)


@dataclass
class Component:
    """
    A dataclass representing a Component of a Product with id, quality, part_type and UUID.

    Attributes
    ----------
        id : str
            The identifier for the component
        quality : float
            The quality rating of the component
        part_type : str
            defining which type this component is
        status_time : list[tuple]
            Record every status change and simulation time per component.
        UUID : str
            the unique identifier for a specific component within the simulation
    """
    id: str
    quality: float
    part_type: str
    status_time: List[Tuple] = field(default_factory=list)
    UUID: str = field(default_factory=lambda: str(uuid.uuid4()))


@dataclass(unsafe_hash=True)
class Product:
    """
    A dataclass representing a Product.

    Attributes
    ----------
        id : str
            The unique identifier for the product
        type : str
            The type of the product, e.g. Laptop, Monitor, ...
        quality : float
            The quality rating of the product
        components : List[Component]
            The list of Component objects that make up the product
        price : float
            The acquisition price of the product
        status_time : list[tuple]
            Record every status change and simulation time per product.
        UUID : str
            the unique identifier for a specific component within the simulation
    """
    id: str
    type: str
    quality: float
    components: List[Component]
    price: float
    status_time: List[Tuple] = field(default_factory=list)
    UUID: str = field(default_factory=lambda: str(uuid.uuid4()))


@dataclass
class Order:
    """
    A dataclass representing an order of a core

    Attributes
    ----------
        BatchesOfCores : List[Product]
            A list holding the products from the order.
        delivered : bool
            Boolean to indicate if order is delivered or not.
        lead_time : int
            Time until the order is delivered or can be collected.
        UUID : str
            the unique identifier for a specific offer within the simulation
    """
    BatchesOfCores: [Product]
    delivered: bool
    lead_time: int
    UUID: str = field(default_factory=lambda: str(uuid.uuid4()))


@dataclass
class Offer:
    """
    A dataclass representing an offer of a core

    Attributes
    ----------
        BatchesOfCores : List[Product]
            A list holding the products from the offer.
        valid_duration : int
            An integer indicating how long the offer will be valid.
        delivered : bool
            Indicator showing if offer would be delivered or not.
        lead_time : int
            Time until the offer is delivered or can be collected after ordering.
        avg_quality : float
            average quality for the products in the whole Offer
        UUID : str
            the unique identifier for a specific offer within the simulation
    """
    BatchesOfCores: [Product]
    valid_duration: int
    delivered: bool
    lead_time: int
    avg_quality: float
    UUID: str = field(default_factory=lambda: str(uuid.uuid4()))


@dataclass
class Demand:
    """
    A dataclass representing a demand for a core

    Attributes
    ----------
        product_id : str
            The unique identifier for the product
        batch_size : int
            number of products demanded
        valid_duration : int
            An integer indicating how long the demand will be valid
        UUID : str
             the unique identifier for a specific demand within the simulation
    """
    product_id: str
    batch_size: int
    valid_duration: int
    UUID: str = field(default_factory=lambda: str(uuid.uuid4()))


class HumanResources:
    """
    A class representing the Human Resources who are needed for the processes.

    Attributes
    ----------
    skilled_workers : List[SkilledResource]
        list of human resources with different skill sets
    env :

    Methods
    -------
    get_skilled_workers()
        get all skilled workers
    request_skilled_workers
        function to request capable workers for a specified processes
    """
    def __init__(self, skill_data: List[List[str]], env: Environment):
        """
        Parameters
        ----------
        skill_data : List[List[str]]
            List of human resources which have a specified skill data set
        env: Environment
            process environment
        """
        self.skilled_workers = [SkilledResource(skills, env) for skills in skill_data]
        self.env = env

    def get_skilled_workers(self):
        return self.skilled_workers

    def request_skilled_workers(self, skill: str, duration: float, products: List[Product],
                                amount: int = 1, prio: int = 1):
        """
        Request the workers which are capable for specified processes

        Parameters
        ----------
        skill : str
            the skill of a worker needed for the process
        duration : float
            duration of the process
        products : List[Product]
            products which are processed
        amount : int
            amount of human resources needed to perform the process (default 1)
        prio : int
            priority for the process to be processed
        """
        capable_workers = [worker for worker in self.skilled_workers if skill in worker.skills]
        if len(capable_workers) >= amount:
            granted_requests = []

            # Wait for the required number of workers to be granted
            while len(granted_requests) < amount:
                capable_worker_requests = [worker.request(priority=prio) for worker in
                                           capable_workers]  # Request for all capable workers
                # Wait for any worker request to be granted
                any_of_event_true = yield self.env.any_of(capable_worker_requests)

                if len(any_of_event_true.events) < amount:
                    for req in capable_worker_requests:
                        req.cancel()
                        req.resource.release(req)
                    yield self.env.timeout(0.1)
                    continue
                # Add the granted worker to the list
                for req in capable_worker_requests:
                    if req in any_of_event_true and len(granted_requests) < amount:
                        granted_requests.append(req)
                # Remove the granted worker request to avoid reusing it
                for req in capable_worker_requests:
                    if req not in granted_requests:
                        req.cancel()
                        req.resource.release(req)
            # Now we have `amount` of granted workers and can proceed with the task
            for granted_request in granted_requests:
                granted_request.resource.utilization_data.append(
                    (self.env.now, len(granted_request.resource.queue), "start_using", skill, duration)
                )
            for p in products:
                p.status_time.append((self.env.now, skill, 'start'))

            # Proceed with the task (duration of usage)
            yield self.env.timeout(duration)
            return granted_requests
        else:
            raise ValueError("Not enough skilled workers available for task! - check import data")

    def release_skilled_workers(self, granted_requests, skill: str, products: List[Product]):
        """
        Release the workers which are working on special requests

        Parameters
        ----------
        granted_requests : List[Requests]
            List of requests for human skilled resources, which have to be released
        skill : str
            skill which the human resources are used in request
        products : List[Product]
            products which were processed by the human resources
        """
        # Release all the workers after the tasks
        for granted_request in granted_requests:
            granted_request.resource.utilization_data.append(
                (self.env.now, len(granted_request.resource.queue), "finished_using", skill, 0)
            )
            granted_request.resource.release(granted_request)
        for p in products:
            p.status_time.append((self.env.now, skill, 'end'))


class SkilledResource(PriorityResource):
    """
    Skilled Resource is a subclass for Human Resource needed to safe the skill data per worker

    Attributes
    ----------
    skills : List[str]
        List of skills one resource is capable of performing
    utilization_data : List[Tuples]
        List to safe the utilization information of a resource
    UUID : str
        the unique identifier for a specific SkilledResource within the simulation
    """
    def __init__(self, skills: List[str], env, *args, **kwargs):
        super().__init__(env, *args, **kwargs)
        self.skills = skills
        self.utilization_data = []
        self.UUID = str(uuid.uuid4())

    def request(self, *args, **kwargs):
        return super().request(*args, **kwargs)

    def release(self, *args, **kwargs):
        return super().release(*args, **kwargs)


class AbstractReUseModel:
    env: Environment
    stocks: FilterStore
    stock_sellable: FilterStore
    component_stock: FilterStore
    core_stock_capacities: Dict[str, int]
    import_doc: str
    core_quality_sellable_limits: Dict[str, float]
    core_quality_refurbish_limits: Dict[str, float]
    core_quality_scavenging_limits: Dict[str, float]
    products_list: List[Product]
    products_components_dict: Dict[str, Tuple[str, int]]
    producttype_tree: Node
    inventory_components: FilterStore
    orders: List[Order]
    offers: List[Offer]

    @abstractmethod
    def import_values(self, import_document):
        pass

    @abstractmethod
    def generate_initial_stock(self, initial_core_stock: Dict):
        pass

    @abstractmethod
    def create_product(self, core_id: str,
                       quality_distribution,
                       avg_quality: int,
                       buying_price: int):
        pass

    @abstractmethod
    def create_order(self, core_id: str,
                     quality_distribution,
                     batch_size: int,
                     buying_price: int,
                     lead_time: int,
                     delivered: int):
        pass

    @abstractmethod
    def generate_offer_manager(self, offer_number: str):
        pass

    @abstractmethod
    def delete_offer_demand_manager(self):
        pass

    @abstractmethod
    def place_order(self):
        pass

    @abstractmethod
    def order_delivery(self, order: Order):
        pass

    @abstractmethod
    def pickup_goods(self, order: Order):
        pass

    @abstractmethod
    def unpacking(self, order: Order):
        pass

    @abstractmethod
    def grading(self):
        pass

    @abstractmethod
    def grading_process(self, product_grading: Product):
        pass

    @abstractmethod
    def disposition_after_grading(self):
        pass

    @abstractmethod
    def quality_aggregation_func(self, core: Product):
        pass

    @abstractmethod
    def generate_demand_manager(self, demand_number: str):
        pass

    @abstractmethod
    def meet_demand(self):
        pass

    @abstractmethod
    def shipping(self, products_to_ship: List[Product]):
        pass

    @abstractmethod
    def refurbish(self):
        pass

    @abstractmethod
    def refurbish_process(self, product_refurbish: Product):
        pass

    @abstractmethod
    def refurbish_end(self, product_refurbish: Product):
        pass

    @abstractmethod
    def check_components(self, product_refurbish: Product):
        pass

    @abstractmethod
    def find_replacement_component(self, component_type: str, product_refurbish: Product):
        pass

    @abstractmethod
    def calc_utilization_resource(self):
        pass

    @abstractmethod
    def replenish_component_manager(self):
        pass

    @abstractmethod
    def add_to_stock(self, qty_put_to_storage: List[int], max_stock_sizes: List[int], leadtime_order: int):
        pass

    @abstractmethod
    def determine_order_quantity(self, function_name: str, max_inventory_sizes: List[int],
                                 current_inventories: List[int], arg3: int, arg4: int):
        pass

    @abstractmethod
    def disposition_decision(self, function_name: str, threshold: Dict, min_quantity: int, core_id: str,
                             batch_size: int, quality: float):
        pass

    @abstractmethod
    def scavenging_component_manager(self):
        pass

    @abstractmethod
    def scavenging_process(self, core_to_scavenge: Product):
        pass

    @abstractmethod
    def recycling(self):
        pass

    @abstractmethod
    def recycling_process(self, core_to_recycle: Product):
        pass


class ReUseModel(AbstractReUseModel):
    """
    Class consisting of all processes and information needed for the ReUse Process of a company.

    Attributes
    ----------
    env : Environment
        Simulation Environment
    COMPONENTS : pandas Dataframe
        all possible components for all products simulated (Import File: 'components')
        id, type, name, max stock capacity per component

    stocks : FilterStore
        storing all products which are in the system and need to be processed
        first initialized with the initial stock (Import File: 'products' - initial_core_stock)
        and then adapted by ordering products and finalizing the process
    stocks_sellable : FilterStore
        storing all products which are sellable i.e. their quality exceeds a certain limit
    work_in_progress_stocks : FilterStore
        storing all products during refurbish and demand/shipping
    grading_stock : FilterStore
        storing all products that are currently in the grading process
    refurbish_stock : FilterStore
        storing all products that are currently in the refurbish process
    inventory_components : FilterStore
        storing all components that are in stock
        first initialized with the initial component stock (Import File: 'components' - initial_comp_stock)
        and then adapted by ordering components, scavenging, refurbish

    offers : List(Offers)
        storing all offers of products
    demands : List(Dict)
        storing all valid demands for products

    offer_parameters : Dictionary
        all parameters for the offers saved in a Dictionary (Import File: 'offers')
        offer_number, product_id, offer_arrival_pattern, offer_arrival, valid_duration, batch_size, quality_comp,
        buying_price, lead_time, delivered
    place_order_parameters : Dictionary
        all parameters for the order placement are saved in the Dictionary (Import File: 'place_order')
        price_limit, disposition_below_limit, disposition_above_limit
    pickup_goods_parameters : Dictionary
        all parameters for the pick_up_goods process are saved in the Dictionary (Import File: 'pickup_goods')
        process_simulated, duration, human_resources
    unpacking_parameters : Dictionary
        all parameters for the unpacking process are saved in the Dictionary (Import File: 'unpacking')
        process_simulated, duration, human_resources
    grading_parameters : Dictionary
        all parameters for the grading process are saved in the Dictionary (Import File: 'grading')
        process_simulated, duration, human_resources
    replenish_components_parameters : Dictionary
        all parameters for the replenishment strategy of components (Import File: 'replenish_components')
        process_simulated, period_review, leadtime_order, replenishment_policy
    refurbish_parameters : Dictionary
        all parameters for the refurbish process are saved in the Dictionary (Import File: 'refurbish')
        process_simulated, human_resources, duration
    demand_parameters : Dictionary
        all parameters for the demand process are saved in the Dictionary (Import File: 'demand')
        demand_number, product_type, product_id, demand_arrival_pattern, demand_arrival_distribution, valid_duration,
        batch_size
    shipping_parameters : Dictionary
        all parameters for the shipping process are saved in the Dictionary (Import FIle: 'shipping)
        process_simulated, duration, human_resources
    human_resources: HumanResources
        all workers responsible for the processes in the company with their special skill set
        (Import File: 'company_data_resources')

    Methods
    -------
    import_values(import_doc_excel)
        initializes the attributes with the data given in the Import File
    generate_initial_stock(initial_core_stock)
        creating the products which are on stock at the beginning of simulation

    create_product(core_id, quality_distribution_comp, avg_quality, buying_price)
        create products using the Product and Component dataclasses
    create_order(core_id, quality_distribution, batch_size, buying_price, lead_time, delivered)
        create the initial orders (used only at the start of the simulation)

    generate_offer_manager(offer_number)
        generates offers of products with the information corresponding to the offer_number
    delete_offer_demand_manager()
        deletes offers and demands if their valid duration has expired
    place_order()
        places orders of offered products based on different disposition options
    disposition_decision(function_name, core_id, batch_size, quality)
        determines if an order for products should be placed. based on the function_name parameter individual
        disposition decisions can be implemented
    utilization_based_distribution(threshold_utilization, threshold_inventory, core_id, batch_size)
        returns the order_quantity of the products if the utilization of the human resources is below the threshold
        considering that inventory does not exceed maximum stock capacity times inventory threshold
    quality_based_disposition(core_id, quality, batch_size)
        returns the order_quantity of the products if the offered quality exceeds the core_quality_refurbish_limit
        considering that inventory does not exceed maximum stock capacity times inventory threshold
    inventory_based_disposition(core_id, batch_size)
        returns the order_quantity of the products if there is enough space in inventory
    order_delivery(order)
        after the lead time the order is sent to pickup or unpacking
    pickup_goods(order)
        workers have to pick up and afterwards unpack the order. products are put in stocks.
    unpacking(order)
        workers unpack the order. products are put in stocks.
    grading()
        grading of products resulting in a quality for the products between 0 and 1
    quality_aggregation_func()
        determining the quality of a product based on the weighted average of the components qualities
    disposition_after_grading()
        checking if a products quality exceeds the sellable quality limit (Import File: 'producttype' -
        quality_sellable_limit) of the product type after grading
        products are put into the stocks_sellable
    refurbish()
        refurbishing products which quality exceeds the core_quality_refurbish_limits (Import File: 'producttype' -
        quality_refurbish_limit) and for which all needed components are on stock
        all components with lower quality than the threshold (Import File: 'producttype_parts' - min_quality) are
        exchanged with components on stock (inventory_components)
    check_components(product_refurbish)
        returns True if all components which have to be replaced for the product_refurbish are on stock
    find_replacement_component(component_type, product_refurbish)
        returns the best component to fit for the product to refurbish based on the priorities
    replenish_component_manager()
        checking if new components should be ordered according to the replenishment policy
    add_to_stock(qty_put_to_storage, max_stock_sizes, leadtime_order)
        ordered components are put on stock after the lead time considering the maximum stock capacities per component
    determine_order_quantity(function_name, max_inventory_sizes, current_inventories, arg3=5, arg4=6)
        calculates the order quantity for the components. depending on the function_name parameter different inventory
        order policies are implemented.
    max_order_periodic_review(max_inventory_sizes, current_inventories)
        ordering components up to the max_inventory_sizes per component
    scavenging_component_manager()
        scavenging products which quality is lower than the core_quality_refurbish_limits and exceeds the
        core_quality_scavenging_limits (Import File: 'producttype' - quality_refurbish_limit, quality_scavenging_limit)
        components which exceed their threshold (Import File: 'producttype_parts' - min_quality) quality are stored in
        the inventory_components if there is still stock capacity left for the specific component
    recycling()
        recycling products which quality is lower than the core_quality_scavenging_limits (Import File: 'producttype' -
        quality_scavenging_limit) after grading
    generate_demand_manager(demand_number)
        generates demand for products to be sold with the info corresponding to the demand_number
    meet_demand()
        meeting the demand for used products
    shipping()
        shipping sold products
    calc_utilization_resource()
        calculates the mean utilization of the human resources
    """
    def __init__(self,
                 import_doc_excel: str,
                 env: Environment):

        self.mean_utilization = None
        self.COMPONENTS = None
        self.env = env
        self.stocks = FilterStore(env)
        self.work_in_progress_stocks = FilterStore(env)
        self.grading_stock = FilterStore(env)
        self.refurbish_stock = FilterStore(env)
        self.ordered_quantity = {}
        self.stocks_sellable = FilterStore(env)
        self.inventory_components = FilterStore(env)
        self.ordered_components = FilterStore(env)
        self.products_simulation = []

        self.rng = np.random.default_rng()

        # Offers
        self.offers = []
        self.offer_export_data = []
        self.offer_attributes_export_data = []

        # Orders
        self.orders = []
        self.order_export_data = []

        # Demands
        self.demands = []
        self.demand_export_data = []
        self.demand_attributes_export_data = []

        self.offer_parameters = {}
        self.place_order_parameters = {}
        self.grading_parameters = {}
        self.pickup_goods_parameters = {}
        self.unloading_pickup_parameters = {}
        self.unloading_delivered_parameters = {}
        self.unpacking_parameters = {}
        self.replenish_components_parameters = {}
        self.refurbish_parameters = {}
        self.scavenging_parameters = {}
        self.recycling_parameters = {}
        self.demand_parameters = {}
        self.shipping_parameters = {}
        self.human_resources = HumanResources

        self.import_values(import_doc_excel)

        # Processes
        for offer_number in self.offer_parameters:
            self.env.process(self.generate_offer_manager(offer_number))
        self.env.process(self.delete_offer_demand_manager())
        self.env.process(self.place_order())

        # grading process
        if self.grading_parameters['process_simulated'] == 'no':  # no grading in company but necessary for process
            self.grading_parameters['duration_active_start'] = '0'
            self.grading_parameters['human_resources_active_start'] = '0'
            self.grading_parameters['duration_passive'] = '0'
            self.grading_parameters['duration_active_end'] = '0'
            self.grading_parameters['human_resources_active_end'] = '0'
            self.grading_parameters['priority'] = '0'
        self.env.process(self.grading)

        self.env.process(self.disposition_after_grading())

        if self.refurbish_parameters['process_simulated'] == 'yes':
            self.env.process(self.refurbish)

        for demand_number in self.demand_parameters:
            self.env.process(self.generate_demand_manager(demand_number))
        self.env.process(self.meet_demand())

        if self.replenish_components_parameters['process_simulated'] == 'yes':
            self.env.process(self.replenish_component_manager())

        if self.scavenging_parameters['process_simulated'] == 'yes':
            self.env.process(self.scavenging_component_manager())

        if self.recycling_parameters['process_simulated'] == 'yes':
            self.env.process(self.recycling())

    def import_values(self, import_document):
        """
        function to import all values from the import Excel file
        information on product types, product id, human resources, processes

        Parameter
        ---------
        import_document : str
            Name of the import Excel file containing all information
        """
        # create Dictionary for all product types and their stock capacities
        product_type_df = pd.read_excel(import_document, sheet_name='producttype', comment='#')
        self.core_stock_capacities = dict(zip(product_type_df['producttype'],
                                              product_type_df['max_stock_capacity']))
        self.core_quality_sellable_limits = dict(zip(product_type_df['producttype'],
                                                     product_type_df['quality_sellable_limit']))
        self.core_quality_refurbish_limits = dict(zip(product_type_df['producttype'],
                                                      product_type_df['quality_refurbish_limit']))
        self.core_quality_scavenging_limits = dict(zip(product_type_df['producttype'],
                                                       product_type_df['quality_scavenging_limit']))
        self.ordered_quantity = dict.fromkeys(product_type_df['producttype'], 0)

        # create a list of all possible products
        products_df = pd.read_excel(import_document, sheet_name='products', comment='#')
        self.products_list = [Product(id=product_id, type=product_type, quality=-1.0, components=[], price=0)
                              for product_id, product_type in zip(products_df['products'], products_df['type'])]

        initial_core_stock = {product: {'initial_core_stock': stock,
                                        'quality_distribution_comp': quality,
                                        'buying_price': price}
                              for product, stock, quality, price in zip(products_df['products'],
                                                                        products_df['initial_core_stock'],
                                                                        products_df['quality_distribution_comp'],
                                                                        products_df['buying_price'])}
        # import all product-types with parts
        product_type_parts_df = pd.read_excel(import_document, sheet_name='producttype_parts', comment='#')

        # import all possible components
        self.COMPONENTS = pd.read_excel(import_document, sheet_name='components', index_col=0, comment='#')
        products_components_df = pd.read_excel(import_document, sheet_name='products_components', comment='#')

        self.products_components_dict = products_components_df.groupby('products').apply(
            lambda x: tuple(zip(x['components'], x['prio']))).to_dict()

        # create Anytree for product-types and parts relations
        self.producttype_tree = Node("root")
        for index, row in product_type_parts_df.iterrows():
            parent_node = find(self.producttype_tree, lambda node: node.name == row['producttype'])

            if parent_node is None:
                parent_node = Node(row['producttype'],
                                   parent=self.producttype_tree)

            part_node = Node(row['component type'],
                             parent=parent_node,
                             quantity=row['qty'],
                             repair_duration=row['repair_duration'],
                             scavenge_duration=row['scavenge_duration'],
                             recycle_duration=row['recycle_duration'],
                             min_quality=row['min_quality'],
                             comp_quality_weight=row['comp_quality_weight'])

        # create the initial inventory of components
        self.inventory_components = FilterStore(self.env)
        for index, row in self.COMPONENTS.iterrows():
            for i in range(row['initial_comp_stock']):
                component = Component(index, 1, row['Type'])
                self.inventory_components.put(component)

        self.generate_initial_stock(initial_core_stock)

        initial_orders_pd = pd.read_excel(import_document, sheet_name='initial_orders', comment='#')

        for index, order_row in initial_orders_pd.iterrows():
            self.create_order(order_row['Product_ID'], eval(str(order_row['quality_distribution'])),
                              order_row['quantity'],
                              order_row['buying_price'], order_row['lead_time'], order_row['delivered'])

        # human resources
        human_resources_df = pd.read_excel(import_document, sheet_name='company_data_resources', comment='#')
        skill_data = human_resources_df.iloc[:, 0:].values.tolist()
        skill_data_clean = [[skill for skill in skill_set if not (isinstance(skill, float) and math.isnan(skill))] for
                            skill_set in skill_data]
        skill_data_clean = [skill_set for skill_set in skill_data_clean if len(skill_set) > 0]

        self.human_resources = HumanResources(skill_data_clean, self.env)

        # offers
        offers_df = pd.read_excel(import_document, sheet_name='offers', comment='#')
        self.offer_parameters = {
            offer: {'product_id': product_id,
                    'offer_arrival_pattern': offer_arrival_pattern,
                    'offer_arrival': offer_arrival,
                    'valid_duration': valid_duration,
                    'batch_size': batch_size,
                    'quality_comp': quality_comp,
                    'buying_price': buying_price,
                    'lead_time': lead_time,
                    'delivered': delivered}
            for offer, product_id, offer_arrival_pattern, offer_arrival, valid_duration, batch_size,
            quality_comp, buying_price, lead_time, delivered in
            zip(offers_df['offers'],
                offers_df['product_id'],
                offers_df['offer_arrival_pattern'],
                offers_df['offer_arrival'],
                offers_df['valid_duration'],
                offers_df['batch_size'],
                offers_df['quality_comp'],
                offers_df['buying_price'],
                offers_df['lead_time'],
                offers_df['delivered'])
        }

        # demand
        demand_df = pd.read_excel(import_document, sheet_name='demand', comment='#')
        self.demand_parameters = {
            demand: {'product_type': product_type,
                     'product_id': product_id,
                     'demand_arrival_pattern': demand_arrival_pattern,
                     'demand_arrival_distribution': demand_arrival_distribution,
                     'valid_duration': valid_duration,
                     'batch_size': batch_size}
            for demand, product_type, product_id, demand_arrival_pattern, demand_arrival_distribution,
            valid_duration, batch_size in
            zip(demand_df['demand'],
                demand_df['product_type'],
                demand_df['product_id'],
                demand_df['demand_arrival_pattern'],
                demand_df['demand_arrival_distribution'],
                demand_df['valid_duration'],
                demand_df['batch_size'])
        }

        # place order
        place_order_df = pd.read_excel(import_document, sheet_name='place_order', comment='#')
        self.place_order_parameters = dict(zip(place_order_df['Place_Order'], place_order_df['Value']))

        # grading
        grading_df = pd.read_excel(import_document, sheet_name='grading', comment='#')
        self.grading_parameters = dict(zip(grading_df['Grading'], grading_df['Value']))

        # pickup_goods
        pickup_goods_df = pd.read_excel(import_document, sheet_name='pickup_goods', comment='#')
        self.pickup_goods_parameters = dict(zip(pickup_goods_df['pick up goods'], pickup_goods_df['Value']))

        # unloading after pickup
        unloading_df = pd.read_excel(import_document, sheet_name='unloading_after_pickup', comment='#')
        self.unloading_pickup_parameters = dict(zip(unloading_df['Unloading_after_pickup'], unloading_df['Value']))

        # unloading after delivery
        unloading_df = pd.read_excel(import_document, sheet_name='unloading_after_delivery', comment='#')
        self.unloading_delivered_parameters = dict(zip(unloading_df['Unloading_after_delivery'], unloading_df['Value']))

        # unpacking
        unpacking_df = pd.read_excel(import_document, sheet_name='unpacking', comment='#')
        self.unpacking_parameters = dict(zip(unpacking_df['Unpacking'], unpacking_df['Value']))

        # refurbish
        refurbish_df = pd.read_excel(import_document, sheet_name='refurbish', comment='#')
        self.refurbish_parameters = dict(zip(refurbish_df['Refurbish'], refurbish_df['Value']))

        # scavenging
        scavenging_df = pd.read_excel(import_document, sheet_name='scavenging', comment='#')
        self.scavenging_parameters = dict(zip(scavenging_df['Scavenging'], scavenging_df['Value']))

        # recycling
        recycling_df = pd.read_excel(import_document, sheet_name='recycling', comment='#')
        self.recycling_parameters = dict(zip(recycling_df['Recycling'], recycling_df['Value']))

        # replenish components
        replenish_components_df = pd.read_excel(import_document, sheet_name='replenish_components', comment='#')
        self.replenish_components_parameters = dict(zip(replenish_components_df['replenish_components'],
                                                        replenish_components_df['Value']))

        # shipping components
        shipping_df = pd.read_excel(import_document, sheet_name='shipping', comment='#')
        self.shipping_parameters = dict(zip(shipping_df['shipping'], shipping_df['Value']))

    def generate_initial_stock(self, initial_core_stock):
        """
        Process to generate the initial stock of products

        Parameter
        ---------
        initial_core_stock : Dictionary
            per product id one dictionary with the amount, quality and price of product on stock
            Import File: 'products' - product_id, product_type, initial_core_stock, quality, buying_price
        """
        for product_id in initial_core_stock:
            for count in range(initial_core_stock[product_id]['initial_core_stock']):
                average_quality = -1  # no quality assigned for product
                product = self.create_product(product_id,
                                              eval(str(initial_core_stock[product_id]['quality_distribution_comp'])),
                                              average_quality, initial_core_stock[product_id]['buying_price'])
                self.stocks.put(product)

    def create_product(self, core_id: str,
                       quality_distribution_comp,
                       avg_quality: float,
                       buying_price: int):
        """
        Process to create a product with the necessary components using the dataclasses Product and Component.

        Parameter
        ---------
        core_id : str
            id of the core/product
        quality_distribution_comp : np.distribution/float
            distribution or value for the quality of components between 0 and 1
        avg_quality : float
            average quality of the cores/products in a batch
        buying_price : int
            buying price per core/product
        """
        core_type = next((product.type for product in self.products_list if product.id == core_id), None)
        core_node = find(self.producttype_tree, lambda node: node.name == core_type)

        comp_list = []
        for child in core_node.children:
            component = None
            priority = 0
            for comp in self.products_components_dict[core_id]:
                comp_name = comp[0]
                comp_priority = comp[1]
                comp_type = self.COMPONENTS.loc[comp_name, 'Type']
                if comp_type == child.name and (component is None or comp_priority > priority):
                    quality_component = min(eval(str(quality_distribution_comp)), 1)
                    component = Component(comp_name, quality_component, comp_type)
                    priority = comp[1]
            if component is not None:
                comp_list.append(component)
            else:
                raise ValueError("no component for part found - check import data")
        prod = Product(id=core_id, type=core_type, quality=avg_quality, components=comp_list, price=buying_price)
        self.products_simulation.append(prod)
        return prod

    def create_order(self, core_id: str,
                     quality_distribution,
                     batch_size: int,
                     buying_price: int,
                     lead_time: int,
                     delivered: int):
        """
        create the initial orders (used only at the start of the simulation)
        quality for products is -1, indicating that the quality is not known when ordering the products

        Parameters
        ----------
        core_id : str
            id of the core/product
        quality_distribution : np.distribution/float
            distribution or value for the quality of components between 0 and 1
        batch_size : int
            amount of products in order
        buying_price : int
            buying price per core/product
        lead_time : int

        delivered : int

        """
        products = []
        for product_num in range(batch_size):
            avg_quality = -1
            prod = self.create_product(core_id, quality_distribution, avg_quality, buying_price)
            products.append(prod)

        products_order = Order(BatchesOfCores=products, lead_time=lead_time, delivered=bool(delivered))
        self.env.process(self.order_delivery(products_order))
        product_type = next((product.type for product in self.products_list if product.id == core_id), None)
        self.ordered_quantity[product_type] += batch_size

    def generate_offer_manager(self, offer_number):
        """
        generates offers of products with the information corresponding to the offer_number
        the average quality of the products in the offer is calculated based on the quality of the components

        Parameters
        ----------
        offer_number : str
            Import File: 'offers' - 'offers'
            identifier for the information of offers in the Import File

        Variables from Import File
        --------------------------
        offer_parameters[offer_number]['offer_arrival_pattern'] : str
            'offers' - offer_arrival_pattern
            offers arriving either 'once' or 'cyclical'
        offer_parameters[offer_number]['offer_arrival'] : int/float/numpy rng distribution
            'offers' - offer_arrival
            time period when the offer arrives
        offer_parameters[offer_number]['product_id'] : str
            'offers' - product_id
            id of the offered products
        offer_parameters[offer_number]['valid_duration'] : int/float/numpy rng distribution
            'offers' - valid_duration
            time the offer is valid after arriving
        offer_parameters[offer_number]['batch_size'] : int/float/numpy rng distribution
            'offers' - batch_size
            number of products offered within on offer
        offer_parameters[offer_number]['quality_comp'] : int/float/numpy rng distribution
            'offers' - quality_comp
            quality (distribution) of the offered products components
        offer_parameters[offer_number]['buying_price'] : int/float/numpy rng distribution
            'offers' - buying_price
            buying price per product offered
        offer_parameters[offer_number]['lead_time'] : int/float/numpy rng distribution
            'offers' - lead_time
            time it takes after ordering to receive the products
        offer_parameters[offer_number]['delivered'] : boolean
            'offers' - delivered
            boolean to signal if the offered products would be delivered to the company
        """
        offer_arrival_pattern = self.offer_parameters[offer_number]['offer_arrival_pattern']
        while True:
            offer_arrival = eval(str(self.offer_parameters[offer_number]['offer_arrival']))
            yield self.env.timeout(offer_arrival)

            product_id = self.offer_parameters[offer_number]['product_id']
            valid_duration = eval(str(self.offer_parameters[offer_number]['valid_duration']))
            batch_size = eval(str(self.offer_parameters[offer_number]['batch_size']))
            quality_comp = str(self.offer_parameters[offer_number]['quality_comp'])
            buying_price = eval(str(self.offer_parameters[offer_number]['buying_price']))
            lead_time = eval(str(self.offer_parameters[offer_number]['lead_time']))
            delivered = eval(str(self.offer_parameters[offer_number]['delivered']))

            # create products
            products = []
            for product_num in range(batch_size):
                prod = self.create_product(product_id, quality_comp, avg_quality=-1, buying_price=buying_price)
                products.append(prod)

            # generate the average quality of offer
            avg_quality = 0
            for product in products:
                avg_quality += sum(comp.quality for comp in product.components) / len(product.components)
            avg_quality = avg_quality / len(products)

            # generate offer
            offer = Offer(products, valid_duration=valid_duration, lead_time=lead_time,
                          delivered=delivered, avg_quality=avg_quality)
            self.offers.append(offer)

            # export data
            attributes = ['valid_duration', 'delivered', 'lead_time', 'avg_quality']
            for attr in attributes:
                self.offer_attributes_export_data.append(
                    dict(UUID=offer.UUID, variableID=attr, variable_specification='', value=getattr(offer, attr)))
            for product in offer.BatchesOfCores:
                self.offer_attributes_export_data.append(
                    dict(UUID=offer.UUID, variableID='Product', variable_specification='', value=product.UUID))
            self.offer_export_data.append(
                dict(UUID=offer.UUID, variableID='arrival_time', variable_specification='', value=self.env.now))

            if offer_arrival_pattern == 'once':
                break

    def delete_offer_demand_manager(self):
        """
        deleting all offers and demands, which are not valid anymore
        """
        while True:
            yield self.env.timeout(1)
            for offer in self.offers[:]:
                offer.valid_duration -= 1
                if offer.valid_duration <= 0:
                    self.offers.remove(offer)
                    self.offer_export_data.append(
                        dict(UUID=offer.UUID, variableID='delete_time', variable_specification='', value=self.env.now))
            for demand in self.demands[:]:
                demand.valid_duration -= 1
                if demand.valid_duration <= 0:
                    self.demands.remove(demand)
                    self.demand_export_data.append(
                        dict(UUID=demand.UUID, variableID='delete_time', variable_specification='', value=self.env.now))

    def place_order(self):
        """
        places orders of offered products based on different disposition options
        the products ordered are deleted from the offers list

        Variables from Import File
        --------------------------
        place_order_parameters['price_limit'] : float
            'place_order' - price_limit
            price limit of the offered products where different disposition decisions are made
        place_order_parameters['disposition_below_limit'] : str
            'place_order' - disposition_below_limit
            disposition decision function name which is used for products with prices below or equal to the price limit
        place_order_parameters['disposition_below_limit_threshold_inventory'] : float
            'place_order' - disposition_below_limit_threshold_inventory
            threshold share of maximum stock level (value between 0-1)
        place_order_parameters['disposition_below_limit_threshold_quality'] : str/float
            'place_order' - disposition_below_limit_threshold_quality
            threshold of minimum quality for product to place order (value between 0-1)
        place_order_parameters['disposition_below_limit_threshold_utilization'] : str
            'place_order' - disposition_below_limit_threshold_utilization
            threshold of maximum utilization of human resources to place order (value between 0-1)
        place_order_parameters['disposition_above_limit'] : str
            'place_order' - disposition_above_limit
            disposition decision function name which is used for products with prices above the price limit
        place_order_parameters['disposition_above_limit_threshold_inventory'] : float
            'place_order' - disposition_above_limit_threshold_inventory
            threshold share of maximum stock level (value between 0-1)
        place_order_parameters['disposition_above_limit_threshold_quality'] : str/float
            'place_order' - disposition_above_limit_threshold_quality
            threshold of minimum quality for product to place order (value between 0-1)
        place_order_parameters['disposition_above_limit_threshold_utilization'] : str
            'place_order' - disposition_above_limit_threshold_utilization
            threshold of maximum utilization of human resources to place order (value between 0-1)
        """
        while True:
            yield self.env.timeout(1)
            if len(self.orders) > 5:
                continue
            self.calc_utilization_resource()
            for offer in self.offers[:]:
                product_id = offer.BatchesOfCores[0].id
                product_type = offer.BatchesOfCores[0].type
                batch_size = len(offer.BatchesOfCores)
                avg_quality = offer.avg_quality
                price = offer.BatchesOfCores[0].price
                disposition_threshold = {}
                if price <= self.place_order_parameters['price_limit']:
                    disposition_func_name = self.place_order_parameters['disposition_below_limit']
                    disposition_threshold['inventory'] = (
                        self.place_order_parameters)['disposition_below_limit_threshold_inventory']
                    disposition_threshold['quality'] = (
                        self.place_order_parameters)['disposition_below_limit_threshold_quality']
                    disposition_threshold['utilization'] = (
                        self.place_order_parameters)['disposition_below_limit_threshold_utilization']
                    disposition_min_quantity = self.place_order_parameters['min_quantity_below_limit']
                else:
                    disposition_func_name = self.place_order_parameters['disposition_above_limit']
                    disposition_threshold['inventory'] = (
                        self.place_order_parameters)['disposition_above_limit_threshold_inventory']
                    disposition_threshold['quality'] = (
                        self.place_order_parameters)['disposition_above_limit_threshold_quality']
                    disposition_threshold['utilization'] = (
                        self.place_order_parameters)['disposition_above_limit_threshold_utilization']
                    disposition_min_quantity = self.place_order_parameters['min_quantity_above_limit']

                order_quantity = self.disposition_decision(disposition_func_name, disposition_threshold,
                                                           disposition_min_quantity, product_id,
                                                           batch_size, avg_quality)

                order = Order

                if order_quantity == 0:
                    continue
                elif order_quantity == batch_size:
                    order = Order(offer.BatchesOfCores, lead_time=offer.lead_time, delivered=offer.delivered)
                    self.offers.remove(offer)
                elif order_quantity > 0:
                    order_products = []
                    for product in offer.BatchesOfCores:
                        order_products.append(product)
                        if len(order_products) == order_quantity:
                            break
                    for product in order_products:
                        offer.BatchesOfCores.remove(product)

                    order = Order(order_products, lead_time=offer.lead_time, delivered=offer.delivered)

                self.env.process(self.order_delivery(order))
                self.ordered_quantity[product_type] += order_quantity
                self.orders.append(order)
                self.order_export_data.append({'UUID': order.UUID,
                                               'variableID': 'place_order',
                                               'variable_specification': 'time',
                                               'value': self.env.now})
                for product in order.BatchesOfCores:
                    self.order_export_data.append({'UUID': order.UUID,
                                                   'variableID': 'Product',
                                                   'variable_specification': '',
                                                   'value': product.UUID})

    def order_delivery(self, order: Order):
        """
        after the lead time the order is sent to pickup or unpacking

        Parameters
        ----------
        order : Order
            the order which has to be processed further
        """
        yield self.env.timeout(order.lead_time)
        if not order.delivered:
            self.env.process(self.pickup_goods(order))
        else:
            self.env.process(self.unpacking(order))

    def pickup_goods(self, order: Order):
        """
        human resources with the skill 'pick_up' have to pick up the order then
        human resources with the skill 'unloading' can help unload the order then
        human resources with the skill 'unpacking' have to unpack the order.
        products are put in stocks.

        Parameters
        ----------
        order : Order
            order which has to be picked up and unpacked by workers

        Variables from Import File
        --------------------------
        unloading_pickup_parameters['duration_1'] : numpy rng distribution
            'unloading_after_pickup' - duration
            process based duration for unloading products which consists of all 'duration' parameters
        unloading_pickup_parameters['human_resources'] : int
            'unloading_after_pickup' - human_resources
            amount of human resources needed for the unloading process
        unloading_pickup_parameters['priority'] : int
            'unloading_after_pickup' - priority
            number giving the priority of the process
        pickup_goods_parameters['duration'] : numpy rng distribution
            'pickup_goods' - duration
            process based duration for picking up products
        pickup_goods_parameters['human_resources'] : int
            'pickup_goods' - human_resources
            amount of human resources needed for the pickup process
        pickup_goods_parameters['priority'] : int
            'pickup_goods' - priority
            number giving the priority of the process
        unpacking_parameters['duration'] : numpy rng distribution
            'unpacking' - duration
            process based duration for unpacking products
        unpacking_parameters['human_resources'] : int
            'unpacking' - human_resources
            amount of human resources needed for the unpacking process
        unpacking_parameters['priority'] : int
            'unpacking' - priority
            number giving the priority of the process
        """
        # Pick up
        human_resources_amount_pickup = eval(str(self.pickup_goods_parameters['human_resources']))
        duration_data = {key: value for key, value in self.pickup_goods_parameters.items() if
                         key.startswith('duration')}
        duration_pick_up = 0
        for d in duration_data:
            duration_pick_up += eval(str(duration_data[d]))
        priority_pick_up = eval(str(self.pickup_goods_parameters['priority']))
        granted_requests_pickup = yield self.env.process(self.human_resources.request_skilled_workers
                                                         (skill="pick_up",
                                                          duration=duration_pick_up,
                                                          products=order.BatchesOfCores,
                                                          amount=human_resources_amount_pickup,
                                                          prio=priority_pick_up))
        # Unloading
        if self.unloading_pickup_parameters['process_simulated'] == 'yes':
            human_resources_amount_unloading_extra = max(eval(str(self.unloading_pickup_parameters['human_resources']))
                                                         - human_resources_amount_pickup, 0)
            available_resources = []
            for res in self.human_resources.get_skilled_workers():
                if 'unloading' in res.skills and res.count < res.capacity:
                    available_resources.append(res)
            human_resources_amount_unloading_extra = min(human_resources_amount_unloading_extra,
                                                         len(available_resources))
            human_resources_amount_unloading = human_resources_amount_pickup + human_resources_amount_unloading_extra

            duration_data = {key: value for key, value in self.unloading_pickup_parameters.items() if
                             key.startswith('duration')}
            duration_unloading = 0
            for d in duration_data:
                duration_unloading += eval(str(duration_data[d]))

            if human_resources_amount_unloading > human_resources_amount_pickup:
                priority_unloading = eval(str(self.unloading_pickup_parameters['priority']))
                granted_requests_unload = yield self.env.process(self.human_resources.request_skilled_workers
                                                                 (skill="unloading",
                                                                  duration=duration_unloading,
                                                                  products=order.BatchesOfCores,
                                                                  amount=human_resources_amount_unloading_extra,
                                                                  prio=priority_unloading))
                self.human_resources.release_skilled_workers(granted_requests_unload, skill="unloading",
                                                             products=order.BatchesOfCores)
            else:
                yield self.env.timeout(duration_unloading)

        self.human_resources.release_skilled_workers(granted_requests_pickup, skill="pick_up",
                                                     products=order.BatchesOfCores)

        # Unpack
        human_resources_amount_unpacking = eval(str(self.unpacking_parameters['human_resources']))
        duration_data = {key: value for key, value in self.unpacking_parameters.items() if
                         key.startswith('duration')}
        duration_unpacking = 0
        for d in duration_data:
            duration_unpacking += eval(str(duration_data[d]))
        priority_unpacking = eval(str(self.unpacking_parameters['priority']))
        granted_requests_unpack = yield self.env.process(self.human_resources.request_skilled_workers
                                                         (skill="unpacking",
                                                          duration=duration_unpacking,
                                                          products=order.BatchesOfCores,
                                                          amount=human_resources_amount_unpacking,
                                                          prio=priority_unpacking))
        self.human_resources.release_skilled_workers(granted_requests_unpack, skill="unpacking",
                                                     products=order.BatchesOfCores)

        if order in self.orders:
            self.orders.remove(order)

        for p in order.BatchesOfCores:
            self.stocks.put(p)
            self.ordered_quantity[p.type] -= 1

    def unpacking(self, order: Order):
        """
        human resources with the skill 'unloading' and 'unpacking' can carry out this process to unload and unpack
        the order.
        products are put in stocks.

        Parameters
        ----------
        order : Order
            order which has to be unpacked by workers

        Variables from Import File
        --------------------------
        unloading_delivered_parameters['duration_1'] : numpy rng distribution
            'unloading_after_delivery' - duration
            process based duration for unloading products which consists of all 'duration' parameters
        unloading_delivered_parameters['human_resources'] : int
            'unloading_after_delivery' - human_resources
            amount of human resources needed for the unloading process
        unloading_delivered_parameters['priority'] : int
            'unloading_after_delivery' - priority
            number giving the priority of the process
        unpacking_parameters['duration_1'] : numpy rng distribution
            'unpacking' - duration
            process based duration for unpacking products which consists of all 'duration' parameters
        unpacking_parameters['human_resources'] : int
            'unpacking' - human_resources
            amount of human resources needed for the unpacking process
        unpacking_parameters['priority'] : int
            'unpacking' - priority
            number giving the priority of the process
        """
        yield self.env.timeout(order.lead_time)
        # Unloading
        if self.unloading_delivered_parameters['process_simulated'] == 'yes':
            human_resources_amount_unloading = eval(str(self.unloading_delivered_parameters['human_resources']))
            duration_data = {key: value for key, value in self.unloading_delivered_parameters.items() if
                             key.startswith('duration')}
            duration_unloading = 0
            for d in duration_data:
                duration_unloading += eval(str(duration_data[d]))
            priority_unloading = eval(str(self.unloading_delivered_parameters['priority']))
            granted_requests_unload = yield self.env.process(self.human_resources.request_skilled_workers
                                                             (skill="unloading",
                                                              duration=duration_unloading,
                                                              products=order.BatchesOfCores,
                                                              amount=human_resources_amount_unloading,
                                                              prio=priority_unloading))
            self.human_resources.release_skilled_workers(granted_requests_unload, skill="unloading",
                                                         products=order.BatchesOfCores)

        # Unpacking
        human_resources_amount_unpacking = eval(str(self.unpacking_parameters['human_resources']))
        duration_data = {key: value for key, value in self.unpacking_parameters.items() if
                         key.startswith('duration')}
        duration_unpacking = 0
        for d in duration_data:
            duration_unpacking += eval(str(duration_data[d]))
        priority = eval(str(self.unpacking_parameters['priority']))
        granted_requests_unpack = yield self.env.process(self.human_resources.request_skilled_workers
                                                         (duration=duration_unpacking,
                                                          skill="unpacking",
                                                          products=order.BatchesOfCores,
                                                          amount=human_resources_amount_unpacking,
                                                          prio=priority))
        self.human_resources.release_skilled_workers(granted_requests_unpack, skill="unpacking",
                                                     products=order.BatchesOfCores)

        if order in self.orders:
            self.orders.remove(order)
        for p in order.BatchesOfCores:
            self.stocks.put(p)
            self.ordered_quantity[p.type] -= 1

    @property
    def grading(self):
        """
        grading of products resulting in a quality for the products between 0 and 1 computed with the quality
        aggregation function
        products with a quality of -1 are graded

        Variables from Import File
        --------------------------
        grading_parameters['max_grading_spots'] : int
            'grading' - max_grading_spots
            maximum number of spots where products can be graded simultaneously
        """
        max_grading_spots = self.grading_parameters['max_grading_spots']
        while True:
            if len(self.grading_stock.items) < max_grading_spots:
                product_grading = yield self.stocks.get(lambda p: p.quality == -1)

                self.grading_stock.put(product_grading)
                self.work_in_progress_stocks.put(product_grading)

                self.quality_aggregation_func(product_grading)

                self.env.process(self.grading_process(product_grading))
            else:
                yield self.env.timeout(0.1)

    def grading_process(self, product_grading: Product):
        """
        grading process carried out by human resources with the skill 'grading' for a process based duration

        Parameters
        ----------
        product_grading : Product
            List of products which are chosen to be graded

        Variables from Import File
        --------------------------
        grading_parameters['priority'] : int
            'grading' - priority
            process based priority relevant for the queue of processes for human_resources
        grading_parameters['human_resources_active_start'] : int - numpy rng distribution
            'grading' - human_resources_active_start
            amount of human resources needed for the start of the grading process
        grading_parameters['duration_active_start'] : numpy rng distribution
            'grading' - duration_active_start
            durations for the start of the grading process
        grading_parameters['duration_passive'] : numpy rng distribution
            'grading' - duration_passive
            duration for passive time in the grading process where no human resources are needed
        grading_parameters['human_resources_active_end'] : int - numpy rng distribution
            'grading' - human_resources_active_end
            amount of human resources needed for the end of the grading process after the passive duration
        grading_parameters['duration_active_end'] : numpy rng distribution
            'grading' - duration_active_end
            durations for the end of the grading process
        """
        priority = eval(str(self.grading_parameters['priority']))

        # first active time, where HR are needed
        human_resources_amount = eval(str(self.grading_parameters['human_resources_active_start']))
        random_1 = self.rng.random()
        random_2 = self.rng.random()
        duration_data = {key: value for key, value in self.grading_parameters.items() if
                         key.startswith('duration_active_start')}
        duration_grading = 0
        for d in duration_data:
            duration_grading += eval(str(duration_data[d]))
        if human_resources_amount > 0:
            granted_requests_grading = yield self.env.process(self.human_resources.request_skilled_workers
                                                              (duration=duration_grading,
                                                               skill="grading",
                                                               products=[product_grading],
                                                               amount=human_resources_amount,
                                                               prio=priority))
            self.human_resources.release_skilled_workers(granted_requests_grading, skill="grading",
                                                         products=[product_grading])
        # passive time, no HR are needed but the grading spot is occupied.
        duration_grading_passive = eval(str(self.grading_parameters['duration_passive']))
        yield self.env.timeout(duration_grading_passive)

        # second active time, where HR are needed
        human_resources_amount = eval(str(self.grading_parameters['human_resources_active_end']))
        duration_data = {key: value for key, value in self.grading_parameters.items() if
                         key.startswith('duration_active_end')}
        duration_grading = 0
        for d in duration_data:
            duration_grading += eval(str(duration_data[d]))
        if human_resources_amount > 0:
            granted_requests_grading = yield self.env.process(self.human_resources.request_skilled_workers
                                                              (duration=duration_grading,
                                                               skill="grading",
                                                               products=[product_grading],
                                                               amount=human_resources_amount,
                                                               prio=priority))

            self.human_resources.release_skilled_workers(granted_requests=granted_requests_grading,
                                                         skill="grading",
                                                         products=[product_grading])

        self.stocks.put(product_grading)
        self.work_in_progress_stocks.items.remove(product_grading)
        self.grading_stock.items.remove(product_grading)

    def disposition_after_grading(self):
        """
        checking if a products quality exceeds the sellable quality limit of the product type after grading
        products which fulfill this criterion are put into the stocks_sellable

        Variables from Import File
        --------------------------
        core_quality_sellable_limits[p.type] : float
            'producttype' - quality_sellable_limit
            quality limit for a product to be sellable
        """
        while True:
            sellable_product = yield self.stocks.get(lambda p: p.quality >= self.core_quality_sellable_limits[p.type])
            if sellable_product:
                self.stocks_sellable.put(sellable_product)
                sellable_product.status_time.append((self.env.now, "sellable_after_grading", "start"))
                sellable_product.status_time.append((self.env.now, "sellable_after_grading", "end"))
            else:
                yield self.env.timeout(0.1)

    def quality_aggregation_func(self, core: Product):
        """
        determining the quality of a product based on the weighted average of the components qualities

        Parameters
        ----------
        core : Product
            core for which the quality is determined

        Variables from Import File
        --------------------------
        child.comp_quality_weight : float
            'producttype_parts' - comp_quality_weight
            weight per component in a product for calculating the weighted average
        """
        parent_node = find(self.producttype_tree, lambda node: node.name == core.type)
        sum_weights = 0
        sum_quality_weights = 0

        for component in core.components:
            for child in parent_node.children:
                if child.name == component.part_type:
                    sum_weights += child.comp_quality_weight
                    sum_quality_weights += np.multiply(component.quality, child.comp_quality_weight)
                    break
        core.quality = np.divide(sum_quality_weights, sum_weights)

    def generate_demand_manager(self, demand_number):
        """
        generates demand for products to be sold with the information corresponding to the demand_number
        if the demand is met by products in the sellable stock the products are sold and deleted from the stock

        Parameters
        ----------
        demand_number : str
            Import File: 'demand' - 'demand'
            identifier for the information of demands in the Import File

        Variables from Import File
        --------------------------
        demand_parameters[demand_number]['demand_arrival_pattern'] : str
            'demand' - demand_arrival_pattern
            demand arriving either 'once' or 'cyclical'
        demand_parameters[demand_number]['demand_arrival_distribution'] : int/float/numpy rng distribution
            'demand' - demand_arrival_distribution
            time period when the demand arrives
        demand_parameters[demand_number]['product_type'] : str
            'demand' - product_type
            type of the demanded products
        demand_parameters[demand_number]['product_id'] : str
            'demand' - product_id
            id of the demanded products or 'random' if just a type and no specific id is demanded
        demand_parameters[demand_number]['valid_duration'] : int/float/numpy rng distribution
            'demand' - valid_duration
            time the demand is valid after arriving
        demand_parameters[demand_number]['batch_size'] : int/float/numpy rng distribution
            'demand' - batch_size
            number of products demanded
        """
        demand_arrival_pattern = self.demand_parameters[demand_number]['demand_arrival_pattern']
        core_id = self.demand_parameters[demand_number]['product_id']
        product_type = self.demand_parameters[demand_number]['product_type']

        while True:
            batch_size_demand = eval(str(self.demand_parameters[demand_number]['batch_size']))
            arrival_time = eval(str(self.demand_parameters[demand_number]['demand_arrival_distribution']))
            valid_duration_demand = eval(str(self.demand_parameters[demand_number]['valid_duration']))

            if core_id == "random":
                type_products = [product for product in self.products_list if product.type == product_type]
                core_id_used = type_products[round(self.rng.uniform(low=0, high=len(type_products)-1))].id
            else:
                core_id_used = core_id

            yield self.env.timeout(arrival_time)

            demand = Demand(batch_size=batch_size_demand,
                            product_id=core_id_used, valid_duration=valid_duration_demand)

            # export data
            attributes = ['product_id', 'batch_size', 'valid_duration']
            for attr in attributes:
                self.demand_attributes_export_data.append(
                    dict(UUID=demand.UUID, variableID=attr, variable_specification='', value=getattr(demand, attr)))
            self.demand_export_data.append(
                dict(UUID=demand.UUID, variableID='arrival_time', variable_specification='', value=self.env.now))

            self.demands.append(demand)

            if demand_arrival_pattern == 'once':
                break

    def meet_demand(self):
        """
        checking if there are sellable products in stock to meet demands
        """
        while True:
            yield self.env.timeout(0.1)
            if self.stocks_sellable.items:
                for demand in self.demands[:]:
                    if sum(1 for p in self.stocks_sellable.items if
                           p.id == demand.product_id) - demand.batch_size >= 0:
                        # Change inventory level
                        cores_to_sell = []
                        for p in range(demand.batch_size):
                            core_to_sell = yield self.stocks_sellable.get(lambda product:
                                                                          product.id == demand.product_id)
                            self.work_in_progress_stocks.put(core_to_sell)
                            cores_to_sell.append(core_to_sell)
                            self.demand_export_data.append(
                                dict(UUID=demand.UUID, variableID='Product', variable_specification='',
                                     value=core_to_sell.UUID))
                        self.demands.remove(demand)
                        self.demand_export_data.append(
                            dict(UUID=demand.UUID, variableID='demand_met',
                                 variable_specification='', value=self.env.now))
                        self.env.process(self.shipping(cores_to_sell))

    def shipping(self, products_to_ship):
        """
        shipping sold products, amount of shipped products depend on the demand
        process is carried out by human resources with the skill 'shipping' for a process based duration

        Parameters
        ----------
        products_to_ship : List[Product]
            products to ship

        Variables from Import File
        --------------------------
        shipping_parameters['duration'] : int/float/numpy rng distribution
            'shipping' - duration
            process based duration for shipping
        shipping_parameters['human_resources'] : int
            'shipping' - human_resources
            amount of human resources needed for the shipping process
        """
        human_resources = eval(str(self.shipping_parameters['human_resources']))
        duration_data = {key: value for key, value in self.shipping_parameters.items() if
                         key.startswith('duration')}
        duration_shipping = 0
        for d in duration_data:
            duration_shipping += eval(str(duration_data[d]))
        priority = eval(str(self.shipping_parameters['priority']))
        granted_requests_shipping = yield self.env.process(self.human_resources.request_skilled_workers
                                                           (duration=duration_shipping,
                                                            skill="shipping",
                                                            products=products_to_ship,
                                                            amount=human_resources,
                                                            prio=priority))
        self.human_resources.release_skilled_workers(granted_requests_shipping, skill="shipping",
                                                     products=products_to_ship)
        for product in products_to_ship:
            self.work_in_progress_stocks.items.remove(product)

    @property
    def refurbish(self):
        """
        identifying products which quality exceeds the core_quality_refurbish_limits and is lower than the
        core_quality_sellable_limits and for which all needed components are on stock for the refurbish process

        Variables from Import File
        --------------------------
        core_quality_refurbish_limits[product.type] : float
            'producttype' - quality_refurbish_limit
            quality limit for a product to get refurbished
        core_quality_sellable_limits[product.type] : float
            'producttype' - quality_sellable_limit
            quality limit for a product to be sellable
        """
        max_refurbish_spots = self.refurbish_parameters['max_refurbish_spots']
        while True:
            product_refurbish = None
            if (any([(self.core_quality_refurbish_limits[product.type] <= product.quality <
                     self.core_quality_sellable_limits[product.type] and not
                     self.check_components(product)) for product in self.stocks.items])
                    and len(self.refurbish_stock.items) < max_refurbish_spots):
                product_refurbish = yield self.stocks.get(lambda p: self.core_quality_refurbish_limits[p.type] <=
                                                                    p.quality <
                                                                    self.core_quality_sellable_limits[p.type]
                                                                    and not self.check_components(p))
                self.work_in_progress_stocks.put(product_refurbish)
                self.refurbish_stock.put(product_refurbish)

                self.env.process(self.refurbish_process(product_refurbish))
            else:
                yield self.env.timeout(0.1)

    def refurbish_process(self, product_refurbish: Product):
        """
        process carried out by human resources with the skill 'refurbish' for a process and component based
        duration
        all components with lower quality than the threshold are exchanged with components in inventory_components to
        enhance the quality of the product

        Parameters
        ----------
        product_refurbish : Product
            one product which was selected to be refurbished

        Variables from Import File
        --------------------------
        refurbish_parameters['priority_start'] : int
            'refurbish' - priority_start
            process based priority for the first active process relevant for the queue of processes for human_resources
        refurbish_parameters['duration_active_start'] : int/float/numpy rng distribution
            'refurbish' - duration_active_start
            process based duration for the first active time in refurbishing
        child.repair_duration : int/float/numpy rng distribution
            'producttype_parts' - repair_duration
            duration to refurbish a specific component
        refurbish_parameters['human_resources_active_start'] : int
            'refurbish' - human_resources
            amount of human resources needed for the first part of the refurbish process
        child.min_quality : float
            'producttype_parts' - min_quality
            quality limit for components
        """
        priority = eval(str(self.refurbish_parameters['priority_start']))

        # first active time, where HR are needed
        human_resources_amount = eval(str(self.refurbish_parameters['human_resources_active_start']))
        random_1 = self.rng.random()
        random_2 = self.rng.random()
        duration_refurbish = 0
        duration_data = {key: value for key, value in self.refurbish_parameters.items() if
                         key.startswith('duration_active_start')}
        duration_refurbish_process = 0
        for d in duration_data:
            duration_refurbish_process += eval(str(duration_data[d]))
        duration_refurbish += duration_refurbish_process
        parent_node = find(self.producttype_tree, lambda node: node.name == product_refurbish.type)

        for comp in range(len(product_refurbish.components)):
            for child in parent_node.children:
                if (product_refurbish.components[comp].part_type == child.name and
                        product_refurbish.components[comp].quality < child.min_quality):
                    new_comp = self.find_replacement_component(product_refurbish.components[comp].part_type,
                                                               product_refurbish)
                    if new_comp is None:
                        product_refurbish.components[comp].quality = 1
                    else:
                        yield self.inventory_components.get(lambda p: p.id == new_comp.id)
                        product_refurbish.components[comp] = new_comp

                    repair_duration = eval(str(child.repair_duration))
                    duration_refurbish += repair_duration
                    break

        if duration_refurbish > 0 and human_resources_amount > 0:
            granted_requests_refurbish = yield self.env.process(self.human_resources.request_skilled_workers
                                                                (duration=duration_refurbish,
                                                                 skill="refurbish_start",
                                                                 products=[product_refurbish],
                                                                 amount=human_resources_amount,
                                                                 prio=priority))
            self.human_resources.release_skilled_workers(granted_requests_refurbish, skill="refurbish_start",
                                                         products=[product_refurbish])
        self.env.process(self.refurbish_end(product_refurbish))

    def refurbish_end(self, product_refurbish: Product):
        """
        first a passive time in the refurbish process with no use of human resources, followed by a second active time
        for the refurbish process where human resources are necessary

        Parameters
        ----------
        product_refurbish : Product
            in the refurbish process

        Variables from Import File
        --------------------------
        refurbish_parameters['duration_passive'] : int/float/numpy rng distribution
            'refurbish' - duration_passive
            process based duration for the passive time in refurbishing where no HR are needed
        refurbish_parameters['priority_end'] : int
            'refurbish' - priority_end
            process based priority for the second active process relevant for the queue of processes for human_resources
        refurbish_parameters['duration_active_end'] : int/float/numpy rng distribution
            'refurbish' - duration_active_end
            process based duration for the second active time in refurbishing
        refurbish_parameters['human_resources_active_end'] : int
            'refurbish' - human_resources
            amount of human resources needed for the second part of the refurbish process
        """
        # passive time, no HR are needed but the grading spot is occupied.
        priority = eval(str(self.refurbish_parameters['priority_end']))

        duration_refurbish_passive = 0
        duration_data = {key: value for key, value in self.refurbish_parameters.items() if
                         key.startswith('duration_passive')}
        for d in duration_data:
            duration_refurbish_passive += eval(str(duration_data[d]))
        yield self.env.timeout(duration_refurbish_passive)

        # second active time, where HR are needed
        human_resources_amount = eval(str(self.refurbish_parameters['human_resources_active_end']))
        duration_refurbish = 0
        duration_data = {key: value for key, value in self.refurbish_parameters.items() if
                         key.startswith('duration_active_end')}
        duration_refurbish_process = 0
        for d in duration_data:
            duration_refurbish_process += eval(str(duration_data[d]))
        duration_refurbish += duration_refurbish_process

        if duration_refurbish > 0 and human_resources_amount > 0:
            granted_requests_refurbish = yield self.env.process(self.human_resources.request_skilled_workers
                                                                (duration=duration_refurbish,
                                                                 skill="refurbish_end",
                                                                 products=[product_refurbish],
                                                                 amount=human_resources_amount,
                                                                 prio=priority))
            self.human_resources.release_skilled_workers(granted_requests_refurbish, skill="refurbish_end",
                                                         products=[product_refurbish])

        self.quality_aggregation_func(product_refurbish)
        self.work_in_progress_stocks.items.remove(product_refurbish)
        self.refurbish_stock.items.remove(product_refurbish)
        self.stocks_sellable.put(product_refurbish)

    def check_components(self, product_refurbish: Product):
        """
        returns True if all components which have to be replaced for the product_refurbish are on stock

        Parameters
        ----------
        product_refurbish : Product
            the product for which it is checked if enough components are on stock

        Returns
        -------
        boolean
            indicating if enough components are on stock for the product_refurbish
        """
        parent_node = find(self.producttype_tree, lambda node: node.name == product_refurbish.type)

        for comp in product_refurbish.components:
            for child in parent_node.children:
                if child.name == comp.part_type and comp.quality < child.min_quality:
                    new_comp = self.find_replacement_component(comp.part_type, product_refurbish)
                    if new_comp is None:
                        return True  # not enough components
        return False

    def find_replacement_component(self, component_type, product_refurbish):
        """
        returns the best component to fit for the product to refurbish which is on stock based on the priorities

        Parameters
        ----------
        component_type : str
            type of component which has to be replaced

        product_refurbish : Product
            core which needs to be refurbished and components need to be replaced

        Returns
        -------
        best_comp : Component
            best fit component for the component_type and product_refurbish
        """
        comp_list = []
        comp_priorities = {}
        for comp in self.products_components_dict[product_refurbish.id]:
            comp_name = comp[0]
            comp_prio = comp[1]
            comp_type = self.COMPONENTS.loc[comp_name, 'Type']
            if comp_type == component_type:
                comp_list.append(comp_name)
                comp_priorities[comp_name] = comp_prio

        best_comp = None
        min_prio = float('inf')

        for inv_comp in self.inventory_components.items:
            if inv_comp.id in comp_list:
                if comp_priorities[inv_comp.id] < min_prio:
                    min_prio = comp_priorities[inv_comp.id]
                    best_comp = inv_comp
                    if min_prio == 1:
                        break

        return best_comp

    def calc_utilization_resource(self):
        """
        calculates the utilization of all human resources and the mean of the top utilization
        """
        skilled_workers = self.human_resources.get_skilled_workers()

        utilization = []
        current_time = self.env.now

        for worker in skilled_workers:
            data = pd.DataFrame(worker.utilization_data, columns=["time", "len", "status", "skill", "duration"])
            if not data.empty:
                start_times = data.loc[(data["status"] == "start_using") &
                                       (data["time"].astype(float) >= current_time - 40), "time"].astype(float).values
                end_times = data.loc[(data["status"] == "finished_using") &
                                     (data["time"].astype(float) >= current_time - 40), "time"].astype(float).values
                if len(end_times) > 0:
                    if len(start_times) == 0:
                        start_times = np.append(start_times, current_time-40)
                    else:
                        if start_times[0] >= end_times[0]:
                            start_times = np.insert(start_times, 0, current_time-40)
                        if end_times[-1] <= start_times[-1]:
                            end_times = np.append(end_times, current_time)
                elif len(start_times) > 0:
                    end_times = np.append(end_times, current_time)

                if len(end_times) != len(start_times):
                    print('different len of times')
                    print(end_times)
                    print(start_times)

                used_time = np.sum(end_times - start_times[:len(end_times)])

                if worker.capacity > 0:
                    total_time = min(self.env.now, 40)
                    util = used_time / (total_time * worker.capacity)
                    utilization.append(util)
            else:
                utilization.append(0)

        n_people = int(np.ceil(len(skilled_workers)/3))
        top_n_utilization = sorted(utilization, reverse=True)[:n_people]

        self.mean_utilization = np.mean(top_n_utilization)

    def replenish_component_manager(self):
        """
        checking if new components should be ordered according to the replenishment policy

        Variables from Import File
        --------------------------
        replenish_components_parameters["period_review"] : int
            'replenish_components' - 'period_review'
            time period after the inventories are checked and if necessary orders are placed
        replenish_components_parameters["replenishment_policy"] : str
            'replenish_components' - 'replenishment_policy'
            policy which defines the order quantity for the components
        replenish_components_parameters["leadtime_order"] : int
            'replenish_components' - 'leadtime_order'
            time the components need to be delivered
        COMPONENTS["max stock capacity"] : int
            'components' - 'max stock capacity'
            the maximum stock capacity per component
        """
        while True:
            yield self.env.timeout(self.replenish_components_parameters["period_review"])

            inventories_t = Counter([inv_comp.id for inv_comp in self.inventory_components.items])
            o_inventories_t = Counter([inv_comp.id for inv_comp in self.ordered_components.items])
            combined_inventory = inventories_t + o_inventories_t
            inventories = []
            for i in range(self.COMPONENTS.shape[0]):
                if self.COMPONENTS.index[i] in combined_inventory.keys():
                    inventories.append(combined_inventory.get(self.COMPONENTS.index[i]))
                else:
                    inventories.append(0)

            replenish_quantities = (
                self.determine_order_quantity(self.replenish_components_parameters["replenishment_policy"],
                                              list(self.COMPONENTS["max stock capacity"]),
                                              inventories))
            if sum(replenish_quantities) > 0:
                self.env.process(self.add_to_stock(replenish_quantities,
                                                   list(self.COMPONENTS["max stock capacity"]),
                                                   self.replenish_components_parameters["leadtime_order"]))

            yield self.env.timeout(0.1)

    def add_to_stock(self, qty_put_to_storage, max_stock_sizes, leadtime_order):
        """
        ordered components are put on stock after the lead time considering the maximum stock capacities per component

        Parameters
        ----------
        qty_put_to_storage : List[int]
            List of quantities of components which are ordered to add to stock
        max_stock_sizes : List[int]
            List of maximum stock sizes per component
        leadtime_order : int
            lead time for the order of components
        """
        c_inventories_t = Counter([inv_comp.id for inv_comp in self.inventory_components.items])
        o_inventories_t = Counter([inv_comp.id for inv_comp in self.ordered_components.items])
        combined_inventory = c_inventories_t + o_inventories_t
        c_inventories = []
        for i in range(self.COMPONENTS.shape[0]):
            if self.COMPONENTS.index[i] in combined_inventory.keys():
                c_inventories.append(combined_inventory.get(self.COMPONENTS.index[i]))
            else:
                c_inventories.append(0)

        current_inventory_list = c_inventories

        new_inventory_sizes = [i + j for i, j in zip(current_inventory_list, qty_put_to_storage)]
        new_components = []
        for i in range(len(new_inventory_sizes)):
            if new_inventory_sizes[i] <= max_stock_sizes[i]:
                for j in range(qty_put_to_storage[i]):
                    component = Component(part_type=self.COMPONENTS.loc[self.COMPONENTS.index[i]]["Type"],
                                          id=self.COMPONENTS.index[i],
                                          quality=1)
                    self.ordered_components.put(component)
                    new_components.append(component)
        yield self.env.timeout(leadtime_order)
        for component in new_components:
            self.inventory_components.put(component)
            yield self.ordered_components.get(lambda c: c == component)
        yield self.env.timeout(0.1)

    def max_order_periodic_review(self, max_inventory_sizes: List[int], current_inventories: List[int]):
        """
        ordering components up to the max_inventory_sizes per component

        Parameters
        ----------
        max_inventory_sizes : List[int]
            List of maximum stock sizes per component
        current_inventories : List[int]
            List of current inventories per component

        Returns
        -------
        order_quantities : List[int]
            returns the calculated order quantities of the components
        """
        order_quantities = [max(i - j, 0) for i, j in zip(max_inventory_sizes, current_inventories)]
        return order_quantities

    def max_order_continuous_review(self, max_inventory_sizes: List[int], arg3=4):
        print('This inventory order policy is currently not implemented')
        # return max_inventory_size + arg3

    def fixed_order_periodic_review(self, current_inventories: List[int]):
        print('This inventory order policy is currently not implemented')
        # return current_inventory

    def fixed_order_continuous_review(self, arg4=3):
        print('This inventory order policy is currently not implemented')
        # return arg4

    def determine_order_quantity(self, function_name, max_inventory_sizes: List[int], current_inventories: List[int],
                                 arg3: int = 5, arg4: int = 6):
        """
        calculates the order quantity for the components. depending on the function_name parameter different inventory
        order policies are implemented.

        Parameters
        ----------
        function_name : str
            the name of the function/replenishment policy which is used to calculate the order quantity
        max_inventory_sizes : List[int]
            List of maximum stock sizes per component
        current_inventories : List[int]
            List of current inventories per component
        arg3 : int, optional
            default is 5
        arg4 : int, optional
            default is 6

        Returns
        -------
        method
            name of method based on the function_name
        """
        return {
            'max_order_periodic_review': lambda: self.max_order_periodic_review(max_inventory_sizes,
                                                                                current_inventories),
            'max_order_continuous_review': lambda: self.max_order_continuous_review(max_inventory_sizes, arg3),
            'fixed_order_periodic_review': lambda: self.fixed_order_periodic_review(current_inventories),
            'fixed_order_continuous_review': lambda: self.fixed_order_continuous_review(arg4),
        }[function_name]()

    def utilization_based_disposition(self, threshold_utilization, threshold_inventory, core_id: str,
                                      batch_size: int, min_quantity: int):
        """
        returns the order_quantity of the products if the utilization of the human resources is below the threshold and
        inventory does not exceed maximum stock capacity times inventory threshold

        Parameters
        ----------
        threshold_utilization : float/str
            threshold for utilization of human resources
        threshold_inventory : float/str
            max threshold for inventory
        core_id : str
            id of the products of the possible order
        batch_size : int
            quantity of products which are possible to order
        min_quantity : int
            minimum quantity of products ordered

        Returns
        -------
        order_quantity : int
            quantity which is ordered
        """
        core_type = next((product.type for product in self.products_list if product.id == core_id), None)
        inventory = (len([i for i in self.stocks.items if i.type == core_type]) +
                     len([i for i in self.stocks_sellable.items if i.type == core_type]) +
                     len([i for i in self.work_in_progress_stocks.items if i.type == core_type]) +
                     self.ordered_quantity[core_type])

        order_quantity = 0
        if inventory < (self.core_stock_capacities[core_type] * threshold_inventory):
            if (self.mean_utilization <= eval(str(threshold_utilization)) and
                    (inventory + batch_size) <= (self.core_stock_capacities[core_type] * threshold_inventory)):
                order_quantity = batch_size
            elif (self.mean_utilization <= eval(str(threshold_utilization)) and
                  inventory <= (self.core_stock_capacities[core_type] * threshold_inventory)):
                order_quantity = (self.core_stock_capacities[core_type] * threshold_inventory) - inventory
        else:
            order_quantity = 0
        if order_quantity < min_quantity:
            order_quantity = 0
        return order_quantity

    def quality_based_disposition(self, threshold_quality, threshold_inventory,
                                  core_id: str, quality: float, batch_size: int, min_quantity: int):
        """
        returns the order_quantity of the products if the offered quality exceeds the quality threshold and
        inventory does not exceed maximum stock capacity times inventory threshold

        Parameters
        ----------
        threshold_quality : float/str
            threshold for quality - products with quality below the threshold are not ordered
        threshold_inventory : float/str
            max threshold for inventory
        core_id : str
            id of the products of the possible order
        quality : float
            quality of the batch of products for the possible order
        batch_size : int
            quantity of products which are possible to order
        min_quantity : int
            minimum quantity of products ordered

        Returns
        -------
        order_quantity : int
            quantity which is ordered
        """
        core_type = next((product.type for product in self.products_list if product.id == core_id), None)
        inventory = (len([i for i in self.stocks.items if i.type == core_type]) +
                     len([i for i in self.stocks_sellable.items if i.type == core_type]) +
                     len([i for i in self.work_in_progress_stocks.items if i.type == core_type]) +
                     self.ordered_quantity[core_type])
        if (quality >= eval(str(threshold_quality)) and
                (inventory + batch_size) <= (self.core_stock_capacities[core_type] * threshold_inventory)):
            order_quantity = batch_size
        elif (quality >= eval(str(threshold_quality)) and
              inventory <= (self.core_stock_capacities[core_type] * threshold_inventory)):
            order_quantity = (self.core_stock_capacities[core_type] * threshold_inventory) - inventory
        else:
            order_quantity = 0
        if order_quantity < min_quantity:
            order_quantity = 0
        return order_quantity

    def inventory_based_disposition(self, threshold: float, core_id: str, batch_size: int, min_quantity: int):
        """
        returns the order_quantity of the products if the inventory does not exceed a certain amount of
        the max stock space

        Parameters
        ----------
        threshold : float
            max threshold for inventory
        core_id : str
            id of the products of the possible order
        batch_size : int
            quantity of products which are possible to order
        min_quantity : int
            minimum quantity of products ordered

        Returns
        -------
        order_quantity : int
            quantity which is ordered
        """
        core_type = next((product.type for product in self.products_list if product.id == core_id), None)
        inventory = (len([i for i in self.stocks.items if i.type == core_type]) +
                     len([i for i in self.stocks_sellable.items if i.type == core_type]) +
                     len([i for i in self.work_in_progress_stocks.items if i.type == core_type]) +
                     self.ordered_quantity[core_type])
        if inventory + batch_size <= (self.core_stock_capacities[core_type] * threshold):
            order_quantity = batch_size
        elif inventory <= (self.core_stock_capacities[core_type] * threshold):
            order_quantity = (self.core_stock_capacities[core_type] * threshold) - inventory
        else:
            order_quantity = 0
        if order_quantity < min_quantity:
            order_quantity = 0
        return order_quantity

    def disposition_decision(self, function_name, threshold: Dict, min_quantity: int, core_id: str,
                             batch_size: int = 1, quality: float = None):
        """
        determines if an order for products should be placed. based on the function_name parameter individual
        disposition decisions can be implemented

        Parameters
        ----------
        function_name : str
            the name of the function/disposition decision which is used to calculate the order quantity
        threshold : dict
            thresholds relevant for the disposition decision
        min_quantity : int
            minimum quantity to order
        core_id : str
            id of the products of the possible order
        batch_size : int, optional
            quantity of products which are possible to order, default is 1
        quality : float, optional
            quality of the batch of products for the possible order, default is None

        Returns
        -------
        method
            name of method based on the function_name
        """
        if np.isnan(threshold['inventory']):
            threshold['inventory'] = 1
        return {
            'inventory_based': lambda: self.inventory_based_disposition(threshold['inventory'], core_id,
                                                                        batch_size, min_quantity),
            'quality_based': lambda: self.quality_based_disposition(threshold['quality'], threshold['inventory'],
                                                                    core_id, quality, batch_size, min_quantity),
            'utilization_based': lambda: self.utilization_based_disposition(threshold['utilization'],
                                                                            threshold['inventory'],
                                                                            core_id, batch_size, min_quantity),
        }[function_name]()

    def scavenging_component_manager(self):
        """
        identifying products which quality is lower than the core_quality_refurbish_limits and exceeds the
        core_quality_scavenging_limits for scavenging

        Variables from Import File
        --------------------------
        core_quality_scavenging_limits[core.type] : float
            'producttype' - quality_scavenging_limit
            quality limit for a product to get scavenged
        core_quality_refurbish_limits[core.type] : float
            'producttype' - quality_scavenging_limit
            quality limit for a product to get refurbished
        """
        while True:
            if any([(self.core_quality_refurbish_limits[core.type] >
                     core.quality >=
                     self.core_quality_scavenging_limits[core.type]) for core in self.stocks.items]):
                core_to_scavenge = yield self.stocks.get(lambda p: self.core_quality_refurbish_limits[p.type] >
                                                                   p.quality >=
                                                                   self.core_quality_scavenging_limits[p.type])

                self.env.process(self.scavenging_process(core_to_scavenge))
            else:
                yield self.env.timeout(0.1)

    def scavenging_process(self, core_to_scavenge: Product):
        """
        scavenging process carried out by human resources with the skill 'scavenging' for a process based duration
        components which exceed their threshold quality are stored in the inventory_components if there is still stock
        capacity left for the specific component, otherwise the components are recycled

        Parameters
        ----------
        core_to_scavenge : Product
            one product which was selected to be scavenged for spare parts

        Variables from Import File
        --------------------------
        COMPONENTS['max stock capacity'] : int
            'components' - max stock capacity
            maximum stock capacity per component type
        scavenging_parameters['priority'] : int
            'scavenging' - priority
            process based priority relevant for the queue of processes for human_resources
        scavenging_parameters['duration'] : int/float/numpy rng distribution
            'scavenging' - duration
            process based duration for scavenging
        child.scavenge_duration : int/float/numpy rng distribution
            'producttype_parts' - scavenge_duration
            duration to scavenge a specific component
        scavenging_parameters['human_resources']
            'scavenging' - human_resources
            amount of human resources needed for the scavenging process
        """
        self.COMPONENTS['inventory'] = 0
        for index, row in self.COMPONENTS.iterrows():
            for inv_comp in self.inventory_components.items:
                if inv_comp.part_type == row['Type']:
                    row['inventory'] += 1

        parent_node = find(self.producttype_tree, lambda node: node.name == core_to_scavenge.type)

        human_resources = eval(str(self.scavenging_parameters['human_resources']))
        priority = eval(str(self.scavenging_parameters['priority']))

        scavenge_duration_total = 0
        duration_data = {key: value for key, value in self.scavenging_parameters.items() if
                         key.startswith('duration')}
        duration_scavenging_process = 0
        for d in duration_data:
            duration_scavenging_process += eval(str(duration_data[d]))

        scavenge_duration_total += duration_scavenging_process
        for c in core_to_scavenge.components:
            for child in parent_node.children:
                if c.part_type == child.name:
                    if (c.quality > child.min_quality and
                            self.COMPONENTS['inventory'][c.id]+1 <= self.COMPONENTS['max stock capacity'][c.id]):
                        # scavenging of component
                        scavenge_duration = eval(str(child.scavenge_duration))
                        scavenge_duration_total += scavenge_duration
                        self.inventory_components.put(c)
                    else:  # recycling of component
                        recycle_duration = eval(str(child.recycle_duration))
                        scavenge_duration_total += recycle_duration
                    break

        if human_resources == 0:
            raise ValueError("no human resources for scavenging")

        granted_requests_scavenging = yield self.env.process(self.human_resources.request_skilled_workers
                                                             (duration=scavenge_duration_total,
                                                              skill="scavenging",
                                                              products=[core_to_scavenge],
                                                              amount=human_resources,
                                                              prio=priority))
        self.human_resources.release_skilled_workers(granted_requests_scavenging, skill="scavenging",
                                                     products=[core_to_scavenge])

    def recycling(self):
        """
        identifying products which quality is lower than the core_quality_scavenging_limits after grading for recycling

        Variables from Import File
        --------------------------
        core_quality_scavenging_limits[core.type] : float
            'producttype' - quality_scavenging_limit
            quality limit for a product to get scavenged, below this quality the product gets fully recycled
        """
        while True:
            if any([(core.quality < self.core_quality_scavenging_limits[core.type]) and core.quality >= 0
                    for core in self.stocks.items]):
                core_to_recycle = yield self.stocks.get(lambda p:
                                                        self.core_quality_scavenging_limits[p.type] > p.quality >= 0)
                self.env.process(self.recycling_process(core_to_recycle))
            else:
                yield self.env.timeout(0.1)

    def recycling_process(self, core_to_recycle: Product):
        """
        recycling process carried out by human resources with the skill 'recycling' for a process based duration

        Parameters
        ----------
        core_to_recycle : Product
            one product which was selected to be recycled

        Variables from Import File
        --------------------------
        recycling_parameters['priority'] : int
            'recycling' - priority
            process based priority relevant for the queue of processes for human_resources
        recycling_parameters['duration'] : int/float/numpy rng distribution
            'recycling' - duration
            process based duration for recycling
        child.recycle_duration : int/float/numpy rng distribution
            'producttype_parts' - recycle_duration
            duration to recycle a specific component
        recycling_parameters['human_resources']
            'recycling' - human_resources
            amount of human resources needed for the recycling process
        """
        parent_node = find(self.producttype_tree, lambda node: node.name == core_to_recycle.type)
        human_resources = eval(str(self.recycling_parameters['human_resources']))
        priority = eval(str(self.recycling_parameters['priority']))
        recycle_duration_total = 0
        duration_data = {key: value for key, value in self.recycling_parameters.items() if
                         key.startswith('duration')}
        duration_recycling_process = 0
        for d in duration_data:
            duration_recycling_process += eval(str(duration_data[d]))
        recycle_duration_total += duration_recycling_process

        for c in core_to_recycle.components:
            for child in parent_node.children:
                if c.part_type == child.name:
                    recycle_duration = eval(str(child.recycle_duration))
                    recycle_duration_total += recycle_duration
                    break

        granted_requests_recycling = yield self.env.process(self.human_resources.request_skilled_workers
                                                            (skill="recycling",
                                                             duration=recycle_duration_total,
                                                             products=[core_to_recycle],
                                                             amount=human_resources,
                                                             prio=priority))
        self.human_resources.release_skilled_workers(granted_requests_recycling, skill="recycling",
                                                     products=[core_to_recycle])