diff --git a/navalplanner-business/src/main/java/org/navalplanner/business/orders/entities/SchedulingState.java b/navalplanner-business/src/main/java/org/navalplanner/business/orders/entities/SchedulingState.java
new file mode 100644
index 000000000..d5dd30ffe
--- /dev/null
+++ b/navalplanner-business/src/main/java/org/navalplanner/business/orders/entities/SchedulingState.java
@@ -0,0 +1,262 @@
+/*
+ * This file is part of ###PROJECT_NAME###
+ *
+ * Copyright (C) 2009 Fundación para o Fomento da Calidade Industrial e
+ * Desenvolvemento Tecnolóxico de Galicia
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.navalplanner.business.orders.entities;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.lang.Validate;
+import org.apache.commons.lang.builder.ToStringBuilder;
+
+/**
+ * @author Óscar González Fernández
+ *
+ */
+public class SchedulingState {
+
+ public enum Type {
+ SCHEDULING_POINT {
+ @Override
+ public boolean belongsToSchedulingPoint() {
+ return true;
+ }
+
+ @Override
+ public boolean isCompletelyScheduled() {
+ return true;
+ }
+
+ @Override
+ public boolean isPartiallyScheduled() {
+ return false;
+ }
+ },
+ SCHEDULED_SUBELEMENT {
+ @Override
+ public boolean belongsToSchedulingPoint() {
+ return true;
+ }
+
+ @Override
+ public boolean isCompletelyScheduled() {
+ return true;
+ }
+
+ @Override
+ public boolean isPartiallyScheduled() {
+ return false;
+ }
+ },
+ PARTIALY_SCHEDULED_SUPERELEMENT {
+ @Override
+ public boolean belongsToSchedulingPoint() {
+ return false;
+ }
+
+ @Override
+ public boolean isCompletelyScheduled() {
+ return false;
+ }
+
+ @Override
+ public boolean isPartiallyScheduled() {
+ return true;
+ }
+ },
+ COMPLETELY_SCHEDULED_SUPERELEMENT {
+ @Override
+ public boolean belongsToSchedulingPoint() {
+ return false;
+ }
+
+ @Override
+ public boolean isCompletelyScheduled() {
+ return true;
+ }
+
+ @Override
+ public boolean isPartiallyScheduled() {
+ return false;
+ }
+ },
+ NO_SCHEDULED {
+ @Override
+ public boolean belongsToSchedulingPoint() {
+ return false;
+ }
+
+ @Override
+ public boolean isCompletelyScheduled() {
+ return false;
+ }
+
+ @Override
+ public boolean isPartiallyScheduled() {
+ return false;
+ }
+ };
+
+ public abstract boolean belongsToSchedulingPoint();
+
+ public abstract boolean isPartiallyScheduled();
+
+ public abstract boolean isCompletelyScheduled();
+
+ public boolean isSomewhatScheduled() {
+ return isPartiallyScheduled() || isCompletelyScheduled();
+ }
+ }
+
+ private Type type = Type.NO_SCHEDULED;
+ private SchedulingState parent;
+
+ private List children = new ArrayList();
+
+ public SchedulingState() {
+ }
+
+ public SchedulingState(List children) {
+ for (SchedulingState each : children) {
+ if (!each.isRoot()) {
+ throw new IllegalArgumentException(each
+ + " is already child of another "
+ + SchedulingState.class.getSimpleName());
+ }
+ add(each);
+ }
+ }
+
+ public SchedulingState(Type type) {
+ this.type = type;
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ public void add(SchedulingState child) {
+ child.parent = this;
+ children.add(child);
+ setType(calculateTypeFromChildren());
+ }
+
+ public SchedulingState getParent() {
+ return parent;
+ }
+
+ public boolean isRoot() {
+ return parent == null;
+ }
+
+ public boolean canBeScheduled() {
+ return type == Type.NO_SCHEDULED;
+ }
+
+ public void schedule() {
+ if (type.isSomewhatScheduled()) {
+ throw new IllegalStateException("it's already somewhat scheduled");
+ }
+ setType(Type.SCHEDULING_POINT);
+ for (SchedulingState schedulingState : getDescendants()) {
+ schedulingState.setType(Type.SCHEDULED_SUBELEMENT);
+ }
+ }
+
+ private void setType(Type type) {
+ if (this.type == type) {
+ return;
+ }
+ this.type = type;
+ notifyParentOfTypeChange();
+ }
+
+ private void notifyParentOfTypeChange() {
+ if (isRoot()) {
+ return;
+ }
+ parent.typeChangedOnChild(this);
+ }
+
+ private void typeChangedOnChild(SchedulingState child) {
+ if (getType().belongsToSchedulingPoint()) {
+ return;
+ }
+ setType(calculateTypeFromChildren());
+ }
+
+ private Type calculateTypeFromChildren() {
+ Validate.isTrue(!children.isEmpty());
+ boolean allScheduled = true;
+ boolean someScheduled = false;
+ for (SchedulingState each : children) {
+ someScheduled = someScheduled
+ || each.getType().isSomewhatScheduled();
+ allScheduled = allScheduled
+ && each.getType().isCompletelyScheduled();
+ }
+ if (allScheduled) {
+ return Type.COMPLETELY_SCHEDULED_SUPERELEMENT;
+ } else if (someScheduled) {
+ return Type.PARTIALY_SCHEDULED_SUPERELEMENT;
+ } else {
+ return Type.NO_SCHEDULED;
+ }
+ }
+
+ private List getDescendants() {
+ List result = new ArrayList();
+ addDescendants(result);
+ return result;
+ }
+
+ private void addDescendants(List result) {
+ for (SchedulingState each : children) {
+ result.add(each);
+ each.addDescendants(result);
+ }
+ }
+
+ public boolean isCompletelyScheduled() {
+ return type.isCompletelyScheduled();
+ }
+
+ public boolean isPartiallyScheduled() {
+ return type.isPartiallyScheduled();
+ }
+
+ public boolean isSomewhatScheduled() {
+ return type.isSomewhatScheduled();
+ }
+
+ public void removeChild(SchedulingState schedulingState) {
+ boolean removed = children.remove(schedulingState);
+ if (removed) {
+ schedulingState.parent = null;
+ }
+ setType(calculateTypeFromChildren());
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this).append(type).toString();
+ }
+
+
+}
diff --git a/navalplanner-business/src/test/java/org/navalplanner/business/test/orders/entities/SchedulingStateTest.java b/navalplanner-business/src/test/java/org/navalplanner/business/test/orders/entities/SchedulingStateTest.java
new file mode 100644
index 000000000..51472c5d1
--- /dev/null
+++ b/navalplanner-business/src/test/java/org/navalplanner/business/test/orders/entities/SchedulingStateTest.java
@@ -0,0 +1,277 @@
+/*
+ * This file is part of ###PROJECT_NAME###
+ *
+ * Copyright (C) 2009 Fundación para o Fomento da Calidade Industrial e
+ * Desenvolvemento Tecnolóxico de Galicia
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.navalplanner.business.test.orders.entities;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.matchers.JUnitMatchers.each;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.junit.Before;
+import org.junit.Test;
+import org.navalplanner.business.orders.entities.SchedulingState;
+import org.navalplanner.business.orders.entities.SchedulingState.Type;
+
+/**
+ * @author Óscar González Fernández
+ *
+ */
+public class SchedulingStateTest {
+
+ private SchedulingState root;
+
+ private SchedulingState childA;
+
+ private SchedulingState childB;
+
+ private SchedulingState grandChildA1;
+
+ private SchedulingState grandChildA2;
+
+ private SchedulingState grandChildB1;
+
+ private SchedulingState grandChildB2;
+
+ @Before
+ public void setUp() {
+ root = new SchedulingState();
+ root.add(childA = new SchedulingState());
+ childA.add(grandChildA1 = new SchedulingState());
+ childA.add(grandChildA2 = new SchedulingState());
+ root.add(childB = new SchedulingState());
+ childB.add(grandChildB1 = new SchedulingState());
+ childB.add(grandChildB2 = new SchedulingState());
+ }
+
+ private List all(){
+ return Arrays.asList(root,childA, childB, grandChildA1, grandChildA2,
+ grandChildB1, grandChildB2);
+ }
+
+ private List allRootDescendants() {
+ return Arrays.asList(childA, childB, grandChildA1, grandChildA2,
+ grandChildB1, grandChildB2);
+ }
+
+ @Test
+ public void aNewlyCreatedSchedulingStateIsNoScheduled() {
+ SchedulingState schedulingState = new SchedulingState();
+ assertThat(schedulingState.getType(),
+ equalTo(Type.NO_SCHEDULED));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void cannotCreateASchedulingStateWithChildrenAlreadyAssigned() {
+ new SchedulingState(Arrays.asList(childA));
+ }
+
+ @Test
+ public void anAddedSchedulingStateHasAParent() {
+ assertThat(childA.getParent(), equalTo(root));
+ }
+
+ @Test
+ public void theRootOfASchedulingStateTreeHasNoParent() {
+ assertNull(root.getParent());
+ }
+
+ @Test
+ public void ifHasNoParentItsRoot() {
+ assertTrue(root.isRoot());
+ }
+
+ @Test
+ public void whenSchedulingAElementItTursIntoASchedulingPoint() {
+ grandChildA1.schedule();
+ assertThat(grandChildA1.getType(), equalTo(Type.SCHEDULING_POINT));
+ }
+
+ @Test
+ public void whenSchedulingAElementItTurnsAllItsDescendantsIntoScheduledSubelements() {
+ root.schedule();
+ assertThat(allRootDescendants(),
+ each(hasType(Type.SCHEDULED_SUBELEMENT)));
+ }
+
+ @Test
+ public void aScheduledElementIsCompletelyScheduled() {
+ root.schedule();
+ assertThat(all(), each(completelyScheduled()));
+ }
+
+ @Test
+ public void aSomewhatScheduledElemenetCannotBeScheduled() {
+ grandChildA1.schedule();
+ grandChildB1.schedule();
+ for (SchedulingState schedulingState : all()) {
+ if (schedulingState == grandChildA2
+ || schedulingState == grandChildB2) {
+ // can be scheduled
+ continue;
+ }
+ try {
+ schedulingState.schedule();
+ fail("must send " + IllegalStateException.class);
+ } catch (IllegalStateException e) {
+ // ok
+ }
+ }
+ }
+
+ @Test
+ public void scheduledSubelementsCantBeScheduled() {
+ root.schedule();
+ assertThat(allRootDescendants(), each(not(canBeScheduled())));
+ }
+
+ @Test
+ public void aNoScheduledElementCanBeScheduled() {
+ assertThat(all(), each(canBeScheduled()));
+ }
+
+ @Test
+ public void aSchedulingPointCantBeScheduled() {
+ root.schedule();
+ assertFalse(root.canBeScheduled());
+ }
+
+ @Test
+ public void rootIsPartiallyScheduledWhenSchedulingOneOfTheChildren() {
+ childA.schedule();
+ assertThat(root, hasType(Type.PARTIALY_SCHEDULED_SUPERELEMENT));
+ }
+
+ @Test
+ public void rootIsCompletelyScheduledWhenSchedulingAllOfTheChildren() {
+ childA.schedule();
+ childB.schedule();
+ assertThat(root, hasType(Type.COMPLETELY_SCHEDULED_SUPERELEMENT));
+ }
+
+ @Test
+ public void whenSchedulingAGrandChildrenTheRootIsPartiallyScheduled() {
+ grandChildA1.schedule();
+ assertThat(root, hasType(Type.PARTIALY_SCHEDULED_SUPERELEMENT));
+ }
+
+ @Test
+ public void ifSchedulingAllGrandChildrenTheRootIsCompletelyScheduled() {
+ grandChildA1.schedule();
+ grandChildA2.schedule();
+ grandChildB1.schedule();
+ grandChildB2.schedule();
+ assertThat(root, hasType(Type.COMPLETELY_SCHEDULED_SUPERELEMENT));
+ }
+
+ @Test
+ public void addingANewChildToACompletelyScheduled() {
+ childA.schedule();
+ childB.schedule();
+ root.add(new SchedulingState());
+ assertThat(root, hasType(Type.PARTIALY_SCHEDULED_SUPERELEMENT));
+ }
+
+ @Test
+ public void removingTheOnlyNoScheduled() {
+ childA.schedule();
+ root.removeChild(childB);
+ assertThat(root, hasType(Type.COMPLETELY_SCHEDULED_SUPERELEMENT));
+ }
+
+ @Test
+ public void removingAChildMakesItHasNoParent() {
+ childA.schedule();
+ root.removeChild(childB);
+ assertThat(childB.getParent(), nullValue());
+ }
+
+ abstract static class SchedulingStateMatcher extends
+ BaseMatcher {
+ @Override
+ public boolean matches(Object object) {
+ if (object instanceof SchedulingState) {
+ return matches((SchedulingState) object);
+ } else {
+ return false;
+ }
+ }
+
+ protected abstract boolean matches(SchedulingState schedulingState);
+ }
+
+ private Matcher hasType(final Type type) {
+ return new SchedulingStateMatcher() {
+
+ @Override
+ public boolean matches(SchedulingState state) {
+ return state.getType() == type;
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description
+ .appendText("the type of the SchedulingState must be: "
+ + type);
+ }
+ };
+ }
+
+ private Matcher completelyScheduled() {
+ return new SchedulingStateMatcher() {
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("completely scheduled");
+ }
+
+ @Override
+ protected boolean matches(SchedulingState schedulingState) {
+ return schedulingState.isCompletelyScheduled();
+ }
+ };
+ }
+
+ private Matcher canBeScheduled() {
+ return new SchedulingStateMatcher() {
+
+ @Override
+ public boolean matches(SchedulingState state) {
+ return state.canBeScheduled();
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("can be scheduled");
+ }
+ };
+ }
+
+}