From 29ebadc835ea92514eba5a55f1a72a39efe8127d Mon Sep 17 00:00:00 2001 From: "MSI\\2Fi" Date: Fri, 26 Jul 2024 14:14:42 +0800 Subject: [PATCH] Update PnL Report to show Salary Effective info --- .../entity/projections/SalaryEffectiveInfo.kt | 3 + .../data/service/SalaryEffectiveService.kt | 27 ++- .../modules/report/service/ReportService.kt | 191 +++++++++++++++--- .../report/AR07_Project P&L Report v02.xlsx | Bin 12815 -> 12589 bytes 4 files changed, 187 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/projections/SalaryEffectiveInfo.kt b/src/main/java/com/ffii/tsms/modules/data/entity/projections/SalaryEffectiveInfo.kt index a828556..857b0f3 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/projections/SalaryEffectiveInfo.kt +++ b/src/main/java/com/ffii/tsms/modules/data/entity/projections/SalaryEffectiveInfo.kt @@ -5,11 +5,14 @@ import org.springframework.beans.factory.annotation.Value import java.time.LocalDate interface SalaryEffectiveInfo { + val id: Long? val date: LocalDate val salary: Salary @get:Value("#{target.staff?.name}") val name: String? + @get:Value("#{target.staff?.id}") + val idInStaff: Long @get:Value("#{target.staff?.staffId}") val staffId: String? @get:Value("#{target.staff?.grade?.name}") diff --git a/src/main/java/com/ffii/tsms/modules/data/service/SalaryEffectiveService.kt b/src/main/java/com/ffii/tsms/modules/data/service/SalaryEffectiveService.kt index f78fd42..4ab6e32 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/SalaryEffectiveService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/SalaryEffectiveService.kt @@ -2,11 +2,13 @@ package com.ffii.tsms.modules.data.service import com.ffii.core.support.AbstractIdEntityService import com.ffii.core.support.JdbcDao +import com.ffii.tsms.modules.data.entity.Salary import com.ffii.tsms.modules.data.entity.SalaryEffective import com.ffii.tsms.modules.data.entity.SalaryEffectiveRepository import com.ffii.tsms.modules.data.entity.SalaryRepository import com.ffii.tsms.modules.data.entity.StaffRepository import org.springframework.stereotype.Service +import java.math.BigDecimal import java.time.LocalDate @Service @@ -14,7 +16,7 @@ open class SalaryEffectiveService( private val salaryEffectiveRepository: SalaryEffectiveRepository, private val staffRepository: StaffRepository, private val salaryRepository: SalaryRepository, - private val jdbcDao: JdbcDao, + jdbcDao: JdbcDao, ) : AbstractIdEntityService(jdbcDao, salaryEffectiveRepository) { // open fun combo(args: Map): List> { @@ -63,4 +65,27 @@ open class SalaryEffectiveService( return salaryEffective } + + data class SalaryData(val idInStaff: Long, val staffId: String, val financialYear: LocalDate, val hourlyRate: BigDecimal, val salaryPoint: Int) + data class StaffSalaryData(val staffId: String, val salaryData: List) + + open fun getStaffSalaryDataByProjectId(projectId: Long): List { + + val sql = StringBuilder( + " select distinct t.staffId from timesheet t where t.projectId = :projectId " + ) + val staffIdList = jdbcDao.queryForList(sql.toString(), mapOf("projectId" to projectId)) + + val temp = staffIdList.mapNotNull{ + it["staffId"].toString().toLong() + } + + val salaryEffectiveLists = salaryEffectiveRepository.findSalaryEffectiveInfoByStaffIdInOrderByStaffId(temp) + .map { SalaryData(it.idInStaff, it.staffId!!, it.date, it.salary.hourlyRate, it.salary.salaryPoint) } + .groupBy { it.staffId } + .map { (staffId, salaryData) -> StaffSalaryData(staffId, salaryData.sortedBy { it.financialYear }) } + +// println(salaryEffectiveLists) + return salaryEffectiveLists + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt index 356575c..9010c7b 100644 --- a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt +++ b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt @@ -5,8 +5,11 @@ import com.ffii.core.utils.ExcelUtils import com.ffii.tsms.modules.data.entity.Customer import com.ffii.tsms.modules.data.entity.Grade import com.ffii.tsms.modules.data.entity.Salary +import com.ffii.tsms.modules.data.entity.SalaryEffective +import com.ffii.tsms.modules.data.entity.SalaryEffectiveRepository import com.ffii.tsms.modules.data.entity.Staff import com.ffii.tsms.modules.data.entity.Team +import com.ffii.tsms.modules.data.service.SalaryEffectiveService import com.ffii.tsms.modules.project.entity.Invoice import com.ffii.tsms.modules.project.entity.Milestone import com.ffii.tsms.modules.project.entity.Project @@ -37,6 +40,7 @@ import java.time.ZoneId import java.time.format.DateTimeParseException import java.time.temporal.ChronoUnit import java.util.* +import kotlin.collections.ArrayList data class DayInfo(val date: String?, val weekday: String?) @@ -45,7 +49,8 @@ data class DayInfo(val date: String?, val weekday: String?) @Service open class ReportService( private val jdbcDao: JdbcDao, - private val projectRepository: ProjectRepository + private val projectRepository: ProjectRepository, + private val salaryEffectiveService: SalaryEffectiveService ) { private val logger: Log = LogFactory.getLog(javaClass) private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd") @@ -2121,6 +2126,37 @@ open class ReportService( return jdbcDao.queryForList(sql.toString(), args) } + fun getSalaryForMonth(recordDate: String, staffId: String, staffSalaryLists: List): Double? { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM") + val monthDate = YearMonth.parse(recordDate, formatter) + + var hourlyRate: Double? = null +// var financialYear = monthDate + + val staffSalaryData = staffSalaryLists.find { it.staffId == staffId } + staffSalaryData?.salaryData?.forEach { salaryData -> + val financialYearStart = YearMonth.of(salaryData.financialYear.year, 10) + val financialYearEnd = YearMonth.of(salaryData.financialYear.year + 1, 9) + + if ((monthDate.isAfter(financialYearStart) || monthDate.equals(financialYearStart)) && (monthDate.isBefore(financialYearEnd) || monthDate.equals(financialYearEnd))) { +// if(staffId == "B627"){ +// println("----------------- Start -------------------------") +// println("financialYearStart: ${financialYearStart}") +// println("financialYearEnd: ${financialYearEnd}") +// println("monthDate: ${monthDate}") +// println("Result: ${(monthDate.isAfter(financialYearStart) || monthDate.equals(financialYearStart)) && monthDate.isBefore(financialYearEnd)}") +// println("hourlyRate: ${salaryData.hourlyRate.toDouble()}") +// println("---------------------- End -----------------------------") +// } + hourlyRate = salaryData.hourlyRate.toDouble() +// financialYear = YearMonth.parse(salaryData.financialYear.toString()) + return@forEach + } + } + + return hourlyRate + } + open fun getManhoursSpent(projectId: Long, startMonth: String, endMonth: String): List> { val sql = StringBuilder( " with cte_timesheet as (" @@ -2189,16 +2225,14 @@ open class ReportService( "endMonth" to queryEndMonth.toString(), ) + // All recorded man hours of the project val manHoursSpent = jdbcDao.queryForList(sql.toString(), args) + val projectCodeSql = StringBuilder( "select p.code, p.description from project p where p.deleted = false and p.id = :projectId" ) - val args2 = mapOf( - "projectId" to projectId, - ) - val projectsCode = jdbcDao.queryForMap(projectCodeSql.toString(), args).get() val otFactor = BigDecimal(1).toDouble() @@ -2212,16 +2246,11 @@ open class ReportService( "code" to projectsCode["code"], "description" to projectsCode["description"] ) - val financialYears = getFinancialYearDates(queryStartMonth, queryEndMonth, 10) val staffInfoList = mutableListOf>() -// println("manHoursSpent------- ${manHoursSpent}") - for (financialYear in financialYears) { -// println("${financialYear.start.year}-${financialYear.start.monthValue} - ${financialYear.end.year}-${financialYear.end.monthValue}") - println("financialYear--------- ${financialYear.start} - ${financialYear.end}") - } + val staffSalaryLists = salaryEffectiveService.getStaffSalaryDataByProjectId(projectId) for (item in manHoursSpent) { if (info["teamLead"] != item.getValue("teamLead")) { @@ -2248,8 +2277,18 @@ open class ReportService( if (info["description"] != item.getValue("description")) { info["description"] = item.getValue("description") } - val hourlyRate = (item.getValue("hourlyRate") as BigDecimal).toDouble() - val recordDate = item.getValue("recordDate") as String + +// val stadd = item.getValue("staffId") as String +// println("StaffId: ${stadd}") +// println( +// "HourlyRate: " + +// "${ +// getSalaryForMonth( +// item.getValue("recordDate") as String, +// item.getValue("staffId") as String, +// staffSalaryLists +// )}") + val hourlyRate = getSalaryForMonth(item.getValue("recordDate") as String, item.getValue("staffId") as String, staffSalaryLists) ?: (item.getValue("hourlyRate") as BigDecimal).toDouble() if (!staffInfoList.any { it["staffId"] == item["staffId"] && it["name"] == item["name"] }) { @@ -2312,14 +2351,47 @@ open class ReportService( staffInfoList[updatedIndex] = updatedMap } } -// println("staffInfoList----------------- $staffInfoList") +// println("staffInfoList----------------- ${staffInfoList.filter { it["staffId"] == "B627" }}") // println("info----------------- $info") + staffInfoList.forEachIndexed { index, staff -> + val staffId = staff["staffId"] + val staffSalaryData = staffSalaryLists.find { it.staffId == staffId } +// println("----------------------- $staffId ---------------------------") +// println(staff) + + val financialYears = (staff["financialYears"] as List).map { year -> + val relevantSalaryData = staffSalaryData?.salaryData?.lastOrNull { + year.isYearMonthInFinancialYear(YearMonth.from(it.financialYear)) + } +// println("it.financialYear: ${relevantSalaryData?.financialYear}") +// println("year: ${year.start}- ${year.end}") +// println("it.salaryPoint: ${relevantSalaryData?.salaryPoint}") +// println("it.staffId: ${relevantSalaryData?.staffId}") + + // Create a new FinancialYear object with the updated salaryPoint + year.copy( + start = year.start, + end = year.end, + salaryPoint = relevantSalaryData?.salaryPoint ?: staff["salaryPoint"] as Int, + hourlyRate = relevantSalaryData?.hourlyRate?.toDouble() ?: staff["hourlyRate"] as Double + ) + } + + // Create a new map with the updated financialYears + val updatedStaff = staff.toMutableMap().apply { + this["financialYears"] = financialYears + } + + // Update the staffInfoList with the new staff map + staffInfoList[index] = updatedStaff + } + tempList.add(mapOf("info" to info)) tempList.add(mapOf("staffInfoList" to staffInfoList)) // println("Only Staff Info List --------------------- ${tempList.first() { it.containsKey("staffInfoList") }["staffInfoList"]}") - println("tempList----------------- $tempList") +// println("tempList----------------- ${tempList.filter { it["staffId"] == "B627" }}") return tempList } @@ -2340,13 +2412,21 @@ open class ReportService( } // For Calculating the Financial Year - data class FinancialYear(val start: YearMonth, val end: YearMonth, val hourlyRate: Double) + data class FinancialYear(val start: YearMonth, val end: YearMonth, var hourlyRate: Double, var salaryPoint: Int){ + fun isYearMonthInFinancialYear(salaryEffectiveMonth: YearMonth) :Boolean{ + if ((salaryEffectiveMonth.isAfter(start) || salaryEffectiveMonth.equals(start)) && + (salaryEffectiveMonth.isBefore(end) || salaryEffectiveMonth.equals(end))){ + return true + } + return false + } + } fun getFinancialYearDates( startDate: YearMonth, endDate: YearMonth, financialYearStartMonth: Int - ): Array { + ): List { val financialYearDates = mutableListOf() var currentYear = startDate.year @@ -2375,7 +2455,8 @@ open class ReportService( val financialYear = FinancialYear( start = YearMonth.of(startYear, startMonth), end = YearMonth.of(endYear, endMonth), - hourlyRate = 0.0 + hourlyRate = 0.0, + salaryPoint = 0 ) financialYearDates.add(financialYear) @@ -2386,9 +2467,10 @@ open class ReportService( } } - return financialYearDates.toTypedArray() + return financialYearDates } + fun createPandLReportWorkbook( templatePath: String, manhoursSpent: List>, @@ -2422,6 +2504,7 @@ open class ReportService( val convertEndMonth = YearMonth.of(endDate.year, endDate.month) val monthRange: MutableList> = ArrayList() + val financialYears = getFinancialYearDates(queryStartMonth, queryEndMonth, 10) var currentDate = startDate if (currentDate == endDate) { @@ -2520,12 +2603,35 @@ open class ReportService( } row6Cell.setCellValue(clientSubsidiary) + // Average Hourly Rate by Pay Scale Point + rowNum = 8 + val row8: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) + sheet.addMergedRegion(CellRangeAddress(rowNum, rowNum, 2, (financialYears.size+1)*2-1)) + val row8Cell = row8.getCell(2) ?: row8.createCell(2) + row8Cell.apply { + setCellValue("Average Hourly Rate by Pay Scale Point") + } + CellUtil.setAlignment(row8Cell, HorizontalAlignment.CENTER); + CellUtil.setVerticalAlignment(row8Cell, VerticalAlignment.CENTER); + CellUtil.setCellStyleProperty(row8Cell, CellUtil.WRAP_TEXT, true) + + // Need to be updated to financial year // Base on the searching criteria rowNum = 9 - val row9: Row = sheet.getRow(rowNum) - val row9Cell = row9.getCell(3) - row9Cell.setCellValue("${convertStartMonth.format(monthFormat)} - ${convertEndMonth.format(monthFormat)}") + val row9: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) + var column = 2 + financialYears.indices.forEach { i -> + val row9Cell = row9.getCell(column) ?: row9.createCell(column) + val row9Cell2 = row9.getCell(column+1) ?: row9.createCell(column+1) + sheet.addMergedRegion(CellRangeAddress(rowNum, rowNum, column, column + 1)) + row9Cell.setCellValue("${financialYears[i].start.format(monthFormat)} - ${financialYears[i].end.format(monthFormat)}") + CellUtil.setCellStyleProperty(row9Cell, "borderBottom", BorderStyle.THIN) + CellUtil.setCellStyleProperty(row9Cell2, "borderBottom", BorderStyle.THIN) + column = column.plus(2) + } + + rowNum = 10 for (staff in staffInfoList) { @@ -2542,19 +2648,37 @@ open class ReportService( } CellUtil.setAlignment(gradeCell, HorizontalAlignment.CENTER); - val salaryPointCell = row.getCell(2) ?: row.createCell(2) - salaryPointCell.apply { - setCellValue((staff.getValue("salaryPoint") as Long).toDouble()) - } - CellUtil.setAlignment(salaryPointCell, HorizontalAlignment.CENTER); + // Get the array of FinancialYear instances + val financialYears = staff["financialYears"] as List + - sheet.addMergedRegion(CellRangeAddress(rowNum, rowNum, 3, 4)) - val hourlyRateCell = row.getCell(3) ?: row.createCell(3) - hourlyRateCell.apply { - setCellValue((staff.getValue("hourlyRate") as Double)) + // Access individual FinancialYear instances + financialYears.let { years -> + var salaryColumn = 2 + for (year in years) { + val salaryPointCell = row.getCell(salaryColumn) ?: row.createCell(salaryColumn) + sheet.addMergedRegion(CellRangeAddress(rowNum, rowNum, salaryColumn, salaryColumn+1)) + salaryPointCell.apply { + setCellValue("${year.salaryPoint} (${year.hourlyRate})") + } + CellUtil.setAlignment(salaryPointCell, HorizontalAlignment.CENTER); + salaryColumn = salaryColumn.plus(2) + } } - CellUtil.setAlignment(hourlyRateCell, HorizontalAlignment.CENTER); - CellUtil.setVerticalAlignment(hourlyRateCell, VerticalAlignment.CENTER); + +// val salaryPointCell = row.getCell(2) ?: row.createCell(2) +// salaryPointCell.apply { +// setCellValue((staff.getValue("salaryPoint") as Long).toDouble()) +// } +// CellUtil.setAlignment(salaryPointCell, HorizontalAlignment.CENTER); +// +// sheet.addMergedRegion(CellRangeAddress(rowNum, rowNum, 3, 4)) +// val hourlyRateCell = row.getCell(3) ?: row.createCell(3) +// hourlyRateCell.apply { +// setCellValue((staff.getValue("hourlyRate") as Double)) +// } +// CellUtil.setAlignment(hourlyRateCell, HorizontalAlignment.CENTER); +// CellUtil.setVerticalAlignment(hourlyRateCell, VerticalAlignment.CENTER); sheet.setRowBreak(rowNum++); } @@ -2608,6 +2732,7 @@ open class ReportService( CellUtil.setVerticalAlignment(manhourCell, VerticalAlignment.CENTER); CellUtil.setCellStyleProperty(manhourCell, CellUtil.WRAP_TEXT, true) CellUtil.setCellStyleProperty(manhourCell, "borderBottom", BorderStyle.THIN) + sheet.setColumnWidth(2 + monthColumnEnd, 15 * 256) val manhourECell = row.getCell(2 + monthColumnEnd + 1) ?: row.createCell(2 + monthColumnEnd + 1) manhourECell.apply { diff --git a/src/main/resources/templates/report/AR07_Project P&L Report v02.xlsx b/src/main/resources/templates/report/AR07_Project P&L Report v02.xlsx index 8acd339b417e0be160ca7ad10007451065686150..ba5e1b121514b9d51a0b43207583e890c0af2eb9 100644 GIT binary patch delta 4222 zcmZ8lcRbXO|391KBr6H$j54zK$R>MlkyQwhJx*^Uqs-iKR&?1jE32Fp$+!sNbSOKn zvuF7A`F$Uc-{<%Hz5af_{&_uK&+&StyVttb<0yef+Z@#dG7#toH3)PC1OkQmh=uwE zc)R-fd5ebm`qWtY`c=y^U#NIplI*xF2*}2Ks4uf&NV2YMfAO^jU!t4kR?rL1nJv=` z4!%6QlaL`TH-S8$=a+4{l3o;u4cyu^dCw!~l$&u~?9)w*Mp=*k5TXXomER>+{%uUU zga>$_5#ZF=a57MGvaz~WRM&d%LyZC;0eX#eld`JX^>z4)WYKckd68A$_~Q*XIN|4P zc9t{WOF_4)L0>8~#xbKt+JXH3z1SYP{FKI|i4Nz|N9yFm&q+@uM}=n5Ec=XYu}P?o zMgcLQXj_l)vUzsX34sdW>|TKSH-)y&zCxgZX?wE|%IjJ1_?E5WT-9L3Vy53Di`tyX zhSa4*ql5}g&u?W)6s*0)`E4%tM@-f_kYRNhJ*67!vft;)Rx;(;pST#sFskGqIFi3< zl--nQ-9cGd^`&GicgTqMmU+KknXRNOmH4tlNCD`rmScQyc3+hgn|#y+Mqw~q)ENM^ zUkT{f(qqowHjkM+oCGYbA%}b`+^%jt7j;rSuS3){7U`Rxow|#xX_e}T>^3Ou*mPjs zwFMkRh#p7Z^2V-N-xL;H*SGls28D?TAcXs0fQ3lr$*bE;t3$#3Yb)DOY5fZNmk<}P z%2wh+BDFHZ)AGJ{M#>KhI-wcKk-#oEH|Ec+v)<15v88znz4-JZ%syp|QqZl26-qPb zcBge)82iC(xfh&U5D6XFDHxlCF;S?r>M-d@;K~2-^&pXvQfPQp2qRfx#@=~5RpLFNF_uVGL=cWcE6eOsrSYHhu^#`?@ z-A8w3$8IE4P%+~L3++X@V8V1~bt~Evg>v2Fe1~Os8*%5OR3;T@JbW~%3N0CICFgd( zb>e)>C1ApM3lVCbal!NT$pUchmvFQgg%y=9RJrFVf1>Pw6>aXknT#??pD;*`vty$` z%~2(5Z?8#S6wmcue2T^y^#;!kPX2Ze*n=R@WXw$&U@1~?7_^ezk{kqD1S3Z2xB*{3 zoH)a$1ruCo(w{q<>4ElWD%M5idej zOdhA3hl#z0*aWNdsPb`58fJZ%XI91=pN^)*b7Qd!x!jsG9f&&_xvLtp+6#g;9a#@WjA8Np~Xl^V#Ut$ z9V4s@?jTt=Q>r$lswRE;^#nZkDc3T|gF73Nm(}8C?4DsieuEZFch>l{fxbePuAWSj z-bSv~q0�POQsBSC!%4nJ&sXiN)jGM#Rh><>U$VLiaxTqA%9f+861Dw|t9g4uZap zi6}=$J^F^LZi}+?zSr%<-xI{R@5KKp!C!k@+L9tyd+7nQBI?`Ht;D!+xue^7`2Jit~_gPN=&U?bqI-8;+LH1gbh-$LSuv|+me5T z`~Iy7dn9B{=Se!q&lX|CpexUdv5b2f$XBn#^Uib99 zE{~CXvy~b1QpI`kB$Uo(!dRF2v{+z-35BgrOTS*%?I>v%fH*8@eRRNVbJoIeNdf|0 zUjCK*%gX|!#c_Fx&y-SNE({2FL4uxY08c{6@>18=P8r{AX|~NbFgd*%_V_?f1;LUwsNa(S;4TG-mH3-EACKB+^VEzDvV zTMzrIKEiQ+cqnGR+sdeP8OA8fZwrcNNsI; zP((Nc<<@KKPaVKVepRCDI_r(aBO-icO(3-$AqQeaoMd3az=hTKLA%hna;1P%$3?-t z?7dqhI@sifXEb8!WjWObY$B_tzXmKO8Ou*dk;A=jQvnYvm4fjUaW&Q;gY5nY%JMIf z>kVy;V0S2R`y%EZL9@|2$}g?L?@x*avqE-Kje2c~uvk z+&SVk84E%{oS|v=YBME(ZVrHz{?!}rzu^G#pL`G)6z=mN@Sk)rY2%PQr^EbIY+sdH z1X*HPoy=dmJTq^92qyKS#*6I3>IZiRxp+q9i+6O4X_&$MVjC8M=T``tT&H8Gi+n!c zOYJWsw%H*~*!d15W+rb=Vc`VpTh@A?r9C9p_POB3CU96+>YY>)^x|{PTOgSEMoe9n zGHimcoc!Q`Md;wX=M(fmA2T%mg9BC(Bc$dwD}3 z5i5ZNzRxZLtdiz=7G+P8Zglf{=$doCTf&>Xz5GKscwYn@pP``r^S1^4G3MTBIT<@G z^Y@&$FAjkZ@WYP05`g?ikep5(fZOX=Jhh8iPGTlNQ*C1kDAdc z5Pyg_{Lp-;x$kHPPgQ14aKf%}UKh6siYuvkw%8Mn`AkpMsA)(SVX7sxOdHXPN5LwIz%To?Yd0+{VHF^ZFiePlZPQkmY{(qBmZ*W7 zo}~n++^y}Z%haYB)CGraVzfove3)co9+)vK)(qqPSbnJhTYKi*Md*PK$%}$-8+;gjAbJ1;e{x)C8 zuSQ>*-OPD&s@~}>lWHO2X2Br?jQ=`2&0pv`-&}e74uIv~GAIZ%lwJA#hm8H$g^7`6 zQ&gh)l^>5Ehb`9A{n_tAzqXg0Y?{-wsWadjG$&>8%=NB}(8LaOt=WgD1NpWE3UkinaWP>Rm; zJ$i?si0-jK@(&k4eOb>U_RcBtem)?v{q~;g2h&h#5@sH*%5aU!!D-aAAByyEPXK&B zi)REd5aO5OW+X_3!E~*0%Dnq;8h*4Fdn%`C4maqC*^3q5_>jD;NpIgk2pQ&ZGV$@< zV*leJH9hS>MsfepI2u2o&83Ku+4mVvQp)yid&57|HQi5|II^1}45UI&tbK+GUS zw>T>hWNu{8r^e7pdy(iuB$QRM3hG59*^<|yYsYxFzHOD*>4hx))*9&wHE_5>Grzjj zc>*8*Szfnzqt-&_laC}l+iPs9rj>=?ON*C0?Le6k{^S))b{Ik+v&571RKoH%S0TgFbQQ z1R}+y=I5yV<}-`jc|L}6u-tuSI#xH^DMcNdR zcSEKeSDB-K+9MyK(9^D*qGryV7O__L>Iufq<3e4Xdf`UxvfDmEkbx{`G86OcT}92v zwMQ=+Rw6S-bTDXm+mw#v8VY!r=slo1OYg5S$YJ1bDE`TxmTog939H*f}A@u-lRXIr*8 zw2RLVahB+sy8G;7Q^ts!-?Ou1M44PLF>pof^ zK)cwp)4R(H|BScswj&wM|J>_{hhoO$02l;9yaux)Zb>k3{CgUJKx}`l`rm4#LyXh0 zBiO`+NR_W5wy&}v?8F&v{(o!#7ynQD-$%fI8$DuNoQ326a_4^mL1P?$nWRQ)&V?YA qFd?;sA}qND5pW5H|EvJwr34FUiU6WQ;tmM|VpW2da!2H!?*9T9c>2Zw delta 4451 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