void () { @NonNls String part1 = """ package com.test; //------------------------------------------------------------------ // Copyright (c) 1999, 2007 // WorkForce Software, Inc. // All rights reserved. // // Web-site: http://www.workforcesoftware.com // E-mail: support@workforcesoftware.com // Phone: (877) 493-6723 // // This program is protected by copyright laws and is considered // a trade secret of WorkForce Software. Access to this program // and source code is granted only to licensed customers. Under // no circumstances may this software or source code be distributed // without the prior written consent of WorkForce Software. // ----------------------------------------------------------------- import com.workforcesoftware.Data.Employee.*; import com.workforcesoftware.Data.*; import com.workforcesoftware.Data.assignment.Asgnmt_master; import com.workforcesoftware.Data.assignment.Asgnmt; import com.workforcesoftware.Data.Output.Time_sheet_output; import com.workforcesoftware.Data.TimeSched.PayPeriodData; import com.workforcesoftware.Data.TimeSched.TimeSchedUtils; import com.workforcesoftware.Data.TimeSched.Schedule.Schedule_detailList; import com.workforcesoftware.Data.TimeSched.TimeSheet.*; import com.workforcesoftware.Data.assignment.Asgnmt_masterList; import com.workforcesoftware.Exceptions.*; import com.workforcesoftware.Gen.Choice.Approval_event_type; import com.workforcesoftware.Gen.Choice.Transaction_status; import com.workforcesoftware.Gen.Choice.Messages; import com.workforcesoftware.Gen.Choice.Program_source; import com.workforcesoftware.Gen.Other.DbRec.DbRecTime_sheet_output; import com.workforcesoftware.Gen.Other.DbRec.DbRecTime_sheet; import com.workforcesoftware.Gen.Other.List.WDateList; import com.workforcesoftware.Gen.Policy.Right_grp; import com.workforcesoftware.Gen.Policy.Policy_profile; import com.workforcesoftware.Policy.*; import com.workforcesoftware.Util.DateTime.WDate; import com.workforcesoftware.Util.DateTime.WDateTime; import com.workforcesoftware.Util.DB.ListWriter; import com.workforcesoftware.Util.DB.DbRecFieldCopier; import com.workforcesoftware.ClientRequests.TimeSched.TimeSchedUtil; import com.workforcesoftware.ClientRequests.TimeSched.PayPeriodInfo; import com.workforcesoftware.ClientRequests.TimeEntry.ApprovalInfo; import com.workforcesoftware.ClientRequests.TimeEntry.BankBalanceResults; import com.workforcesoftware.Misc.ServerErrorLogger; import com.workforcesoftware.Misc.ServerError; import com.workforcesoftware.AssignmentPeriod.*; import com.workforcesoftware.AssignmentPeriod.TimeSheetState; import com.workforcesoftware.Dictionary.DataDictionary; import java.sql.SQLException; import java.util.*; import org.log4j.Category; /** * Holds definition of {@link CalcInfo} */ class AssignmentManager { /** * Implementation of {@link TimeSheetCalculationInfo}. * Extracts all the required data from AllCalcDataManager, AllCalculationData, Time_sheet, etc. * Makes copies of all mutable data, unmodifiable lists where possible. */ static class CalcInfo implements TimeSheetCalculationInfo { /** * Use the passed in AllCalculationData object - DO NOT try to get it from the passed in AllCalcDataManager because * the ACD object that's passed in is obtained from AssignmentManager.getAllCalcData method which handles the * scenario where a prior period may need to be amended automatically and if ACDM.getAllCalcData() method is called, * you might get an unmodifiable ACD * @param acdm * @param acd * @param timeSheetId * @param parms */ CalcInfo(AllCalcDataManager acdm, AllCalculationData acd, TimeSheetIdentifier timeSheetId, TimeEntryParms parms) { // we'll extract all these vars separately, for readability final Right_grp rightGrp = parms.getRight_grp(timeSheetId); final AllPolicies ap = acd.getAllPolicies(); final WDate ppEndForToday = TimeSchedUtil.getPayPeriodRangeForToday(acdm).getEnd(); this.acd = acd; this.timeSheetIdentifier = timeSheetId; this.approval_eventList = acd.getApproval_eventList().getUnmodifiableList(); this.policyProfile = acd.getAsgnmtMaster().getPolicyProfilePolicy(ppEndForToday, ap); this.adjustmentsPaidWithTimesheet = createAdjustmentsPaidWithTimesheet(acdm, acd); this.bankBalanceResults = acdm.getBankDataForBankBalancePreview(ap, acd, rightGrp.getRight_grp(), acd.getPP_end()); this.tsoListForPayPreview = Collections.unmodifiableList(acdm.getTsoListForPayPreview(acd, rightGrp.getRight_grp())); this.approvedDays = createApprovedDays(acd); try { // these are fairly involved calculations, and we want to validate the pay period list, // so we'll do them both here together instead of upon request employeePeriodInfo = acdm.getEmployeePeriodInfo(timeSheetId.getPpEnd()); payPeriodList = TimeSchedUtil.calcPayPeriodList(acdm, rightGrp.getRight_grp()); } catch (Exception e) { throw new InternalApplicationException("Loading calc data for: " + timeSheetId, e); } assertPayPeriodList(); AssignmentPeriodStateImpl aps = new AssignmentPeriodStateImpl(employeePeriodInfo, rightGrp, parms.getSystemFeature(), ap, ppEndForToday, acdm, policyProfile); assignmentPeriodState = aps; timeSheetState = new TimeSheetStateImpl(this, aps, policyProfile); } /** * Returns an ApprovalInfo object, which contains information about whether * employee/manager approved a timesheet. * * @see ApprovalInfo */ public ApprovalInfo getApprovalInfo() { return new ApprovalInfo(acd.getTime_sheet(), approval_eventList); } public Collection<DbRecTime_sheet_output> getAdjustmentsPaidWithTimesheet() { return adjustmentsPaidWithTimesheet; } public Approval_eventList getApproval_eventList() { return approval_eventList; } /** * Returns state information about a given assignment and period, encompassing * the cross-section of security related settings calculated from: * <ul> * <li>App_user Roles (app_user_right and right_grp tables) * <li>Assignment effective dates * <li>Employee record effective dates * <li>System_feature read/write privileges * </ul> * * @return not ever null */ public AssignmentPeriodState getAssignmentPeriodState() { return assignmentPeriodState; } public Set<WDate> getDailyApprovalDays() { return approvedDays; } /** * Returns map to the accrual banks data, which is a read only reference * of the information contained there. The banks returned will be all banks attached * to the assignment, including aggregate banks if applicable. * * The list will not contain any banks that the current App_user's Right_grp cannot view for this assignment's * Policy_profile (as defined by {@link com.workforcesoftware.Gen.Policy.User_entry_rule_detail#getDisplay_bank_set()}) */ public List<BankBalanceResults> getBankBalanceResults() { return bankBalanceResults; } public Approval_event getLastValidEmployeeApproveEvent() { return approval_eventList.getLastValidEmployeeApproveEvent(); } public Time_sheet_detail_splitList getTime_sheet_detail_splitList() { return acd.getTime_sheet().getTime_sheet_detail_splitList().getUnmodifiableList(); } public Time_sheet_detailList getTime_sheet_detailList() { return acd.getTime_sheet().getDetailList().getUnmodifiableList(); } public Schedule_detailList getSchedule_detailList() { return acd.getSchedule().getDetailList().getUnmodifiableList(); } public Time_sheet_exceptionList getTime_sheet_exceptionList() { return acd.getTime_sheet().getTime_sheet_exceptionList().getUnmodifiableList(); } public List<Time_sheet_output> getTsoListForPayPreview() { return tsoListForPayPreview; } public Approval_eventList getValidManagerApproveEvents() { return approval_eventList.getValidManagerApproveEvents(); } public TimeSheetIdentifier getTimeSheetIdentifier() { return timeSheetIdentifier; } public EmployeePeriodInfo getEmployeePeriodInfo() { return employeePeriodInfo; } public Employee_master getEmployeeMaster() { return new Employee_master(acd.getEmployee_master()); } public Employee getEmployee() { return new Employee(acd.getEmployee_master().getEmployeeAsOfOrAfter(getPPEnd())); } public Asgnmt_master getAsgnmtMaster() { return new Asgnmt_master(acd.getAsgnmtMaster()); } public Asgnmt getAsgnmt() { return new Asgnmt(acd.getAsgnmt()); } public WDate getPPEnd() { return employeePeriodInfo.getPp_end(); } public WDate getPPBegin() { return employeePeriodInfo.getPp_begin(); } public Policy_profile getPolicy_profile() { return policyProfile; } /** * Return state information about the timesheet, which considers security * relationship between the current state (locked/closed/amended) in relation to: * <ul> * <li>App_user Roles (app_user_right and right_grp tables) * <li>Assignment effective dates * <li>Employee record effective dates * <li>System_feature read/write privileges * <li>Approval level of the timesheet * </ul> * * @return not ever null */ public TimeSheetState getTimeSheetState() { return timeSheetState; } public int getVersionNumber() { return timeSheetIdentifier.getVersion(); } public PayPeriodData getPayPeriodData() { return acd.getPayPeriodData(); } public boolean getIsNewlyUserAmended() { return acd.isNewlyUserAmended(); } public List<PayPeriodInfo> getPayPeriodList() { return payPeriodList; } private static Collection<DbRecTime_sheet_output> createAdjustmentsPaidWithTimesheet(AllCalcDataManager acdm, AllCalculationData acd) { try { return Collections.unmodifiableCollection(acdm.getAdjustmentsPaidWithTimesheet(acd)); } catch (SQLException e) { throw new InternalApplicationException("Unexpected SQL Exception", e); } } private void assertPayPeriodList() { //TODO - probably need to show a "nice" message to the user saying that the employee doesn't //TODO - have a viewable/editable pay period. if(payPeriodList.isEmpty()) { throw new InternalApplicationException("No viewable/editable pay periods found for employee " + employeePeriodInfo.getEmployeeMaster().getEmployee()); } } private static Set<WDate> createApprovedDays(AllCalculationData acd) { // calculate days approved by daily approvals Set<WDate> approvedDays = new HashSet<WDate>(); for( WDate date : acd.getPayPeriodData().getActiveDates().getAllDates() ) { if( acd.isDayApproved(date) ) { approvedDays.add(date); } } return Collections.unmodifiableSet(approvedDays); } private final Collection<DbRecTime_sheet_output> adjustmentsPaidWithTimesheet; private final Approval_eventList approval_eventList; private final AssignmentPeriodState assignmentPeriodState; private final List<BankBalanceResults> bankBalanceResults; private final List<Time_sheet_output> tsoListForPayPreview; private final TimeSheetIdentifier timeSheetIdentifier; private final TimeSheetState timeSheetState; private final EmployeePeriodInfo employeePeriodInfo; private final ArrayList<PayPeriodInfo> payPeriodList; private final Set<WDate> approvedDays; private final Policy_profile policyProfile; private final AllCalculationData acd; } AssignmentManager(GeneratedId assignmentId, Set<TimeSheetIdentifier> timeSheetIdentifiers, TimeEntryParmsPerAssignment parms) { this.asgnmtMaster = getAggregateOrSingleAssignmentMaster(assignmentId); setClassFields(parms, timeSheetIdentifiers); } /** * Obtains the single or aggregate assignment master for the given assignmentId. If the given assignmentId is not a * single or aggregate assignment, fetches the aggregate associated with the given assignmentId and returns that. * * @param assignmentId Id of the assigment to load. * @return the single or aggregate assignment master for the given assignmentId. */ private static Asgnmt_master getAggregateOrSingleAssignmentMaster(GeneratedId assignmentId) { Asgnmt_master am = Asgnmt_master.load(assignmentId); if(am.isAggregate() || am.isSingle()) { return am; } //If it's not a single or aggregate, we need to load the aggregate and load that. return Asgnmt_master.load(am.getAggregate_asgnmt()); } AggregateTimeEntryTransactionResults load() { return loadImpl(); } /** * Reverts any changes that have not been committed to the database on this AssignmentManager */ void revert() { updatedAllCalcDataManager = null; } /** * @return Return initial transaction results from originalAllCalcData, which must be set before calling this. */ private AggregateTimeEntryTransactionResults loadImpl() { AggregateTimeEntryTransactionResults aggregateResults = (AggregateTimeEntryTransactionResults) parms.getTimeEntryResultsFactory().newInstance(); for (TimeSheetIdentifier timeSheetId : getTimeSheetIdentifiers()) { AllCalculationData acd = getOriginalAllCalcData(timeSheetId); // Make sure system_record_id's are assigned - TimeEntryManager and client do not function correctly without assignSystemRecordIds(acd); CalcInfo calcInfo = new CalcInfo(getOriginalAllCalcDataManager(timeSheetId.getAsgnmt()), acd, timeSheetId, parms); //todo-ute-nazim do we really need to perform TimeSheetDiff for the initial load ? TimeEntryTransactionResults singleTimeSheetResults = new TimeSheetDiff(timeSheetId, acd, calcInfo, TimeSheetCounters.getEmptyCounters(), NewToOldIdMap.EMPTY, Collections.EMPTY_LIST, new StatusMapsForTimesheetAndSchedule(), true, parms, null); aggregateResults.add(singleTimeSheetResults); } return aggregateResults; } /** * Assign system_record_id's for time_sheet, time_sheet_detail's and time_sheet_exception's * @param acd */ private void assignSystemRecordIds(AllCalculationData acd) { assignSystemRecordId(acd.getTime_sheet()); assignSystemRecordIds(acd.getTime_sheet().getTime_sheet_detailList()); assignSystemRecordIds(acd.getTime_sheet().getTime_sheet_exceptionList()); } /** * todo: store the event.getComment() * @param event */ AggregateTimeEntryTransactionResults amend(TransactionApprovalEvent event) { TimeSheetIdentifier singleTimeSheetId = event.getTimeSheetIdentifier(); //TODO: Shouldn't this call createOriginalAllCalcDataManagerIfNeeded? Seems like we needlessly reload the original //TODO: ACDM. This is because a timesheet which was prepared for amendment couldn't have been modified before it //TODO: was amended. invalidateAllCalcDataManagers(); // Create the amended ACD and put into the ACDM cache EmployeePeriodInfo epInfo = null; try { AllCalcDataManager acdm = getOriginalAllCalcDataManager(singleTimeSheetId.getAsgnmt()); epInfo = acdm.getEmployeePeriodInfo(singleTimeSheetId.getPpEnd()); AllCalculationData amendedAllCalcData = acdm.createAmendedAllCalcData(epInfo, parms.getApp_user(), WDateTime.now()); // No need to hook this up to approval change logic, since it is an in-memory change so far. //Recalculate the amended ACD object so that timesheet exceptions that are on the closed version (if any) // get created on the amended timesheet as well amendedAllCalcData.recalc(acdm); } catch (SQLException e) { throw new InternalApplicationException("amending time sheet for " + singleTimeSheetId, e); } catch (MultipleRowDbRecException e) { throw new InternalApplicationException("amending time sheet for " + singleTimeSheetId, e); } catch (Exception e) { throw new InternalApplicationException("amending time sheet for " + singleTimeSheetId, e); } return loadImpl(); } TimeSheetTransactionApplier applyTransaction(TimeSheetIdentifier id, TimeEntryTransaction trans) { AllCalcDataManager updatedAcdm = getUpdatedAllCalcDataManager(id.getAsgnmt()); AllCalculationData acd = getAllCalcData(updatedAcdm, id); return new TimeSheetTransactionApplier(trans, updatedAcdm, acd, parms, id); } private void recalc(TimeSheetIdentifier timeSheetId, Approval_event_type approvalEventType) { AllCalcDataManager updatedAcdm = getUpdatedAllCalcDataManager(timeSheetId.getAsgnmt()); AllCalculationData acd = getAllCalcData(updatedAcdm, timeSheetId); try { if (approvalEventType == Approval_event_type.SAVE_SCHEDULE) { // if we're saving a change to the schedule, the timesheet might need to be re-initialized. acd.reinitTimeSheet(updatedAcdm, true); } acd.recalc(updatedAcdm); } catch (Exception e) { throw new InternalApplicationException("calculate for " + timeSheetId, e); } } /** * Save one timesheet to a ListWriter using AllCalculationData. * @param timeSheetId * @param saveEvent * @throws Exception */ void save(TimeSheetIdentifier timeSheetId, Approval_event_type saveEvent) throws Exception { GeneratedId asgnmtId = timeSheetId.getAsgnmt(); AllCalcDataManager acdMgr = getUpdatedAllCalcDataManager(asgnmtId); AllCalculationData acd = getAllCalcData(acdMgr, timeSheetId); // TODO: it'd be nice if we were able to retry a save if it failed because of unrelated changes (to accomodate for // concurrent save operations on other component timesheets besides the one that the user is trying to save) final ListWriter lw = new ListWriter(); acd.saveAfterApplyingApprovalEvent(acdMgr, saveEvent, lw); //The exceptions must be generated after the ACD has performed approval related operations. // Loading the original ACD to get the timesheet object before the approval. This will be used to determine // timesheet's approval level before this save event. AllCalculationData originalAcd = getAllCalcData(getOriginalAllCalcDataManager(asgnmtId), timeSheetId); acdMgr.generateExceptionNotifications(acd, originalAcd.getTime_sheet(), acd.getTime_sheet(), lw); lw.writeLists(); } /** * If time sheets were saved and written to the database successfully, update internal data * to reflect what's in the database as the new "original" data. */ void postSave() { originalAllCalcDataManager = updatedAllCalcDataManager; updatedAllCalcDataManager = null; } /** * Returns true if the given timesheet contains exceptions that should prevent save. * @param timeSheetId time sheet ID * @return true if exceptions exist that should prevent save, false otherwise. */ boolean hasExceptionsPreventingSave(TimeSheetIdentifier timeSheetId) { AllCalculationData acd = getUpdatedAllCalcData(timeSheetId); try { return acd.getTime_sheet().getTime_sheet_exceptionList().getDisallow_timesheet_save(acd.getAllPolicies()); } catch (PolicyLookupRequiredException e) { throw new InternalApplicationException("Policy error on timesheet " + timeSheetId); } } /** * Returns true if the given timesheet contains exceptions that should prevent submit. * @param timeSheetId time sheet ID * @return true if exceptions exist that should prevent submit, false otherwise. */ boolean hasExceptionsPreventingSubmit(TimeSheetIdentifier timeSheetId) { AllCalculationData acd = getUpdatedAllCalcData(timeSheetId); try { return acd.getTime_sheet().getTime_sheet_exceptionList().getDisallow_timesheet_submit(acd.getAllPolicies()); } catch (PolicyLookupRequiredException e) { throw new InternalApplicationException("Policy error on timesheet " + timeSheetId); } } /** * Compute {@link TimeEntryTransactionResults} changes to be returned in client. * @param timeSheetId * @param trans transaction which client just sent * @param applyResults results (errors) of applying the transaction * @param timeSheetSavedStatus a status to be added representing the save results - SAVED or ERROR, or null if none. * @param transactionSuccessful value for {@link TimeEntryTransactionResults#getTransactionWasSuccessful}. */ TimeSheetDiff getDiff(TimeSheetIdentifier timeSheetId, TimeEntryTransaction trans, TimeSheetTransactionApplier applyResults, TransactionStatus timeSheetSavedStatus, boolean transactionSuccessful) { AllCalcDataManager acdm = getLatestAllCalcDataManager(timeSheetId.getAsgnmt()); AllCalculationData acd = getAllCalcData(acdm, timeSheetId); NewToOldIdMap newToOldIdMap = applyResults.getNewToOldIdMap(); StatusMapsForTimesheetAndSchedule timeSheetRowStatusMap = applyResults.getTimeEntryRowErrorMap(); if (cat.isDebugEnabled()) { ObjectDumper.debug(cat, "TimeSheetDiff getDiff before", timeSheetRowStatusMap, null, true); } List<TransactionStatus> timeSheetStatuses = new ArrayList<TransactionStatus>(applyResults.getTimeSheetErrors()); TimeSheetCounters counters = new TimeSheetCounters(trans, timeSheetId, newToOldIdMap); if (timeSheetSavedStatus != null) { timeSheetStatuses.add(timeSheetSavedStatus); if (timeSheetSavedStatus.getTransaction_status() == Transaction_status.SAVED || timeSheetSavedStatus.getTransaction_status() == Transaction_status.ERROR) { // if we saved or there is an error mark every TimeEntryRow in the transaction as such. // get all row id's from the transaction (new id's - not temp id's) Collection transTimeSheetRowIds = applyResults.getTransactionTimeSheetRowIds(); Collection transScheduleRowIds = applyResults.getTransactionScheduleRowIds(); timeSheetRowStatusMap = new StatusMapsForTimesheetAndSchedule(transTimeSheetRowIds, transScheduleRowIds, timeSheetSavedStatus); timeSheetRowStatusMap.addAll(applyResults.getTimeEntryRowErrorMap()); } } if (cat.isDebugEnabled()) { ObjectDumper.debug(cat, "TimeSheetDiff getDiff after", timeSheetRowStatusMap, null, true); } CalcInfo calcInfo = new CalcInfo(acdm, acd, timeSheetId, parms); return new TimeSheetDiff(timeSheetId, acd, calcInfo, counters, newToOldIdMap, timeSheetStatuses, timeSheetRowStatusMap, transactionSuccessful, parms, trans); } TimeSheetCalculationInfo getCalculationInfo(TimeSheetIdentifier timeSheetIdentifier) { validate(timeSheetIdentifier); final AllCalcDataManager acdm = getLatestAllCalcDataManager(timeSheetIdentifier.getAsgnmt()); return new CalcInfo(acdm, getAllCalcData(acdm, timeSheetIdentifier), timeSheetIdentifier, parms); } /** * Return the calculation info without auto-amending * @param acdm * @param timeSheetIdentifier * @return */ private TimeSheetCalculationInfo getCalculationInfo(AllCalcDataManager acdm, TimeSheetIdentifier timeSheetIdentifier) { validate(timeSheetIdentifier); try { return new CalcInfo(acdm, acdm.getAllCalcData(timeSheetIdentifier.getPpEnd(), timeSheetIdentifier.getVersion()), timeSheetIdentifier, parms); } catch (Exception e) { throw new InternalApplicationException("Error creating CalcInfo ", e);\s } } private void invalidateAllCalcDataManagers() { originalAllCalcDataManager = null; updatedAllCalcDataManager = null; } private void createUpdatedAllCalcDataManager(Set<TimeSheetIdentifier> timeSheetIds) { AllCalcDataManager origAcdMgr = getOriginalAllCalcDataManager(asgnmtMaster.getAsgnmt()); try { WDateList dateList = new WDateList(); for(TimeSheetIdentifier id : timeSheetIds) { dateList.add(id.getPpEnd()); } updatedAllCalcDataManager = origAcdMgr.copyForUpdate(dateList); } catch (Exception e) { throw new InternalApplicationException("Cloning for " + timeSheetIds, e); } } private void validate(TimeSheetIdentifier timeSheetId) throws IllegalArgumentException { if (!timeSheetIdentifiers.contains(timeSheetId)) { throw new IllegalArgumentException("Invalid time sheet specified: " + timeSheetId + ". not in " + timeSheetIdentifiers); } } /** * Obtains the original AllCalcDataManager for the given assignment Id. The assignment id must be given because in a * multiple assignment environment, a specific component needs to be requested, as originalAllCalcDataManager refers * to the aggregate assignment. * * @param asgnmtId The id of the assignment to load. * @return the original AllCalcDataManager for the given assignment Id */ private AllCalcDataManager getOriginalAllCalcDataManager(GeneratedId asgnmtId) { if(originalAllCalcDataManager == null) { originalAllCalcDataManager = createOriginalAllCalcDataManager(); updatedAllCalcDataManager = null; } if(asgnmtId.equals(originalAllCalcDataManager.getAsgnmtId())) { return originalAllCalcDataManager; } return originalAllCalcDataManager.getAllCalcDataManager(asgnmtId); } private AllCalcDataManager createOriginalAllCalcDataManager() { AllPolicies allPolicies = PolicyManager.getInstance().getAllPolicies(); try { return AllCalcDataManager.createForAsgnmt(asgnmtMaster, allPolicies); } catch (Exception e) { throw new InternalApplicationException("Failed to load ACDM for assignment with id:"+asgnmtMaster.getAsgnmt(), e); } } /** * Obtains the updated AllCalcDataManager for the given assignment Id. The assignment id must be given because in a * multiple assignment environment, a specific component needs to be requested, as originalAllCalcDataManager refers * to the aggregate assignment. * * @param asgnmtId The id of the assignment to load. * @return the updated AllCalcDataManager for the given assignment Id */ private AllCalcDataManager getUpdatedAllCalcDataManager(GeneratedId asgnmtId) { if(updatedAllCalcDataManager == null) { throw new InternalApplicationException("Attempted to use the updated all calc data manager before it was created."); } if(asgnmtId.equals(updatedAllCalcDataManager.getAsgnmtId())) { return updatedAllCalcDataManager; } return updatedAllCalcDataManager.getAllCalcDataManager(asgnmtId); } /** * Obtains the updated AllCalcDataManager for the given assignment Id, if present. If not present, returns the * original ACDM instead. The assignment id must be given because in a * multiple assignment environment, a specific component needs to be requested, as originalAllCalcDataManager refers * to the aggregate assignment. * * @param asgnmtId The id of the assignment to load. * @return he updated AllCalcDataManager for the given assignment Id, if present. If not present, returns the * original ACDM instead. */ AllCalcDataManager getLatestAllCalcDataManager(GeneratedId asgnmtId) { if (updatedAllCalcDataManager != null) { return getUpdatedAllCalcDataManager(asgnmtId); } return getOriginalAllCalcDataManager(asgnmtId); } //todo-ute: the method name is misleading as it may also create a new amended ACD if period is amendable by system on user's behalf private AllCalculationData getAllCalcData(AllCalcDataManager mgr, TimeSheetIdentifier id) { try { EmployeePeriodInfo epInfo = mgr.getEmployeePeriodInfo(id.getPpEnd()); if (epInfo == null) { final PolicyID asgnmtPolicyProfile = mgr.getAsgnmtMaster().getPolicy_profile(id.getPpEnd()); final PolicyID policyProfileId = (asgnmtPolicyProfile == null) ? PolicyID.EMPTY_POLICYID : asgnmtPolicyProfile; throw new InvalidPayPeriodException("EmployeePeriodInfo could not be obtained for employee " + id.getEmployee() + " and assignment " + id.getAsgnmt() + " and period end - " + id.getPpEnd() + ". " + "Possibly because the employee is not active on that date or there are not enough initialized policy profile " + "periods for policy profile " + policyProfileId ); } TimeSheetCalculationInfo calcInfo = this.getCalculationInfo(mgr, id); AssignmentPeriodState asgnmtPeriodState = calcInfo.getAssignmentPeriodState(); TimeSheetState timesheetState = calcInfo.getTimeSheetState(); //If requesting EDIT_VERSION, check to see if one exists or that period is amendable by the logged in user if (id.getVersion() == EmployeePeriodVersionInfo.EDIT_VERSION && !epInfo.hasEditVersion() && !isPeriodAmendable(epInfo, mgr, asgnmtPeriodState, timesheetState) ) { throw new InternalApplicationException("An editable timesheet could not be obtained for employee " + id.getEmployee() + " and period end - " + id.getPpEnd()); } //Check to see if an editable ACD is in ACD manager's cache - if one exists, use it //An empty unmodifiable ACD for a prior period might be in cache if an ACD for prior timesheet is requested and //if that's the case, we want to still be able to create an amended ACD final AllCalculationData acd = mgr.getAllCalcData(id.getPpEnd(),id.getVersion(),AllCalculationData.FETCH_CACHED_ONLY); if (acd != null && !acd.isUnmodifiable()) { return acd; } //Create an amended ACD only if the period is amendable and the logged in user can amend it if (isAmendableBySystemForUser(epInfo, mgr, asgnmtPeriodState)) { final AllCalculationData amendedAllCalcData = mgr.createAmendedAllCalcData(epInfo, parms.getApp_user(), WDateTime.now()); // No need to hook this up to approval change logic, since it is an in-memory change so far. //Recalculate the amended ACD so that timesheet exceptions (if any) get created on the amended timesheet amendedAllCalcData.recalc(mgr); } //By this time, the ACD mgr's ACD cache will have the amended ACD if one's created in this method //and we can retrieve the cached amended ACD or the ACD from the database return mgr.getAllCalcData(id.getPpEnd(), id.getVersion()); } catch(InvalidPayPeriodException e) { //Rethrow InvalidPeriodExceptions--don't wrap them. We want them to be typed as InvalidPeriodExceptions. throw e; } catch (Exception e) { throw new InternalApplicationException("Timesheet could not be obtained for employee " + id.getEmployee() + ", assignment " + id.getAsgnmt()+ ", period end " + id.getPpEnd() + " and version " + id.getVersion() + ".", e); } } /** * A period is amendable if one of the following is true: * <pre> * - {@link #isAmendableBySystemForUser(EmployeePeriodInfo, AllCalcDataManager, AssignmentPeriodState) * amendable by system on users's behalf} * - {@link TimeSheetState#isAmendable() amendable directly by user} generally by clicking "Amend" button on * prior active period with one or more closed timesheets * </pre> */ private static boolean isPeriodAmendable(EmployeePeriodInfo epInfo, AllCalcDataManager mgr, AssignmentPeriodState asgnmtPeriodState, TimeSheetState timesheetState) throws SQLException, MultipleRowDbRecException { return timesheetState.isAmendable() || isAmendableBySystemForUser(epInfo, mgr, asgnmtPeriodState); } /** * A period is amendable by system on user's behalf (not the same as AUTO_AMEND or SYS_AMEND which are also created by * system) if user has amend rights for the specified period as of today and if specified period is a priod period * with no timesheet or it's a prior period timesheet with just system amended but not user amended * @param epInfo * @param mgr * @param asgnmtPeriodState * @return * @throws PolicyLookupException * @throws MultipleRowDbRecException * @throws SQLException * @see AssignmentPeriodState#isPeriodAmendable()\s * @see Approval_event_type#SYS_AMEND * @see Approval_event_type#AMEND */ private static boolean isAmendableBySystemForUser(EmployeePeriodInfo epInfo, AllCalcDataManager mgr, AssignmentPeriodState asgnmtPeriodState) throws MultipleRowDbRecException, SQLException { return asgnmtPeriodState.isPeriodAmendable() && // TODO: this doesn't match the javadoc? ( mgr.isPriorModifiablePeriodWithNoTimesheet(epInfo) || epInfo.isPriorPeriodWithSysAmendVersionOnly() ); } private AllCalculationData getOriginalAllCalcData(TimeSheetIdentifier timeSheetIdentifier) { return getAllCalcData(getOriginalAllCalcDataManager(timeSheetIdentifier.getAsgnmt()), timeSheetIdentifier); } private AllCalculationData getUpdatedAllCalcData(TimeSheetIdentifier timeSheetIdentifier) { return getAllCalcData(getUpdatedAllCalcDataManager(timeSheetIdentifier.getAsgnmt()), timeSheetIdentifier); } private Set<TimeSheetIdentifier> getTimeSheetIdentifiers() { return timeSheetIdentifiers; } /** * Returns the pay period end for the period containing date provided, relative * to the defined periods for this assignment. * * @param aDateInUnknownPeriod any date we want to know the period for * @return never null */ WDate getPpEndForDate(GeneratedId asgnmtId, WDate aDateInUnknownPeriod) { return TimeSchedUtils.getPpEndForDate(getOriginalAllCalcDataManager(asgnmtId), aDateInUnknownPeriod); } /** * Assign system_record_id's to any DbRec objecs in the collection which lack them. * @param dbRecList List of DbRec objects on which to check the system_record_id. Records in list will be modified. */ static private void assignSystemRecordIds(ListWrapBase dbRecList) { for (Iterator iterator = dbRecList.getCollection().iterator(); iterator.hasNext();) { DbRec dbRec = (DbRec) iterator.next(); assignSystemRecordId(dbRec); } } /** * Assign system_record_id to a DbRec object, if it is not set or contains a temporary id. * @param dbRec DbRec objects on which to check/change the system_record_id. */ static private void assignSystemRecordId(DbRec dbRec) { if (dbRec.getSystem_record_id() == null || dbRec.getSystem_record_id().requiresPermanentId()) { dbRec.setSystem_record_id(SystemId.getNewID()); } } /** * Removes the last approval event (employee approval) from approval event list from the AllCalculationData object of * {@link #updatedAllCalcDataManager} * @param timeSheetId - Used to get the {@link AllCalculationData} object from {@link #updatedAllCalcDataManager} */ void undoEmployeeApproval(TimeSheetIdentifier timeSheetId) { final AllCalculationData updatedAllCalcData = getUpdatedAllCalcData(timeSheetId); updatedAllCalcData.getApproval_eventList().removeLastApprovalEvent(); } /** * Update the current object to be able to be used for other timesheets for this assignment * @param parms * @param timeSheetIdentifiers */ public void update(TimeEntryParmsPerAssignment parms, Set<TimeSheetIdentifier> timeSheetIdentifiers) { setClassFields(parms, timeSheetIdentifiers); } private void setClassFields(TimeEntryParmsPerAssignment parms, Set<TimeSheetIdentifier> timeSheetIdentifiers) { this.parms = parms; this.timeSheetIdentifiers = TimeEntryParmsPerAssignment.TIMESHEET_IDENTIFIER_TREESET_FACTORY.newInstance(); this.timeSheetIdentifiers.addAll(timeSheetIdentifiers); } /** * Recalculates and Saves timesheets (if saveData == true) for timesheet ids in {@link #parms} that belong to this * assignment * * @param trans * @return AggregateTimeEntryTransactionResults for all saved timesheets * @throws Exception in case of fatal error during timesheet recalc & save e.g. errors while store data in DB etc. * This precludes ConcurrentUserModification exception. Any exception thrown from this method, eventually gets * escalated as transaction exception */ public TimeEntryTransactionResults recalcAndSaveTimesheets(TimeEntryTransaction trans, boolean saveData) throws Exception { // TODO-13285: Remove this code and implement merging concurrently modified time sheets AllCalcDataManager tempAcdMgr = null; AggregateTimeEntryTransactionResults results = (AggregateTimeEntryTransactionResults) parms.getTimeEntryResultsFactory().newInstance(); Set<TimeSheetIdentifier> timeSheetIds = getAffectedTimeSheetIdentifiers(trans, parms); int retryCount = 0; boolean canRetryConcurrentModification = true; if (timeSheetIds.isEmpty()) { return results; } assert timeSheetIdentifiers.containsAll(timeSheetIds) : "Attempted to recalculate a timesheet identifier not managed by this AssignmentManager."; createUpdatedAllCalcDataManager(timeSheetIds); TransactionStatus timeSheetSaveStatus = null; Map<TimeSheetIdentifier, TimeSheetTransactionApplier> applyResultsMap = new HashMap<TimeSheetIdentifier, TimeSheetTransactionApplier>(); for (TimeSheetIdentifier timeSheetId : timeSheetIds) { TimeSheetTransactionApplier applyResults = applyTransaction(timeSheetId, trans); if(applyResults.getApprovalEventType() != Approval_event_type.SAVE_SCHEDULE) { applyResultsMap.put(timeSheetId, applyResults); } recalc(timeSheetId, applyResults.getApprovalEventType()); boolean isWithdrawal = (trans.findTransactionApprovalEvent(timeSheetId, Approval_event_type.WITHDRAWAL) != null); if (!isWithdrawal) { // There are 3 conditions identified in the following if statement that can prevent an "action" on the // current timesheet. The first is an exception that prevents save. In this case, we should never allow a // save of the timesheet to occur. The second condition is an exception that prevents submit. In this // condition, we should only allow a save if the user is not attempting to submit (apply an APPROVAL event). // The third case is errors that occur while applying transactions to the timesheet. An example of when this // may occur is if the same time sheet detail row is modified by 2 users. In all of these cases we prevent // the save operation from executing. // In the next condition where we check if an error has occurred, we remove the last approval event from the // timesheet if we did not save AND it was a submit (APPROVAL) event. Transactions only allow 1 event to be applied // to the timesheet. The transaction explicitly excludes SAVE_TIME_SHEET and SAVE_SCHEDULE events from being applied. // It relies on these events being applied only by the save operation itself. As a result, a maximum of 2 events may // be added to the timesheet as a result of the transaction (the event on the transaction if it was not a save event) // and the SAVE_TIME_SHEET or SAVE_SCHEDULE event added during the save operation. if (hasSubmitEvent(trans, timeSheetId) && hasExceptionsPreventingSubmit(timeSheetId)) { timeSheetSaveStatus = getHighestPriorityError(timeSheetSaveStatus, Messages.EXCEPTIONS_PREVENT_SUBMIT); } if (hasExceptionsPreventingSave(timeSheetId)) { timeSheetSaveStatus = new TransactionStatusImpl(Transaction_status.ERROR, Messages.EXCEPTIONS_PREVENT_SAVE.getLabel()); } } if (applyResults.hasErrors()) { timeSheetSaveStatus = new TransactionStatusImpl(Transaction_status.ERROR, Messages.TIME_SHEET_EXCEPTION_JAVA_EXCEPTION .getLabel()); } } boolean errorPreventsAction = (timeSheetSaveStatus != null); Approval_event lastOriginalApprovalEvent = null; for (TimeSheetIdentifier timeSheetId : timeSheetIds) { TimeSheetTransactionApplier applyResults = applyResultsMap.get(timeSheetId); try { while (saveData && canRetryConcurrentModification) { if (errorPreventsAction) { // If the changes were not saved due to errors, and this operation (transaction) was an employee timesheet submission, // remove the approval event from the unsaved ACD. // NOTE: We do not have to concern ourselves with removing SAVE_TIME_SHEET or SAVE_SCHEDULE events here as these events // are explicitly EXCLUDED in the TimeSheetTransactionApplier (not applied) and are applied by the save operation (which // has not executed if we made it here. if (hasSubmitEvent(trans, timeSheetId)) { undoEmployeeApproval(timeSheetId); } } else { save(timeSheetId, applyResults.getApprovalEventType()); timeSheetSaveStatus = new TransactionStatusImpl(Transaction_status.SAVED, Messages.TIME_SHEET_SAVED.getLabel()); postSave(); } // All operations completed, so retries are no longer necessary canRetryConcurrentModification = false; } } catch (ConcurrentUserModificationException ex) { // Store the old AcdMgr, when a concurrent mod error due to another user modifying a time sheet occurs we want // to receive the same new approval events to ensure that the user is forced to reload the time sheet to continue. // TODO-13285: Remove the tempAcdMgr and make merging concurrently modified time sheets possible, // TODO-13285: requires considering multiple edge cases for the user interface if (tempAcdMgr == null) { tempAcdMgr = getOriginalAllCalcDataManager(asgnmtMaster.getAsgnmt()); } if (retryCount == MAX_RETRIES) { // return the acdMgr to its previous state, this ensures the user must reload the time sheet // TODO-13285: Remove this code and implement merging concurrently modified time sheets originalAllCalcDataManager = tempAcdMgr; GeneratedId errorId = ServerErrorLogger.singleton.log(new ServerError("Exception saving time sheet", ex, Program_source.SERVER_REQUEST, parms.getApp_user().getLogin_id())); cat.error("Unable to save time sheet. debug_error_log id:" + errorId, ex); //Use timeSheetSaveStatus to send concurrent error message to user. errorPreventsAction = true; timeSheetSaveStatus = new TransactionStatusImpl(Transaction_status.ERROR, Messages.TIME_SHEET_EXCEPTION_CONCURRENT_USER.getLabel()); // We have retried saving and have hit the MAX_RETRIES threshold, so stop retrying. canRetryConcurrentModification = false; } else { // On the first retry, we need to capture the last (most recent) approval event that existed on this timesheet prior to applying // any transactions because when we reload the "original" ACD to attempt to reapply the transactions we will lose track // of this (we'll pull in approval events for transactions that were saved in other sessions/processes). This is necessary to // allow more than 1 retry if (lastOriginalApprovalEvent == null) { lastOriginalApprovalEvent = getAllCalcData(getOriginalAllCalcDataManager(timeSheetId.getAsgnmt()), timeSheetId).getApproval_eventList().getLastApprovalEvent(); } retryCount++; // attempt to reapply the transaction try { applyResults = attemptTimeSheetTransactionReapply(trans, timeSheetId, lastOriginalApprovalEvent); } catch (ConcurrentUserModificationException cume) { // return the acdMgr to its previous state, this ensures the user must reload the time sheet // TODO-13285: Remove this code and implement merging concurrently modified time sheets originalAllCalcDataManager = tempAcdMgr; // if we were unable to reapply transactions, no further retries are necessary (because it will just keep failing) // subsequent retries only succeed if we were able to reapply the new transactions successfully and still received // another ConcurrentUserModificationException (a second change occurred while applying transactions) canRetryConcurrentModification = false; errorPreventsAction = true; timeSheetSaveStatus = new TransactionStatusImpl(Transaction_status.ERROR, Messages.TIME_SHEET_EXCEPTION_CONCURRENT_USER.getLabel()); } } } results.add(getDiff(timeSheetId, trans, applyResults, timeSheetSaveStatus, !errorPreventsAction)); } return results; } """; @NonNls String part2 = """ private TransactionStatus getHighestPriorityError(TransactionStatus existingStatus, Message newError) { if(existingStatus == null) { return newError; } TransactionStatus timeSheetSaveStatus = new TransactionStatusImpl(Transaction_status.ERROR, ); return timeSheetSaveStatus; } /** * Returns true if the given TimeEntryTransaction has a submit event for the given time sheet. * @param trans TimeEntryTransaction to search * @param timeSheetId time sheet to search * @return true if a submit event exists */ private static boolean hasSubmitEvent(TimeEntryTransaction trans, TimeSheetIdentifier timeSheetId) { return trans.findTransactionApprovalEvent(timeSheetId, Approval_event_type.APPROVAL) != null; } /** * Resolves modifications that impact only non-"input" values on the timesheet to prevent them * from causing ConcurrentUserModificationExceptions on subsequent attempts at saving the data * @param trans time entry transaction to resolve * @param id time sheet ID * @param lastApprovalEvent the most recent approval event on the timesheet prior to any transactions * @return TimeSheetTransactionApplier if transactions were reapplied * @throws Exception on error loading the acd */ private TimeSheetTransactionApplier attemptTimeSheetTransactionReapply(final TimeEntryTransaction trans, final TimeSheetIdentifier id, Approval_event lastApprovalEvent) throws Exception { // Clean out the ACDM cache to force reloads of the data from the database invalidateAllCalcDataManagers(); AllCalculationData originalAcd = getAllCalcData(getOriginalAllCalcDataManager(id.getAsgnmt()), id); // Get any new approval events that exist in the database Approval_eventList newApprovalEvents = originalAcd.getApproval_eventList().getApprovalsSince(lastApprovalEvent); // Check if a "concurrent save" is possible given the events that have occurred if (newApprovalEvents.canConcurrentSave()) { //TODO:FIX //createUpdatedAllCalcDataManager(listOfAllIdsHere); TimeSheetTransactionApplier applyResults = applyTransaction(id, trans); // reapply transactions recalc(id, applyResults.getApprovalEventType()); // recalculate DbRecTime_sheet originalTimeSheet = originalAcd.getTime_sheet(); DbRecTime_sheet updatedTimeSheet = getAllCalcData(getUpdatedAllCalcDataManager(id.getAsgnmt()), id).getTime_sheet(); // Copy the system fields from the original time sheet to the updated time sheet because // we have determined at this point that the concurrent save is okay based on the difference // between these 2 time sheets. Copying these fields over will allow the save to complete // without causing a ConcurrentUserModificationException by making the update counter match. DbRecFieldCopier copier = new DbRecFieldCopier(DataDictionary.getCltnOfSystemFieldNames()); copier.copyFields(originalTimeSheet, updatedTimeSheet); return applyResults; } else { // Otherwise, throw the exception throw new ConcurrentUserModificationException("Unable to save time sheet for assignment " + originalAcd.getAsgnmtId() + "due to concurrent changes."); } } /** * Returns a set of AssignmentManagers that are affected by changes in the given transaction and managed by this * TimeEntryManager. * * @return a set of AssignmentManagers that are affected by changes in the given transaction and managed by this * TimeEntryManager. */ public Set<TimeSheetIdentifier> getAffectedTimeSheetIdentifiers(TimeEntryTransaction transaction, TimeEntryParmsPerAssignment parms) { Set<TimeSheetIdentifier> affectedTimeSheetIds = new HashSet<TimeSheetIdentifier>(); for(GeneratedId assignmentId : getAssignmentIds()) { for(TimeSheetIdentifier timeSheetId : parms.getTimeSheetIdentifiersForAssignment(assignmentId)) { if(!transaction.obtainAllRows(timeSheetId).isEmpty()) { affectedTimeSheetIds.add(timeSheetId); } } } return affectedTimeSheetIds; } /** * Obtains all assignment id's that are managed by this AssignmentManager. * * @return all assignment id's that are managed by this AssignmentManager. */ public Set<GeneratedId> getAssignmentIds() { Set<GeneratedId> assignmentIds = new HashSet<GeneratedId>(); Asgnmt_masterList amList = asgnmtMaster.getCompAsgnmtMasters(); for(Asgnmt_master am : amList) { assignmentIds.add(am.getAsgnmt()); } return assignmentIds; } public GeneratedId getEmployee() { return asgnmtMaster.getEmployee(); } /** Single or aggregate assignment master associated with this manager */ private final Asgnmt_master asgnmtMaster; private TimeEntryParmsPerAssignment parms; /** Set of {@link TimeSheetIdentifier}'s which are managed by this object. * Composed of all the identifiers in the TimeEntryParms for this assignment and related components. */ private Set<TimeSheetIdentifier> timeSheetIdentifiers; /** * Original Single or Aggregate ACDM, as loaded from the database. ACD's in here are not modified. * This ACDM is lazily loaded, and should always be obtained from {@link #getOriginalAllCalcDataManager(GeneratedId)}. * * Null means that the ACDM has not yet been loaded from the database, and any attempts to use it should first * initialize it. Note: The meaning of null is different than updatedAllCalcDataManager's null meaning. */ private AllCalcDataManager originalAllCalcDataManager = null; /** * Updated Single or Aggregate ACDM--a shallow copy of originalAllCalcDataManager. ACD's in here are modified by * TimeEntryTransactions. This ACDM is NOT lazily loaded--null means no changes present, and non-null means the * updated ACDM has been explicitly created. * * Null means that no changes have been made to the ACDM or data, and any attempts to use it should either throw a * descriptive error, or explicitly create the updatedAllCalcDataManager. * Note: The meaning of null is different than originalAllCalcDataManager's null meaning. */ private AllCalcDataManager updatedAllCalcDataManager = null; private static final int MAX_RETRIES = 2; private static final Category cat=Category.getInstance(AssignmentManager.class.getName()); }"""; configureFromFileText("Foo.java", part1 + part2); final PsiDocumentManager docManager = PsiDocumentManager.getInstance(getProject()); final Document doc = docManager.getDocument(getFile()); WriteCommandAction.runWriteCommandAction(getProject(), () -> doc.insertString(part1.length(), "/**")); boolean old = DebugUtil.CHECK; DebugUtil.CHECK = true; try { docManager.commitAllDocuments(); } finally { DebugUtil.CHECK = old; } }