first draft of timesheet reports.
still need to modify all existing reports to match up to the schema of what's actually coming out from HR lately.master
parent
bc0c38745d
commit
8e6a2f376d
@ -0,0 +1,337 @@
|
|||||||
|
package hr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"mercury/src/db"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
HourReport struct {
|
||||||
|
FilePath string
|
||||||
|
Records *[][]string
|
||||||
|
SkipFirstRow bool
|
||||||
|
ReportLines *[]*HourReportLine
|
||||||
|
}
|
||||||
|
|
||||||
|
HourReportLoadTask struct {
|
||||||
|
Records *[][]string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
HourReportLine struct {
|
||||||
|
PayGroup string
|
||||||
|
LName string
|
||||||
|
FName string
|
||||||
|
HomeDept string
|
||||||
|
ManagerName string
|
||||||
|
WeekEnding string
|
||||||
|
EEId int
|
||||||
|
Overtime float64
|
||||||
|
Regular float64
|
||||||
|
Sick float64
|
||||||
|
Vacation float64
|
||||||
|
Bereavement float64
|
||||||
|
Service float64
|
||||||
|
Total float64
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var namePattern = regexp.MustCompile("^\\d{1,2}.\\d{1,2}.\\d{2}-(\\d{1,2}.\\d{1,2}.\\d{2}).*csv$")
|
||||||
|
|
||||||
|
func loadReports(pathlikeBase string) *[]HourReport {
|
||||||
|
files, err := getAllFilesInDir(pathlikeBase)
|
||||||
|
reports := make([]HourReport, 0, 300)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
for _, file := range *files {
|
||||||
|
sReport := NewHourReport(file, false)
|
||||||
|
reports = append(reports, *sReport)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &reports
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateTimesheetReport(pathlike string) {
|
||||||
|
connector := &db.ConnectorGeneric{}
|
||||||
|
|
||||||
|
reports := loadReports(pathlike)
|
||||||
|
|
||||||
|
tableWipe := db.NewRunner("create-mercury-hrTimesheets-table.sql", db.MercuryDatabaseName)
|
||||||
|
connector.ExecuteSqlScript(tableWipe)
|
||||||
|
for _, hourReport := range *reports {
|
||||||
|
log.Printf("Updating database\n")
|
||||||
|
db.BlockUpdate[HourReportLine](connector, db.MercuryDatabaseName, "update-mercury-hrTimesheets.sql", hourReport.ReportLines)
|
||||||
|
log.Printf("Updates finished.\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
tablePrune := db.NewRunner("update-mercury-hrTimesheetsCleanup.sql", db.MercuryDatabaseName)
|
||||||
|
connector.ExecuteSqlScript(tablePrune)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHourReport(pathlike string, skipFirstRow bool) *HourReport {
|
||||||
|
report := HourReport{
|
||||||
|
FilePath: pathlike,
|
||||||
|
Records: nil,
|
||||||
|
SkipFirstRow: skipFirstRow,
|
||||||
|
}
|
||||||
|
|
||||||
|
asyncChan := make(chan *HourReportLoadTask)
|
||||||
|
|
||||||
|
go loadTimeSheet(report.FilePath, asyncChan)
|
||||||
|
recordStatus := <-asyncChan
|
||||||
|
if recordStatus.Err != nil {
|
||||||
|
fmt.Printf("Error in the following file: %s\n", report.FilePath)
|
||||||
|
panic(recordStatus.Err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
report.Records = recordStatus.Records
|
||||||
|
|
||||||
|
report.ReportLines = processReportToLines(report)
|
||||||
|
|
||||||
|
return &report
|
||||||
|
}
|
||||||
|
|
||||||
|
func processReportToLines(report HourReport) *[]*HourReportLine {
|
||||||
|
lines := make([]*HourReportLine, 0, 250)
|
||||||
|
localTable := *report.Records
|
||||||
|
headersRaw := (localTable)[0]
|
||||||
|
headers := make([]string, len(headersRaw), len(headersRaw))
|
||||||
|
for i, v := range headersRaw {
|
||||||
|
key := strings.Trim(v, " \t\uFEFF")
|
||||||
|
headers[i] = key
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i < len(localTable); i++ {
|
||||||
|
row := localTable[i]
|
||||||
|
line := newHourReportLineFromRow(headers, row)
|
||||||
|
line.WeekEnding = fileNameToSQLDate(report.FilePath)
|
||||||
|
lines = append(lines, &line)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileNameToSQLDate(fileName string) string {
|
||||||
|
name := path.Base(fileName)
|
||||||
|
parts := strings.Split(name, "_")
|
||||||
|
year := parts[0][:4]
|
||||||
|
month := parts[0][4:6]
|
||||||
|
date := parts[0][6:8]
|
||||||
|
return fmt.Sprintf("%s-%s-%s", year, month, date)
|
||||||
|
}
|
||||||
|
|
||||||
|
// I hate that this has a ton of hard-coded stuff. I'm not sure if there's a way around it though
|
||||||
|
func newHourReportLineFromRow(headers []string, row []string) HourReportLine {
|
||||||
|
line := HourReportLine{}
|
||||||
|
if len(headers) != len(row) {
|
||||||
|
panic("header array and row array are different sizes in newHourReportLineFromRow")
|
||||||
|
}
|
||||||
|
//"Paygroup", "Last Name", "First Name", "Home Department", "Manager Name", "Worked DeptName", "OT", "Reg", "Sick", "Vac", "Total"
|
||||||
|
|
||||||
|
for i, header := range headers {
|
||||||
|
strVal := row[i]
|
||||||
|
switch header {
|
||||||
|
case "Paygroup":
|
||||||
|
line.PayGroup = strVal
|
||||||
|
case "Last Name":
|
||||||
|
line.LName = strVal
|
||||||
|
case "First Name":
|
||||||
|
line.FName = strVal
|
||||||
|
case "Home Department":
|
||||||
|
line.HomeDept = strVal
|
||||||
|
case "Manager Name":
|
||||||
|
line.ManagerName = strVal
|
||||||
|
case "Worked DeptName":
|
||||||
|
continue
|
||||||
|
case "OT":
|
||||||
|
v, err := strconv.ParseFloat(strVal, 64)
|
||||||
|
if err != nil {
|
||||||
|
v = 0
|
||||||
|
}
|
||||||
|
line.Overtime = v
|
||||||
|
|
||||||
|
case "Reg":
|
||||||
|
|
||||||
|
v, err := strconv.ParseFloat(strVal, 64)
|
||||||
|
if err != nil {
|
||||||
|
v = 0
|
||||||
|
}
|
||||||
|
line.Regular = v
|
||||||
|
|
||||||
|
case "Sick":
|
||||||
|
|
||||||
|
v, err := strconv.ParseFloat(strVal, 64)
|
||||||
|
if err != nil {
|
||||||
|
v = 0
|
||||||
|
}
|
||||||
|
line.Sick = v
|
||||||
|
|
||||||
|
case "Vac":
|
||||||
|
|
||||||
|
v, err := strconv.ParseFloat(strVal, 64)
|
||||||
|
if err != nil {
|
||||||
|
v = 0
|
||||||
|
}
|
||||||
|
line.Vacation = v
|
||||||
|
|
||||||
|
case "Total":
|
||||||
|
|
||||||
|
v, err := strconv.ParseFloat(strVal, 64)
|
||||||
|
if err != nil {
|
||||||
|
v = 0
|
||||||
|
}
|
||||||
|
line.Total = v
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadTimeSheet(pathlike string, asyncChan chan<- *HourReportLoadTask) {
|
||||||
|
f, err := os.OpenFile(pathlike, os.O_RDONLY, 0755)
|
||||||
|
if err != nil {
|
||||||
|
asyncChan <- &HourReportLoadTask{nil, err}
|
||||||
|
close(asyncChan)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
reader := csv.NewReader(f)
|
||||||
|
records, err := reader.ReadAll()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
asyncChan <- &HourReportLoadTask{nil, err}
|
||||||
|
close(asyncChan)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
asyncChan <- &HourReportLoadTask{&records, nil}
|
||||||
|
close(asyncChan)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (line HourReportLine) ToQueryBlock() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"('%s','%s','%s','%s','%s','%s','%d','%f','%f','%f','%f','%f','%f','%f')",
|
||||||
|
line.PayGroup,
|
||||||
|
line.LName,
|
||||||
|
line.FName,
|
||||||
|
line.HomeDept,
|
||||||
|
line.ManagerName,
|
||||||
|
line.WeekEnding,
|
||||||
|
line.EEId,
|
||||||
|
line.Overtime,
|
||||||
|
line.Regular,
|
||||||
|
line.Sick,
|
||||||
|
line.Vacation,
|
||||||
|
line.Bereavement,
|
||||||
|
line.Service,
|
||||||
|
line.Total,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
//deprecated
|
||||||
|
func rename(report HourReport) {
|
||||||
|
fileName := path.Base(report.FilePath)
|
||||||
|
if namePattern.MatchString(fileName) {
|
||||||
|
idx := namePattern.FindAllStringSubmatch(fileName, -1)
|
||||||
|
parts := strings.Split(idx[0][1], ".")
|
||||||
|
year, err := strconv.Atoi(parts[2])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
month, err := strconv.Atoi(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
date, err := strconv.Atoi(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
full := fmt.Sprintf("20%02d%02d%02d", year, month, date)
|
||||||
|
tStamp := time.Date(year, getMonth(month), date, 10, 0, 0, 0, time.UTC)
|
||||||
|
_, week := tStamp.ISOWeek()
|
||||||
|
fileName := fmt.Sprintf("%s_Paycor_W%d.csv", full, week)
|
||||||
|
|
||||||
|
err = copyFile(report.FilePath, fileName)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Couldn't find match for %s\n", fileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMonth(month int) time.Month {
|
||||||
|
switch month {
|
||||||
|
case 1:
|
||||||
|
return time.January
|
||||||
|
case 2:
|
||||||
|
return time.February
|
||||||
|
case 3:
|
||||||
|
return time.March
|
||||||
|
case 4:
|
||||||
|
return time.April
|
||||||
|
case 5:
|
||||||
|
return time.May
|
||||||
|
case 6:
|
||||||
|
return time.June
|
||||||
|
case 7:
|
||||||
|
return time.July
|
||||||
|
case 8:
|
||||||
|
return time.August
|
||||||
|
case 9:
|
||||||
|
return time.September
|
||||||
|
case 10:
|
||||||
|
return time.October
|
||||||
|
case 11:
|
||||||
|
return time.November
|
||||||
|
case 12:
|
||||||
|
return time.December
|
||||||
|
default:
|
||||||
|
return time.January
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAllFilesInDir(pathlikeBase string) (*[]string, error) {
|
||||||
|
listing, err := os.ReadDir(pathlikeBase)
|
||||||
|
res := make([]string, 0, 300)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, list := range listing {
|
||||||
|
if list.IsDir() || path.Ext(list.Name()) != ".csv" {
|
||||||
|
fmt.Printf("Skipping: %s\n", list.Name())
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
res = append(res, path.Join(pathlikeBase, list.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFile(inPath string, outpath string) error {
|
||||||
|
outPathBase := "/home/dtookey/work/clarity-reporting/pcorrect"
|
||||||
|
outFinal := path.Join(outPathBase, outpath)
|
||||||
|
b, err := os.ReadFile(inPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = os.WriteFile(outFinal, b, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
DROP TABLE IF EXISTS mercury.hr_timesheet_report;
|
||||||
|
|
||||||
|
CREATE TABLE mercury.hr_timesheet_report
|
||||||
|
(
|
||||||
|
PayGroup VARCHAR(150),
|
||||||
|
LName VARCHAR(150),
|
||||||
|
FName VARCHAR(150),
|
||||||
|
HomeDept VARCHAR(150),
|
||||||
|
ManagerName VARCHAR(150),
|
||||||
|
WeekEnding VARCHAR(150),
|
||||||
|
EEId INT,
|
||||||
|
Overtime REAL,
|
||||||
|
Regular REAL,
|
||||||
|
Sick REAL,
|
||||||
|
Vacation REAL,
|
||||||
|
Bereavement REAL,
|
||||||
|
Service REAL,
|
||||||
|
Total REAL
|
||||||
|
);
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
SELECT refnum, location, IF(price_override = 1, override_price, default_price) as fee
|
||||||
|
FROM projects.all_projects
|
||||||
|
inner join billing b on all_projects.refnum = b.refNumber;
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
INSERT INTO mercury.hr_timesheet_report (PayGroup, LName, FName, HomeDept, ManagerName, WeekEnding, EEId, Overtime, Regular,
|
||||||
|
Sick, Vacation, Bereavement, Service, Total)
|
||||||
|
VALUES %s;
|
||||||
@ -0,0 +1 @@
|
|||||||
|
DELETE FROM mercury.hr_timesheet_report where FName = '' and LName = '';
|
||||||
@ -0,0 +1,114 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/csv"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"mercury/src/db"
|
||||||
|
"mercury/src/mercury"
|
||||||
|
"mercury/src/projectClarity"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
projectPattern = regexp.MustCompile("^[A-z]{3}\\d{7}$")
|
||||||
|
locationPattern = regexp.MustCompile(".*, ([NSC]{2})")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
csvPath := "/home/dtookey/work/clarity-reporting/billing2021.CSV"
|
||||||
|
outPath := "/home/dtookey/work/clarity-reporting/extended-billing2021.csv"
|
||||||
|
records := open(csvPath)
|
||||||
|
projectMap := getClarityData()
|
||||||
|
augmentedData := augmentData(records, *projectMap)
|
||||||
|
write(augmentedData, outPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClarityData() *map[string]projectClarity.ClarityProjectBilling {
|
||||||
|
cdb := mercury.NewInterconnect()
|
||||||
|
cdb.PseudoInit()
|
||||||
|
cb := func(rows *sql.Rows) *projectClarity.ClarityProjectBilling {
|
||||||
|
container := projectClarity.ClarityProjectBilling{}
|
||||||
|
err := rows.Scan(&container.Refnum, &container.Location, &container.Fee)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
return &container
|
||||||
|
}
|
||||||
|
rMap := make(map[string]projectClarity.ClarityProjectBilling)
|
||||||
|
thing := db.QueryForObjects[projectClarity.ClarityProjectBilling](cdb.InsightDBConnector.ConnectorGeneric, db.ClarityDatabaseName, "read-clarity-billingProjects.sql", cb)
|
||||||
|
for _, thing := range *thing {
|
||||||
|
rMap[thing.Refnum] = *thing
|
||||||
|
}
|
||||||
|
|
||||||
|
return &rMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func augmentData(data *[][]string, clarityData map[string]projectClarity.ClarityProjectBilling) *[][]string {
|
||||||
|
results := make([][]string, 0, 10000)
|
||||||
|
longest := ""
|
||||||
|
for _, row := range *data {
|
||||||
|
refNum := row[3]
|
||||||
|
nRow := row
|
||||||
|
if projectPattern.MatchString(refNum) {
|
||||||
|
project := clarityData[refNum]
|
||||||
|
deciString := strconv.FormatFloat(project.Fee, 'f', 2, 64)
|
||||||
|
if len(project.Location) > len(longest) {
|
||||||
|
longest = project.Location
|
||||||
|
}
|
||||||
|
|
||||||
|
nRow = append(nRow, project.Location)
|
||||||
|
nRow = append(nRow, deciString)
|
||||||
|
b := []byte(project.Location)
|
||||||
|
if locationPattern.Match(b) {
|
||||||
|
idx := locationPattern.FindAllSubmatchIndex(b, -1)[0]
|
||||||
|
fmt.Printf("%#v\n", idx)
|
||||||
|
state := string(b[idx[2]:idx[3]])
|
||||||
|
fmt.Println(state)
|
||||||
|
nRow = append(nRow, state)
|
||||||
|
} else {
|
||||||
|
nRow = append(nRow, "")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nRow = append(nRow, "")
|
||||||
|
nRow = append(nRow, "")
|
||||||
|
nRow = append(nRow, "")
|
||||||
|
}
|
||||||
|
results = append(results, nRow)
|
||||||
|
}
|
||||||
|
fmt.Println(longest)
|
||||||
|
|
||||||
|
return &results
|
||||||
|
}
|
||||||
|
|
||||||
|
func write(data *[][]string, pathlike string) {
|
||||||
|
f, err := os.Create(pathlike)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
writer := csv.NewWriter(f)
|
||||||
|
for _, row := range *data {
|
||||||
|
err := writer.Write(row)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writer.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func open(pathlike string) *[][]string {
|
||||||
|
f, err := os.OpenFile(pathlike, os.O_RDONLY, 0755)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
reader := csv.NewReader(f)
|
||||||
|
records, err := reader.ReadAll()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return &records
|
||||||
|
}
|
||||||
@ -1,12 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import "mercury/src/hr"
|
||||||
"mercury/src/finance"
|
|
||||||
"mercury/src/mercury"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ic := mercury.NewInterconnect()
|
reportBase := "/home/dtookey/work/clarity-reporting/paycor/"
|
||||||
ic.ResetTables()
|
hr.LoadReports(reportBase)
|
||||||
finance.GenerateArAgingReport("/home/dtookey/work/clarity-reporting/qb/ar/")
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue