diff --git a/CHANGELOG.md b/CHANGELOG.md index 705dd118..c706be1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 See [Release](https://github.com/itsallcode/white-rabbit/releases/tag/v1.10.0) / [Milestone](https://github.com/itsallcode/white-rabbit/milestone/12?closed=1) -## [1.9.0] - 2022-??-?? +## [1.9.0] - 2024-??-?? See [Release](https://github.com/itsallcode/white-rabbit/releases/tag/v1.9.0) / [Milestone](https://github.com/itsallcode/white-rabbit/milestone/11?closed=1) @@ -22,7 +22,8 @@ This release requires Java 21. ### New Features -* [#273](https://github.com/itsallcode/white-rabbit/pull/273): Added buttons to monthly report for jumping to the previous/next month +* [#273](https://github.com/itsallcode/white-rabbit/pull/273): Added buttons to monthly report for jumping to the previous/next month. +* [#276](https://github.com/itsallcode/white-rabbit/pull/276): Added config option `reduce_mandatory_break_by_interruption`. ### Bugfixes diff --git a/docs/user_guide.md b/docs/user_guide.md index 99b1d714..02a80a9e 100644 --- a/docs/user_guide.md +++ b/docs/user_guide.md @@ -69,10 +69,12 @@ Restart WhiteRabbit after changing the configuration file. * We recommend to configure this when starting to use WhiteRabbit. * `mandatory_break`: mandatory break per day (default: 45 minutes). Format: see [below](#duration-format). Caution: This setting will also affect the past, i.e. the overtime of **all** days will be re-calculated. * We recommend to configure this when starting to use WhiteRabbit. +* `reduce_mandatory_break_by_interruption`: Reduce the mandatory break by entered interruption (`true` or `false`, default: `false`). If this is `true`, the mandatory break will be set to zero when the interruption is longer than the mandatory break. + * We recommend to configure this when starting to use WhiteRabbit. #### Duration format -Enter duration values in the format used by [Duration.parse()](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)). Examples: +Enter duration values in the format used by [Duration.parse()](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)). Examples: * `PT5H`: 5 hours * `PT5H30M`: 5 hours and 30 minutes @@ -221,4 +223,4 @@ The default values are: csv.destination = $HOME csv.separator = "," csv.filter_for_weekdays = False -``` \ No newline at end of file +``` diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/Config.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/Config.java index a1f165a8..d6aefb1a 100644 --- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/Config.java +++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/Config.java @@ -21,6 +21,8 @@ public interface Config Optional getMandatoryBreak(); + boolean reduceMandatoryBreakByInterruption(); + default Path getProjectFile() { return getDataDir().resolve(PROJECTS_JSON); diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/ConfigFile.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/ConfigFile.java index f7ae5968..fb4f577e 100644 --- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/ConfigFile.java +++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/ConfigFile.java @@ -79,6 +79,12 @@ public Optional getMandatoryBreak() return getOptionalValue("mandatory_break").map(Duration::parse); } + @Override + public boolean reduceMandatoryBreakByInterruption() + { + return getOptionalValue("reduce_mandatory_break_by_interruption").map(Boolean::valueOf).orElse(false); + } + @Override public boolean allowMultipleInstances() { diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/contract/ContractTermsService.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/contract/ContractTermsService.java index b48477f2..37ea733d 100644 --- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/contract/ContractTermsService.java +++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/contract/ContractTermsService.java @@ -18,7 +18,6 @@ public ContractTermsService(final Config config) this.config = config; } - public Duration getMandatoryBreak(final DayRecord day) { if (!day.getType().isWorkDay()) @@ -28,7 +27,15 @@ public Duration getMandatoryBreak(final DayRecord day) final Duration workingTime = day.getRawWorkingTime().minus(day.getInterruption()); if (workingTime.compareTo(MIN_WORKING_TIME_WITHOUT_BREAK) > 0) { - return getMandatoryBreak(); + Duration mandatoryBreak = getMandatoryBreak(); + if (config.reduceMandatoryBreakByInterruption()) + { + mandatoryBreak = mandatoryBreak.minus(day.getInterruption()); + } + if (mandatoryBreak.isPositive()) + { + return mandatoryBreak; + } } return Duration.ZERO; } diff --git a/logic/src/test/java/org/itsallcode/whiterabbit/logic/ConfigFileTest.java b/logic/src/test/java/org/itsallcode/whiterabbit/logic/ConfigFileTest.java index b1a94519..fcdfc860 100644 --- a/logic/src/test/java/org/itsallcode/whiterabbit/logic/ConfigFileTest.java +++ b/logic/src/test/java/org/itsallcode/whiterabbit/logic/ConfigFileTest.java @@ -114,6 +114,27 @@ void getMandatoryBreak_returnsCustomValue() assertThat(configFile.getMandatoryBreak()).isPresent().hasValue(Duration.ofMinutes(0)); } + @Test + void reduceMandatoryBreakByInterruption_returnsDefault() + { + when(propertiesMock.getProperty("reduce_mandatory_break_by_interruption")).thenReturn(null); + assertThat(configFile.reduceMandatoryBreakByInterruption()).isFalse(); + } + + @Test + void reduceMandatoryBreakByInterruption_returnsCustomValue() + { + when(propertiesMock.getProperty("reduce_mandatory_break_by_interruption")).thenReturn("true"); + assertThat(configFile.reduceMandatoryBreakByInterruption()).isTrue(); + } + + @Test + void reduceMandatoryBreakByInterruption_invalidValue() + { + when(propertiesMock.getProperty("reduce_mandatory_break_by_interruption")).thenReturn("invalid"); + assertThat(configFile.reduceMandatoryBreakByInterruption()).isFalse(); + } + @Test void getLocale_returnsDefault() { diff --git a/logic/src/test/java/org/itsallcode/whiterabbit/logic/ConfigTest.java b/logic/src/test/java/org/itsallcode/whiterabbit/logic/ConfigTest.java index 19ec2fad..cd1b21b4 100644 --- a/logic/src/test/java/org/itsallcode/whiterabbit/logic/ConfigTest.java +++ b/logic/src/test/java/org/itsallcode/whiterabbit/logic/ConfigTest.java @@ -59,6 +59,12 @@ public Optional getMandatoryBreak() return Optional.empty(); } + @Override + public boolean reduceMandatoryBreakByInterruption() + { + return false; + } + @Override public boolean allowMultipleInstances() { diff --git a/logic/src/test/java/org/itsallcode/whiterabbit/logic/model/DayRecordTest.java b/logic/src/test/java/org/itsallcode/whiterabbit/logic/model/DayRecordTest.java index 631793f2..78f9d7a8 100644 --- a/logic/src/test/java/org/itsallcode/whiterabbit/logic/model/DayRecordTest.java +++ b/logic/src/test/java/org/itsallcode/whiterabbit/logic/model/DayRecordTest.java @@ -16,6 +16,8 @@ import org.itsallcode.whiterabbit.logic.service.project.ProjectService; import org.itsallcode.whiterabbit.logic.storage.data.JsonModelFactory; import org.itsallcode.whiterabbit.logic.test.TestingConfig; +import org.itsallcode.whiterabbit.logic.test.TestingConfig.Builder; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -27,6 +29,13 @@ class DayRecordTest @Mock private ProjectService projectServiceMock; private final ModelFactory modelFactory = new JsonModelFactory(); + private Builder configBuilder; + + @BeforeEach + void createDefaultConfig() + { + configBuilder = TestingConfig.builder(); + } @Test void mandatoryWorkingTimeIsZeroOnWeekend() @@ -119,6 +128,30 @@ void testMandatoryBreakConsidersInterruptionLessThan6hours() Duration.ZERO); } + @Test + void testMandatoryBreakReducedByInterruption() + { + configBuilder.withReduceMandatoryBreakByInterruption(true); + assertMandatoryBreak(LocalDate.of(2018, 10, 1), LocalTime.of(8, 0), LocalTime.of(18, 0), Duration.ofMinutes(10), + Duration.ofMinutes(35)); + } + + @Test + void testMandatoryBreakNotReducedByInterruptionWhenWorkingLessThan6h() + { + configBuilder.withReduceMandatoryBreakByInterruption(true); + assertMandatoryBreak(LocalDate.of(2018, 10, 1), LocalTime.of(8, 0), LocalTime.of(13, 0), Duration.ofMinutes(10), + Duration.ZERO); + } + + @Test + void testMandatoryBreakZeroForLongerInterruption() + { + configBuilder.withReduceMandatoryBreakByInterruption(true); + assertMandatoryBreak(LocalDate.of(2018, 10, 1), LocalTime.of(8, 0), LocalTime.of(18, 0), Duration.ofMinutes(50), + Duration.ZERO); + } + @Test void testWorkingTime1h() { @@ -507,17 +540,17 @@ void dayWithActivitiesListIsNonDummy() assertNonDummyDay(day); } - private void assertDummyDay(DayRecord day) + private void assertDummyDay(final DayRecord day) { assertThat(day.isDummyDay()).as("dummy").isTrue(); } - private void assertNonDummyDay(DayRecord day) + private void assertNonDummyDay(final DayRecord day) { assertThat(day.isDummyDay()).as("dummy").isFalse(); } - private MonthIndex month(LocalDate date, Duration overtimePreviousMonth, DayData... days) + private MonthIndex month(final LocalDate date, final Duration overtimePreviousMonth, final DayData... days) { final MonthData jsonMonth = modelFactory.createMonthData(); jsonMonth.setDays(asList(days)); @@ -527,59 +560,63 @@ private MonthIndex month(LocalDate date, Duration overtimePreviousMonth, DayData return MonthIndex.create(contractTerms(), projectServiceMock, modelFactory, jsonMonth); } - private void assertOvertime(LocalDate date, LocalTime begin, LocalTime end, Duration expectedOvertime) + private void assertOvertime(final LocalDate date, final LocalTime begin, final LocalTime end, + final Duration expectedOvertime) { final DayRecord day = createDay(date, begin, end, null, null); assertThat(day.getOvertime()).as("overtime").isEqualTo(expectedOvertime); } - private void assertMandatoryBreak(LocalDate date, LocalTime begin, LocalTime end, Duration expectedDuration) + private void assertMandatoryBreak(final LocalDate date, final LocalTime begin, final LocalTime end, + final Duration expectedDuration) { assertMandatoryBreak(date, begin, end, Duration.ZERO, expectedDuration); } - private void assertMandatoryBreak(LocalDate date, LocalTime begin, LocalTime end, Duration interruption, - Duration expectedDuration) + private void assertMandatoryBreak(final LocalDate date, final LocalTime begin, final LocalTime end, + final Duration interruption, + final Duration expectedDuration) { assertThat(getMandatoryBreak(date, begin, end, interruption)).as("mandatory break").isEqualTo(expectedDuration); } - private void assertMandatoryWorkingTime(LocalDate date, LocalTime begin, LocalTime end, - Duration expectedMandatoryWorkingTime) + private void assertMandatoryWorkingTime(final LocalDate date, final LocalTime begin, final LocalTime end, + final Duration expectedMandatoryWorkingTime) { assertThat(getMandatoryWorkingTime(date, begin, end)).as("mandatory working time") .isEqualTo(expectedMandatoryWorkingTime); } - private Duration getMandatoryWorkingTime(LocalDate date, LocalTime begin, LocalTime end) + private Duration getMandatoryWorkingTime(final LocalDate date, final LocalTime begin, final LocalTime end) { final DayRecord day = createDay(date, begin, end, null, null); return day.getMandatoryWorkingTime(); } - private Duration getMandatoryBreak(LocalDate date, LocalTime begin, LocalTime end, Duration interruption) + private Duration getMandatoryBreak(final LocalDate date, final LocalTime begin, final LocalTime end, + final Duration interruption) { final DayRecord day = createDay(date, begin, end, null, interruption); return day.getMandatoryBreak(); } - private void assertWorkingDay(LocalDate date, boolean expected) + private void assertWorkingDay(final LocalDate date, final boolean expected) { assertThat(createDay(date).getType().isWorkDay()).isEqualTo(expected); } - private void assertType(LocalDate date, DayType expected) + private void assertType(final LocalDate date, final DayType expected) { final DayRecord day = createDay(date); assertDayType(day, expected); } - private void assertDayType(DayRecord day, DayType expected) + private void assertDayType(final DayRecord day, final DayType expected) { assertThat(day.getType()).isEqualTo(expected); } - private DayRecord createDay(LocalDate date) + private DayRecord createDay(final LocalDate date) { return createDay(date, null, null); } @@ -589,29 +626,32 @@ private DayRecord createDummyDay() return createDummyDay(LocalDate.of(2021, 7, 20)); } - private DayRecord createDummyDay(LocalDate date) + private DayRecord createDummyDay(final LocalDate date) { return createDay(date); } - private DayRecord createDay(LocalDate date, LocalTime begin, LocalTime end) + private DayRecord createDay(final LocalDate date, final LocalTime begin, final LocalTime end) { return createDay(date, begin, end, null, null); } - private DayRecord createDay(LocalDate date, LocalTime begin, LocalTime end, DayType type, Duration interruption) + private DayRecord createDay(final LocalDate date, final LocalTime begin, final LocalTime end, final DayType type, + final Duration interruption) { return createDay(date, begin, end, type, interruption, null); } - private DayRecord createDay(LocalDate date, LocalTime begin, LocalTime end, DayType type, Duration interruption, - DayRecord previousDay) + private DayRecord createDay(final LocalDate date, final LocalTime begin, final LocalTime end, final DayType type, + final Duration interruption, + final DayRecord previousDay) { return createDay(date, begin, end, type, interruption, previousDay, null); } - private DayRecord createDay(LocalDate date, LocalTime begin, LocalTime end, DayType type, Duration interruption, - DayRecord previousDay, MonthIndex month) + private DayRecord createDay(final LocalDate date, final LocalTime begin, final LocalTime end, final DayType type, + final Duration interruption, + final DayRecord previousDay, final MonthIndex month) { final DayData day = modelFactory.createDayData(); day.setBegin(begin); @@ -628,7 +668,7 @@ private DayRecord createDay(final DayData DayData) return new DayRecord(null, DayData, null, null, projectServiceMock, modelFactory); } - private DayRecord dayRecord(DayData day, DayRecord previousDay, MonthIndex month) + private DayRecord dayRecord(final DayData day, final DayRecord previousDay, final MonthIndex month) { final ContractTermsService contractTerms = contractTerms(); return new DayRecord(contractTerms, day, previousDay, month, projectServiceMock, modelFactory); @@ -636,6 +676,6 @@ private DayRecord dayRecord(DayData day, DayRecord previousDay, MonthIndex month private ContractTermsService contractTerms() { - return new ContractTermsService(TestingConfig.builder().build()); + return new ContractTermsService(configBuilder.build()); } } diff --git a/logic/src/test/java/org/itsallcode/whiterabbit/logic/test/TestingConfig.java b/logic/src/test/java/org/itsallcode/whiterabbit/logic/test/TestingConfig.java index e2d82daa..c2f6f459 100644 --- a/logic/src/test/java/org/itsallcode/whiterabbit/logic/test/TestingConfig.java +++ b/logic/src/test/java/org/itsallcode/whiterabbit/logic/test/TestingConfig.java @@ -13,6 +13,7 @@ public class TestingConfig implements Config private final Locale locale; private final Duration currentHoursPerDay; private final Duration mandatoryBreak; + private final boolean reduceMandatoryBreakByInterruption; private TestingConfig(final Builder builder) { @@ -20,6 +21,7 @@ private TestingConfig(final Builder builder) this.locale = builder.locale; this.currentHoursPerDay = builder.currentHoursPerDay; this.mandatoryBreak = builder.mandatoryBreak; + this.reduceMandatoryBreakByInterruption = builder.reduceMandatoryBreakByInterruption; } @Override @@ -46,6 +48,12 @@ public Optional getMandatoryBreak() return Optional.ofNullable(mandatoryBreak); } + @Override + public boolean reduceMandatoryBreakByInterruption() + { + return reduceMandatoryBreakByInterruption; + } + @Override public boolean allowMultipleInstances() { @@ -77,7 +85,8 @@ public static Builder builder() public static final class Builder { - public Duration mandatoryBreak; + private boolean reduceMandatoryBreakByInterruption = false; + private Duration mandatoryBreak; private Path dataDir; private Locale locale; private Duration currentHoursPerDay; @@ -110,6 +119,12 @@ public Builder withMandatoryBreak(final Duration mandatoryBreak) return this; } + public Builder withReduceMandatoryBreakByInterruption(final boolean reduceMandatoryBreakByInterruption) + { + this.reduceMandatoryBreakByInterruption = reduceMandatoryBreakByInterruption; + return this; + } + public TestingConfig build() { return new TestingConfig(this);