feat: pro-rata berdasarkan time span commit bukan jumlah commit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,8 +3,11 @@
|
||||
Ambil semua commit hari ini dari semua repo, distribusikan 8 jam pro-rata,
|
||||
lalu upload ke Odoo timesheet.
|
||||
|
||||
Pro-rata: bobot = jumlah commit per task. Total = 8 jam.
|
||||
Contoh: Task A 3 commit + Task B 1 commit → A=6h, B=2h.
|
||||
Pro-rata: bobot = time span per task (last commit - first commit).
|
||||
Single commit mendapat minimum 30 menit.
|
||||
Contoh: Task A span 2 jam + Task B span 1.5 jam + Task C 1 commit →
|
||||
bobot: 120 + 90 + 30 = 240 menit
|
||||
Task A = 8 × 120/240 = 4.00h, Task B = 3.00h, Task C = 1.00h
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -13,7 +16,7 @@ import subprocess
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import argparse
|
||||
from datetime import date, timedelta
|
||||
from datetime import date, datetime, timedelta
|
||||
from collections import defaultdict
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
|
||||
@@ -153,7 +156,7 @@ def get_commits_today(repo_path: str, author: str, today: str) -> list[dict]:
|
||||
f"--after={today}",
|
||||
f"--before={until}",
|
||||
"--format=%h|%ad|%s",
|
||||
"--date=short",
|
||||
"--date=format:%Y-%m-%d %H:%M",
|
||||
]
|
||||
try:
|
||||
out = subprocess.check_output(cmd, stderr=subprocess.DEVNULL, text=True)
|
||||
@@ -165,17 +168,34 @@ def get_commits_today(repo_path: str, author: str, today: str) -> list[dict]:
|
||||
continue
|
||||
parts = line.split("|", 2)
|
||||
if len(parts) == 3:
|
||||
commits.append({"hash": parts[0], "date": parts[1], "message": parts[2]})
|
||||
dt = datetime.strptime(parts[1], "%Y-%m-%d %H:%M")
|
||||
commits.append({
|
||||
"hash": parts[0],
|
||||
"date": dt.strftime("%Y-%m-%d"),
|
||||
"time": dt.strftime("%H:%M"),
|
||||
"dt": dt,
|
||||
"message": parts[2],
|
||||
})
|
||||
return commits
|
||||
|
||||
|
||||
# ── Pro-rata ──────────────────────────────────────────────────────────────────
|
||||
|
||||
SINGLE_COMMIT_MINUTES = 30 # bobot minimum untuk task dengan 1 commit
|
||||
|
||||
|
||||
def span_minutes(timestamps: list[datetime]) -> int:
|
||||
"""Menit antara commit pertama dan terakhir. Minimum 30 menit."""
|
||||
if len(timestamps) <= 1:
|
||||
return SINGLE_COMMIT_MINUTES
|
||||
return max(
|
||||
int((max(timestamps) - min(timestamps)).total_seconds() / 60),
|
||||
SINGLE_COMMIT_MINUTES,
|
||||
)
|
||||
|
||||
|
||||
def distribute_hours(weights: list[int], total: float = 8.0) -> list[float]:
|
||||
"""
|
||||
Distribusi jam pro-rata berdasarkan bobot (jumlah commit).
|
||||
Adjustment di entry terakhir supaya sum persis = total.
|
||||
"""
|
||||
"""Pro-rata berdasarkan bobot (menit span). Sum = total. Adjustment di entry terakhir."""
|
||||
total_weight = sum(weights)
|
||||
raw = [Decimal(str(total)) * Decimal(w) / Decimal(total_weight) for w in weights]
|
||||
rounded = [float(r.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)) for r in raw]
|
||||
@@ -221,6 +241,8 @@ def main():
|
||||
"code": m.group(1),
|
||||
"description": m.group(2),
|
||||
"date": c["date"],
|
||||
"time": c["time"],
|
||||
"dt": c["dt"],
|
||||
"hash": c["hash"],
|
||||
"repo": repo_name,
|
||||
"project": repo["project"],
|
||||
@@ -238,9 +260,11 @@ def main():
|
||||
|
||||
task_cache = {} # (code, project_id) → (task_id, task_name) | None
|
||||
|
||||
# Group: key = (task_id, project_id), value = list of commit descriptions & count
|
||||
groups = defaultdict(lambda: {"descriptions": [], "count": 0, "project_id": None,
|
||||
"project": None, "task_name": None, "date": None})
|
||||
# Group: key = (task_id, project_id)
|
||||
groups = defaultdict(lambda: {
|
||||
"descriptions": [], "timestamps": [], "count": 0,
|
||||
"project_id": None, "project": None, "task_name": None, "date": None,
|
||||
})
|
||||
|
||||
not_found = []
|
||||
for c in raw_commits:
|
||||
@@ -257,6 +281,7 @@ def main():
|
||||
task_id, task_name = task
|
||||
key = (task_id, c["project_id"])
|
||||
groups[key]["descriptions"].append(c["description"])
|
||||
groups[key]["timestamps"].append(c["dt"])
|
||||
groups[key]["count"] += 1
|
||||
groups[key]["project_id"] = c["project_id"]
|
||||
groups[key]["project"] = c["project"]
|
||||
@@ -268,22 +293,27 @@ def main():
|
||||
print("\nTidak ada task yang ditemukan di Odoo.")
|
||||
return
|
||||
|
||||
# 3. Hitung pro-rata jam
|
||||
# 3. Hitung pro-rata berdasarkan time span
|
||||
keys = list(groups.keys())
|
||||
weights = [groups[k]["count"] for k in keys]
|
||||
hours = distribute_hours(weights, total=float(TOTAL_HOURS))
|
||||
spans = [span_minutes(groups[k]["timestamps"]) for k in keys]
|
||||
hours = distribute_hours(spans, total=float(TOTAL_HOURS))
|
||||
|
||||
# 4. Tampilkan preview
|
||||
print(f"\n{'─' * 65}")
|
||||
print(f" {'PROJECT':<20} {'TASK':<10} {'JAM':>5} {'COMMIT':>6} DESKRIPSI")
|
||||
print(f"{'─' * 65}")
|
||||
print(f"\n{'─' * 75}")
|
||||
print(f" {'PROJECT':<20} {'TASK':<10} {'WAKTU':<13} {'SPAN':>6} {'JAM':>5} DESKRIPSI")
|
||||
print(f"{'─' * 75}")
|
||||
|
||||
entries = []
|
||||
for key, h in zip(keys, hours):
|
||||
for key, h, span in zip(keys, hours, spans):
|
||||
g = groups[key]
|
||||
desc = g["descriptions"][0] if len(g["descriptions"]) == 1 else \
|
||||
"; ".join(dict.fromkeys(g["descriptions"])) # deduplicate, preserve order
|
||||
print(f" {g['project']:<20} {str(g['task_id']):<10} {h:>5.2f} {g['count']:>6}x {desc[:35]}")
|
||||
ts = sorted(g["timestamps"])
|
||||
if len(ts) == 1:
|
||||
waktu = ts[0].strftime("%H:%M") + " (1 commit)"
|
||||
else:
|
||||
waktu = f"{ts[0].strftime('%H:%M')}–{ts[-1].strftime('%H:%M')}"
|
||||
span_label = f"{span}m" if span > SINGLE_COMMIT_MINUTES else "30m*"
|
||||
desc = "; ".join(dict.fromkeys(g["descriptions"]))
|
||||
print(f" {g['project']:<20} {str(g['task_id']):<10} {waktu:<13} {span_label:>6} {h:>5.2f} {desc[:30]}")
|
||||
entries.append({
|
||||
"odoo": {
|
||||
"name": desc,
|
||||
@@ -297,8 +327,10 @@ def main():
|
||||
"display": g,
|
||||
})
|
||||
|
||||
print(f"{'─' * 65}")
|
||||
print(f" {'TOTAL':<20} {'':<10} {sum(hours):>5.2f} {sum(weights):>6}x")
|
||||
print(f"{'─' * 75}")
|
||||
total_span = sum(spans)
|
||||
print(f" {'TOTAL':<20} {'':<10} {'':<13} {total_span:>5}m {sum(hours):>5.2f}")
|
||||
print(f" * single commit → minimum {SINGLE_COMMIT_MINUTES} menit")
|
||||
|
||||
if not_found:
|
||||
print(f"\n {len(not_found)} commit diabaikan (task tidak ditemukan di Odoo)")
|
||||
|
||||
Reference in New Issue
Block a user