DefaultIBizProjectImporter.java

  1. /*
  2.  * *************************************************************************************************************************************************************
  3.  *
  4.  * blueHour: open source accounting
  5.  * http://tidalwave.it/projects/bluehour
  6.  *
  7.  * Copyright (C) 2013 - 2024 by Tidalwave s.a.s. (http://tidalwave.it)
  8.  *
  9.  * *************************************************************************************************************************************************************
  10.  *
  11.  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
  12.  * You may obtain a copy of the License at
  13.  *
  14.  *     http://www.apache.org/licenses/LICENSE-2.0
  15.  *
  16.  * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
  17.  * CONDITIONS OF ANY KIND, either express or implied.  See the License for the specific language governing permissions and limitations under the License.
  18.  *
  19.  * *************************************************************************************************************************************************************
  20.  *
  21.  * git clone https://bitbucket.org/tidalwave/bluehour-src
  22.  * git clone https://github.com/tidalwave-it/bluehour-src
  23.  *
  24.  * *************************************************************************************************************************************************************
  25.  */
  26. package it.tidalwave.accounting.importer.ibiz.impl;

  27. import javax.annotation.Nonnull;
  28. import java.util.List;
  29. import java.util.stream.Stream;
  30. import java.io.IOException;
  31. import java.nio.file.FileVisitResult;
  32. import java.nio.file.Files;
  33. import java.nio.file.Path;
  34. import java.nio.file.SimpleFileVisitor;
  35. import java.nio.file.attribute.BasicFileAttributes;
  36. import it.tidalwave.accounting.importer.ibiz.spi.IBizProjectImporter;
  37. import it.tidalwave.accounting.model.CustomerRegistry;
  38. import it.tidalwave.accounting.model.JobEvent;
  39. import it.tidalwave.accounting.model.JobEventGroup;
  40. import it.tidalwave.accounting.model.ProjectRegistry;
  41. import it.tidalwave.accounting.model.spi.TimedJobEventSpi;
  42. import it.tidalwave.accounting.model.types.Money;
  43. import it.tidalwave.util.NotFoundException;
  44. import lombok.RequiredArgsConstructor;
  45. import lombok.extern.slf4j.Slf4j;
  46. import static java.util.stream.Collectors.*;

  47. /***************************************************************************************************************************************************************
  48.  *
  49.  * @author  Fabrizio Giudici
  50.  *
  51.  **************************************************************************************************************************************************************/
  52. @Slf4j @RequiredArgsConstructor
  53. public class DefaultIBizProjectImporter implements IBizProjectImporter
  54.   {
  55.     @Nonnull
  56.     private final CustomerRegistry customerRegistry;

  57.     @Nonnull
  58.     private final ProjectRegistry projectRegistry;

  59.     @Nonnull
  60.     private final Path path;

  61.     /***********************************************************************************************************************************************************
  62.      * Imports the projects.
  63.      **********************************************************************************************************************************************************/
  64.     @Override
  65.     public void importProjects()
  66.       throws IOException
  67.       {
  68.         log.debug("importProjects()");
  69.         Files.walkFileTree(path.resolve("Projects"), new SimpleFileVisitor<>()
  70.           {
  71.             @Override
  72.             public FileVisitResult visitFile (@Nonnull final Path file, @Nonnull final BasicFileAttributes attrs)
  73.                     throws IOException
  74.               {
  75.                 if (!".DS_Store".equals(file.toFile().getName()))
  76.                   {
  77.                     importProject(file);
  78.                   }

  79.                 return FileVisitResult.CONTINUE;
  80.               }
  81.           });
  82.       }
  83.    
  84.     /***********************************************************************************************************************************************************
  85.      * @throws  IOException  in case of error
  86.      **********************************************************************************************************************************************************/
  87.     private void importProject (@Nonnull final Path file)
  88.       throws IOException
  89.       {
  90.         try
  91.           {
  92.             importProject(IBizUtils.loadConfiguration(file));
  93.           }
  94.         catch (NotFoundException e)
  95.           {
  96.             throw new IOException(e);
  97.           }
  98.       }

  99.     /***********************************************************************************************************************************************************
  100.      * Imports a project from the given configuration object.
  101.      *
  102.      * @param  projectConfig  the configuration object
  103.      **********************************************************************************************************************************************************/
  104.     private void importProject (@Nonnull final ConfigurationDecorator projectConfig)
  105.       throws NotFoundException
  106.       {
  107.         final var customerId = projectConfig.getId("clientIdentifier");
  108.         final var customer =
  109.                 customerRegistry.findCustomers().withId(customerId).optionalResult().orElseThrow(NotFoundException::new);
  110.         final var status = IBizProjectStatus.values()[projectConfig.getInt("projectStatus")];

  111.         if (status.getMappedStatus() == null)
  112.           {
  113.             log.warn("IGNORING PROJECT {} with status {}", projectConfig.getString("projectName"), status);  
  114.           }
  115.         else
  116.           {
  117.             final var jobEvents = importJobEvents(projectConfig.getStream("jobEvents"));
  118.             projectRegistry.addProject().withId(projectConfig.getId("uniqueIdentifier"))
  119.                                         .withBudget(projectConfig.getMoney("projectEstimate"))
  120.                                         .withCustomer(customer)
  121.                                         .withName(projectConfig.getString("projectName"))
  122.     //                                           .withDescription("description of project 1")
  123.                                         .withStartDate(projectConfig.getDate("projectStartDate"))
  124.                                         .withEndDate(projectConfig.getDate("projectDueDate"))
  125.                                         .withNotes(projectConfig.getString("projectNotes"))
  126.                                         .withNumber(projectConfig.getString("projectNumber"))
  127.                                         .withStatus(status.getMappedStatus())
  128.                                         .withHourlyRate(getHourlyRate(projectConfig, jobEvents))
  129.                                         .withEvents(jobEvents)
  130.                                         .create();
  131.           }
  132.       }

  133.     /***********************************************************************************************************************************************************
  134.      * Retrieves the hourly rates - if missing from the project description, tries to recover it from the first
  135.      * meaningful job event.
  136.      **********************************************************************************************************************************************************/
  137.     @Nonnull
  138.     private Money getHourlyRate(final ConfigurationDecorator projectConfig, final List<? extends JobEvent> jobEvents)
  139.             throws NotFoundException
  140.       {
  141.         var hourlyRate = projectConfig.getMoney("projectRate");
  142.      
  143.         if ((hourlyRate.compareTo(Money.ZERO) == 0) && !jobEvents.isEmpty())
  144.             // don't use equals() - see http://stackoverflow.com/questions/6787142/bigdecimal-equals-versus-compareto
  145.                                         {
  146.                                           var event = jobEvents.get(0);
  147.            
  148.             while ((event instanceof JobEventGroup) && ((JobEventGroup)event).findChildren().count() > 0)
  149.               {
  150.                 event = ((JobEventGroup)event).findChildren().optionalFirstResult().orElseThrow(NotFoundException::new);
  151.               }
  152.            
  153.             if (event instanceof TimedJobEventSpi)
  154.               {
  155.                 hourlyRate = ((TimedJobEventSpi)event).getHourlyRate();
  156.               }
  157.           }
  158.         return hourlyRate;
  159.       }

  160.     /***********************************************************************************************************************************************************
  161.      * Imports the job events.
  162.      **********************************************************************************************************************************************************/
  163.     @Nonnull
  164.     private List<JobEvent> importJobEvents (@Nonnull final Stream<? extends ConfigurationDecorator> jobEventsConfig)
  165.       {
  166.         return jobEventsConfig.map(this::importJobEvent).collect(toList());
  167.       }

  168.     /***********************************************************************************************************************************************************
  169.      * Imports a single job event.
  170.      **********************************************************************************************************************************************************/
  171.     @Nonnull
  172.     private JobEvent importJobEvent (@Nonnull final ConfigurationDecorator jobEventConfig)
  173.       {
  174. //        log.debug(">>>> properties: {}", toList(jobEvent.getKeys()));

  175. //        final boolean checkedOut = jobEvent.getBoolean("checkedout");
  176. //        final boolean isExpense = jobEvent.getBoolean("isExpense");
  177. //        final boolean nonBillable = jobEvent.getBoolean("nonBillable");
  178. //        final int taxable = jobEvent.getInt("taxable");
  179. //        final DateTime lastModifiedDate = jobEvent.getDateTime("lastModifiedDate");
  180. //        final int paid = jobEvent.getInt("jobEventPaid");
  181.         final var type = IBizJobEventType.values()[jobEventConfig.getInt("jobEventType")];

  182.         return JobEvent.builder().withId(jobEventConfig.getId("uniqueIdentifier"))
  183.                                  .withType(type.getMappedType())
  184.                                  .withStartDateTime(jobEventConfig.getDateTime("jobEventStartDate"))
  185.                                  .withEndDateTime(jobEventConfig.getDateTime("jobEventEndDate"))
  186.                                  .withName(jobEventConfig.getString("jobEventName"))
  187.                                  .withDescription(jobEventConfig.getString("jobEventNotes"))
  188.                                  .withHourlyRate(jobEventConfig.getMoney("jobEventRate"))
  189.                                  .withEarnings(jobEventConfig.getMoney("jobEventEarnings"))
  190.                                  .withEvents(importJobEvents(jobEventConfig.getStream("children")))
  191.                                  .create();
  192.         /*
  193.                                         <key>tax1</key>
  194.                                         <real>22</real>

  195.          */
  196.       }
  197.   }