mcpField
D
daily-report-generator

daily-report-generator

github.com/datadrivenconstruction文档处理375114 次安装1 周前更新
代码仓库

关于 daily-report-generator

daily-report-generator 是面向工地管理者的日报生成技能,介绍如何汇总项目、天气和工时数据,生成 PDF 报告并自动分发。

主要功能

  1. 01收集项目数据
  2. 02纳入天气信息
  3. 03汇总工人工时表
  4. 04生成 PDF 报告
  5. 05自动分发报告

适用场景

  1. 01编制每日工地进展报告
  2. 02整合工时与天气情况
  3. 03向相关方发送完成的报告

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/datadrivenconstruction/ddc_skills_for_ai_agents_in_construction --skill daily-report-generator

安装后在 Claude Code 中输入/daily-report-generator确认已加载。

附带文件claw.jsoninstructions.md

详细介绍

施工现场日报生成器

从多个来源汇总数据,生成专业文档,自动创建全面的施工日报。

商业案例

问题:现场经理每天要花 45-60 分钟:

  • 收集工头提供的信息
  • 检查天气状况
  • 汇总工人人数和工时
  • 撰写叙述性摘要
  • 设置报告格式并分发

解决方案:自动化系统能够:

  • 从 Google Sheets/项目数据库提取数据
  • 整合天气 API 数据
  • 汇总工人考勤表
  • 生成专业的 PDF 报告
  • 自动分发给相关人员

投资回报率:日报编制时间减少 80%(45 分钟 → 审核需 9 分钟)

报告结构

┌──────────────────────────────────────────────────────────────────────┐
│                    DAILY CONSTRUCTION REPORT                          │
│                                                                       │
│  Project: ЖК Солнечный, Корпус 2          Date: 24.01.2026           │
│  Report #: DCR-2026-024                   Weather: ☁️ -5°C           │
├──────────────────────────────────────────────────────────────────────┤
│                                                                       │
│  1. WEATHER CONDITIONS                                                │
│  ┌────────────┬────────────┬────────────┬────────────┐               │
│  │ Morning    │ Afternoon  │ Evening    │ Impact     │               │
│  │ -8°C ☀️    │ -5°C ☁️    │ -7°C 🌙    │ Normal     │               │
│  └────────────┴────────────┴────────────┴────────────┘               │
│                                                                       │
│  2. WORKFORCE                                                         │
│  ┌────────────────────────────────────────────────────┐              │
│  │ Category          │ Planned │ Actual │ Hours      │              │
│  ├────────────────────────────────────────────────────┤              │
│  │ GC Supervision    │    3    │   3    │    27      │              │
│  │ Electrical        │   12    │  11    │    88      │              │
│  │ Plumbing          │    8    │   8    │    64      │              │
│  │ HVAC              │    6    │   6    │    48      │              │
│  │ TOTAL             │   29    │  28    │   227      │              │
│  └────────────────────────────────────────────────────┘              │
│                                                                       │
│  3. WORK COMPLETED TODAY                                              │
│  • Electrical: Completed rough-in floors 5-6                         │
│  • Plumbing: Installed risers section A                              │
│  • HVAC: Ductwork installation 60% complete                          │
│                                                                       │
│  4. WORK PLANNED FOR TOMORROW                                         │
│  • Electrical: Begin rough-in floor 7                                │
│  • Plumbing: Continue risers section B                               │
│  • HVAC: Complete ductwork, begin testing                            │
│                                                                       │
│  5. ISSUES / DELAYS                                                   │
│  • Material delay: Electrical panels (ETA: 26.01)                    │
│  • Weather: Expected snow may delay exterior work                    │
│                                                                       │
│  6. SAFETY                                                            │
│  ✅ No incidents                                                      │
│  ✅ Toolbox talk completed: Fall protection                          │
│                                                                       │
│  7. PHOTOS                                                            │
│  [Photo 1: Floor 5 electrical]  [Photo 2: Riser installation]        │
│                                                                       │
│  ─────────────────────────────────────────────────────────────────   │
│  Prepared by: Иван Петров, Site Manager                              │
│  Approved by: ___________________                                     │
│  Distribution: Owner, Architect, PM                                   │
└──────────────────────────────────────────────────────────────────────┘

Python 实现

import pandas as pd
from datetime import datetime, date
from typing import Optional, List, Dict
import requests
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm
import os

class DailyReportGenerator:
    """Generate professional daily construction reports"""

    def __init__(self, config: dict):
        self.config = config
        self.weather_api_key = config.get('weather_api_key')
        self.project_name = config.get('project_name')
        self.report_date = config.get('report_date', date.today())

    def get_weather_data(self, location: str) -> dict:
        """Fetch weather data from API"""
        if not self.weather_api_key:
            return self._mock_weather()

        url = f"https://api.openweathermap.org/data/2.5/weather"
        params = {
            'q': location,
            'appid': self.weather_api_key,
            'units': 'metric',
            'lang': 'ru'
        }

        response = requests.get(url, params=params)
        if response.status_code == 200:
            data = response.json()
            return {
                'temp': round(data['main']['temp']),
                'description': data['weather'][0]['description'],
                'humidity': data['main']['humidity'],
                'wind_speed': round(data['wind']['speed']),
                'icon': self._get_weather_icon(data['weather'][0]['main'])
            }
        return self._mock_weather()

    def _get_weather_icon(self, condition: str) -> str:
        icons = {
            'Clear': '☀️',
            'Clouds': '☁️',
            'Rain': '🌧️',
            'Snow': '❄️',
            'Thunderstorm': '⛈️',
            'Mist': '🌫️'
        }
        return icons.get(condition, '🌤️')

    def _mock_weather(self) -> dict:
        return {
            'temp': -5,
            'description': 'облачно',
            'humidity': 65,
            'wind_speed': 3,
            'icon': '☁️'
        }

    def get_workforce_data(self, source: pd.DataFrame) -> dict:
        """Aggregate workforce data from timesheet"""
        # Expected columns: trade, worker_name, hours_worked, planned_hours

        summary = source.groupby('trade').agg({
            'worker_name': 'count',
            'hours_worked': 'sum',
            'planned_hours': 'sum'
        }).reset_index()

        summary.columns = ['trade', 'actual_count', 'actual_hours', 'planned_hours']

        # Calculate planned count (assuming 8-hour shifts)
        summary['planned_count'] = (summary['planned_hours'] / 8).astype(int)

        return {
            'trades': summary.to_dict('records'),
            'total_workers': summary['actual_count'].sum(),
            'total_hours': summary['actual_hours'].sum(),
            'total_planned': summary['planned_count'].sum()
        }

    def get_work_completed(self, tasks: pd.DataFrame) -> List[dict]:
        """Extract completed work from task system"""
        # Filter completed tasks for today
        completed = tasks[
            (tasks['date'] == self.report_date.strftime('%d.%m.%Y')) &
            (tasks['status'].isin(['Completed', 'Partial']))
        ]

        work_items = []
        for _, row in completed.iterrows():
            work_items.append({
                'trade': row['trade'],
                'description': row['description'],
                'status': row['status'],
                'notes': row.get('notes', '')
            })

        return work_items

    def get_work_planned(self, tasks: pd.DataFrame) -> List[dict]:
        """Get planned work for tomorrow"""
        tomorrow = self.report_date + pd.Timedelta(days=1)

        planned = tasks[
            tasks['date'] == tomorrow.strftime('%d.%m.%Y')
        ]

        work_items = []
        for _, row in planned.iterrows():
            work_items.append({
                'trade': row['trade'],
                'description': row['description'],
                'priority': row.get('priority', 'Medium')
            })

        return work_items

    def get_issues(self, issues_log: pd.DataFrame) -> List[dict]:
        """Get active issues and delays"""
        active = issues_log[
            (issues_log['status'] == 'Open') |
            (issues_log['date_reported'] == self.report_date.strftime('%d.%m.%Y'))
        ]

        return active[['category', 'description', 'impact', 'resolution_date']].to_dict('records')

    def get_safety_data(self, safety_log: pd.DataFrame) -> dict:
        """Get safety information for the day"""
        today_incidents = safety_log[
            safety_log['date'] == self.report_date.strftime('%d.%m.%Y')
        ]

        return {
            'incidents': len(today_incidents[today_incidents['type'] == 'Incident']),
            'near_misses': len(today_incidents[today_incidents['type'] == 'Near Miss']),
            'toolbox_talk': today_incidents[
                today_incidents['type'] == 'Toolbox Talk'
            ]['topic'].tolist(),
            'observations': today_incidents[
                today_incidents['type'] == 'Observation'
            ]['description'].tolist()
        }

    def generate_report(self, data: dict, output_path: str) -> str:
        """Generate PDF report"""

        doc = SimpleDocTemplate(
            output_path,
            pagesize=A4,
            rightMargin=2*cm,
            leftMargin=2*cm,
            topMargin=2*cm,
            bottomMargin=2*cm
        )

        styles = getSampleStyleSheet()
        title_style = ParagraphStyle(
            'Title',
            parent=styles['Heading1'],
            fontSize=16,
            alignment=1,
            spaceAfter=12
        )
        heading_style = ParagraphStyle(
            'Heading',
            parent=styles['Heading2'],
            fontSize=12,
            spaceBefore=12,
            spaceAfter=6
        )

        elements = []

        # Title
        elements.append(Paragraph(
            f"DAILY CONSTRUCTION REPORT",
            title_style
        ))

        # Header info
        header_data = [
            ['Project:', self.project_name, 'Date:', self.report_date.strftime('%d.%m.%Y')],
            ['Report #:', data.get('report_number', 'DCR-001'), 'Weather:', f"{data['weather']['icon']} {data['weather']['temp']}°C"]
        ]
        header_table = Table(header_data, colWidths=[3*cm, 6*cm, 3*cm, 4*cm])
        header_table.setStyle(TableStyle([
            ('FONTNAME', (0, 0), (-1, -1), 'Helvetica'),
            ('FONTSIZE', (0, 0), (-1, -1), 10),
            ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
            ('FONTNAME', (2, 0), (2, -1), 'Helvetica-Bold'),
        ]))
        elements.append(header_table)
        elements.append(Spacer(1, 12))

        # Weather section
        elements.append(Paragraph("1. WEATHER CONDITIONS", heading_style))
        weather = data['weather']
        weather_text = f"""
        Temperature: {weather['temp']}°C | Humidity: {weather['humidity']}% |
        Wind: {weather['wind_speed']} m/s | Conditions: {weather['description']}
        """
        elements.append(Paragraph(weather_text, styles['Normal']))

        # Workforce section
        elements.append(Paragraph("2. WORKFORCE", heading_style))
        workforce = data['workforce']
        workforce_data = [['Trade', 'Planned', 'Actual', 'Hours']]
        for trade in workforce['trades']:
            workforce_data.append([
                trade['trade'],
                str(trade['planned_count']),
                str(trade['actual_count']),
                str(int(trade['actual_hours']))
            ])
        workforce_data.append([
            'TOTAL',
            str(workforce['total_planned']),
            str(workforce['total_workers']),
            str(int(workforce['total_hours']))
        ])

        workforce_table = Table(workforce_data, colWidths=[6*cm, 3*cm, 3*cm, 3*cm])
        workforce_table.setStyle(TableStyle([
            ('BACKGROUND', (0, 0), (-1, 0), colors.grey),
            ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
            ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
            ('FONTNAME', (0, -1), (-1, -1), 'Helvetica-Bold'),
            ('GRID', (0, 0), (-1, -1), 1, colors.black),
            ('ALIGN', (1, 0), (-1, -1), 'CENTER'),
        ]))
        elements.append(workforce_table)

        # Work completed
        elements.append(Paragraph("3. WORK COMPLETED TODAY", heading_style))
        for item in data.get('work_completed', []):
            bullet = f"• {item['trade']}: {item['description']}"
            if item.get('notes'):
                bullet += f" ({item['notes']})"
            elements.append(Paragraph(bullet, styles['Normal']))

        # Work planned
        elements.append(Paragraph("4. WORK PLANNED FOR TOMORROW", heading_style))
        for item in data.get('work_planned', []):
            bullet = f"• {item['trade']}: {item['description']}"
            elements.append(Paragraph(bullet, styles['Normal']))

        # Issues
        elements.append(Paragraph("5. ISSUES / DELAYS", heading_style))
        issues = data.get('issues', [])
        if issues:
            for issue in issues:
                bullet = f"• {issue['category']}: {issue['description']}"
                if issue.get('resolution_date'):
                    bullet += f" (ETA: {issue['resolution_date']})"
                elements.append(Paragraph(bullet, styles['Normal']))
        else:
            elements.append(Paragraph("No significant issues reported.", styles['Normal']))

        # Safety
        elements.append(Paragraph("6. SAFETY", heading_style))
        safety = data.get('safety', {})
        if safety.get('incidents', 0) == 0:
            elements.append(Paragraph("✅ No incidents reported", styles['Normal']))
        else:
            elements.append(Paragraph(f"⚠️ {safety['incidents']} incident(s) reported", styles['Normal']))

        if safety.get('toolbox_talk'):
            elements.append(Paragraph(f"✅ Toolbox talk: {', '.join(safety['toolbox_talk'])}", styles['Normal']))

        # Signature block
        elements.append(Spacer(1, 24))
        elements.append(Paragraph("─" * 60, styles['Normal']))
        elements.append(Paragraph(f"Prepared by: {data.get('prepared_by', '_________________')}", styles['Normal']))
        elements.append(Paragraph(f"Date: {datetime.now().strftime('%d.%m.%Y %H:%M')}", styles['Normal']))

        # Build PDF
        doc.build(elements)
        return output_path


# Usage Example
def generate_daily_report(
    project_name: str,
    location: str,
    timesheet_path: str,
    tasks_path: str,
    output_dir: str
) -> str:
    """Generate daily report from source files"""

    # Initialize generator
    generator = DailyReportGenerator({
        'project_name': project_name,
        'weather_api_key': os.environ.get('WEATHER_API_KEY'),
        'report_date': date.today()
    })

    # Load data
    timesheet = pd.read_excel(timesheet_path)
    tasks = pd.read_excel(tasks_path)

    # Compile report data
    report_data = {
        'report_number': f"DCR-{date.today().strftime('%Y-%j')}",
        'weather': generator.get_weather_data(location),
        'workforce': generator.get_workforce_data(timesheet),
        'work_completed': generator.get_work_completed(tasks),
        'work_planned': generator.get_work_planned(tasks),
        'issues': [],  # Load from issues log if available
        'safety': {
            'incidents': 0,
            'toolbox_talk': ['Fall Protection'],
            'near_misses': 0
        },
        'prepared_by': 'Site Manager'
    }

    # Generate PDF
    output_path = os.path.join(
        output_dir,
        f"Daily_Report_{date.today().strftime('%Y%m%d')}.pdf"
    )

    return generator.generate_report(report_data, output_path)


if __name__ == "__main__":
    report_path = generate_daily_report(
        project_name="ЖК Солнечный, Корпус 2",
        location="Moscow,RU",
        timesheet_path="timesheet.xlsx",
        tasks_path="tasks.xlsx",
        output_dir="./reports"
    )
    print(f"Report generated: {report_path}")

数据源集成

从 n8n 项目管理系统获取数据
# Connect to Google Sheets used by n8n bot
def get_data_from_project_management(spreadsheet_id: str) -> dict:
    """Pull data from n8n project management system"""
    import gspread

    gc = gspread.service_account()
    sh = gc.open_by_key(spreadsheet_id)

    # Get completed tasks
    tasks_sheet = sh.worksheet('Tasks')
    tasks = pd.DataFrame(tasks_sheet.get_all_records())

    # Get workforce from worker responses
    workers_sheet = sh.worksheet('Workers')
    workers = pd.DataFrame(workers_sheet.get_all_records())

    return {
        'tasks': tasks,
        'workers': workers
    }
从考勤表系统获取数据
# Integrate with common timesheet formats
def import_timesheet(source: str, format: str = 'excel') -> pd.DataFrame:
    """Import timesheet data from various sources"""

    if format == 'excel':
        df = pd.read_excel(source)
    elif format == 'csv':
        df = pd.read_csv(source)
    elif format == 'procore':
        df = fetch_procore_timesheet(source)

    # Standardize columns
    df = df.rename(columns={
        'Trade': 'trade',
        'Worker': 'worker_name',
        'Hours': 'hours_worked',
        'Planned Hours': 'planned_hours'
    })

    return df

n8n 自动化工作流

name: Daily Report Automation
trigger:
  type: cron
  expression: "0 18 * * 1-6"  # 6 PM daily

steps:
  - collect_task_data:
      node: Google Sheets
      operation: readRows
      sheet: Tasks
      filter: Date = TODAY()

  - collect_timesheet:
      node: Google Sheets
      operation: readRows
      sheet: Timesheet
      filter: Date = TODAY()

  - get_weather:
      node: HTTP Request
      url: "https://api.openweathermap.org/data/2.5/weather"
      params:
        q: "Moscow,RU"
        appid: "{{$env.WEATHER_API_KEY}}"

  - generate_report:
      node: Code (Python)
      code: |
        from daily_report import generate_report
        return generate_report(items)

  - upload_to_drive:
      node: Google Drive
      operation: upload
      file: "={{$json.report_path}}"
      folder: "Daily Reports"

  - send_notification:
      node: Telegram
      operation: sendDocument
      chatId: "MANAGERS_GROUP_ID"
      document: "={{$json.drive_url}}"
      caption: |
        📋 Daily Report - {{$now.format('DD.MM.YYYY')}}

        Workforce: {{$json.total_workers}} workers
        Tasks completed: {{$json.completed_tasks}}
        Issues: {{$json.open_issues}}

报告分发

def distribute_report(report_path: str, recipients: dict):
    """Distribute report to stakeholders"""

    # Email distribution
    for email in recipients.get('email', []):
        send_email(
            to=email,
            subject=f"Daily Report - {date.today().strftime('%d.%m.%Y')}",
            body="Please find attached the daily construction report.",
            attachment=report_path
        )

    # Telegram distribution
    for chat_id in recipients.get('telegram', []):
        send_telegram_document(
            chat_id=chat_id,
            document_path=report_path,
            caption=f"📋 Daily Report - {date.today().strftime('%d.%m.%Y')}"
        )

    # Upload to project portal
    if portal_url := recipients.get('portal'):
        upload_to_portal(portal_url, report_path)

最佳实践

  1. 数据收集:设置自动化数据收集,尽可能减少手动输入
  2. 审核时间:在分发前预留 5-10 分钟供经理审核
  3. 照片:包含 3-5 张展示进度的关键照片
  4. 问题:明确说明影响和解决日期
  5. 分发:在下午 6-7 点发送,以便相关人员审核

“一份好的日报能在 2 分钟内讲清当天的情况。”

评论

支持 Markdown