mcpField
I
Invoice Generator

Invoice Generator

github.com/jmsktmドキュメント10210 回インストール40 週間前に更新
リポジトリ

Invoice Generator について

Invoice Generatorは、ブランドに合わせた請求書を作成するスキルで、フリーランス、代理店、企業向けです。複数形式やテンプレート、バッチ・定期発行に対応し、税額や割引、合計を計算します。

主な機能

  1. 01PDF、HTML、DOCX形式の請求書作成
  2. 02小計・税・割引・合計の計算
  3. 03テンプレートとブランド要素の適用
  4. 04複数顧客向け請求書の一括生成
  5. 05定期請求書の作成

活用シーン

  1. 01顧客向けブランド請求書の作成
  2. 02複数顧客の請求を一括処理
  3. 03定期的な請求書の自動生成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jmsktm/claude-settings --skill invoice-generator

インストール後、Claude Code で/invoice-generatorを入力して読み込まれたか確認します。

詳細

請求書ジェネレーター

請求書ジェネレーターのスキルを使うと、プロフェッショナルなブランド入り請求書を複数の形式(PDF、HTML、DOCX)で自動生成できます。明細項目、税額計算、割引、支払い条件、企業ブランディングなど、請求書の標準的な構成要素をすべて処理します。プログラムから請求書を生成する必要があるフリーランサー、代理店、企業に最適です。

テンプレートから請求書を作成し、定期請求を自動化し、複数のクライアント向けに請求書を一括生成して、すべての請求書類で一貫したブランディングを維持できます。

主なワークフロー

ワークフロー 1:標準請求書の生成(PDF)

目的: 標準的な要素をすべて含む、プロフェッショナルな PDF 請求書を作成します

手順:

  1. 請求書データ(クライアント情報、明細項目、金額)を収集します
  2. 小計、税額、割引、合計を計算します
  3. 請求書テンプレートを読み込むか、レイアウトを作成します
  4. 企業ブランディング(ロゴ、色、連絡先情報)を追加します
  5. 請求書の詳細(番号、日付、支払期日)を入力します
  6. 説明と金額を記載した明細項目の表を追加します
  7. 支払い条件と方法を表示します
  8. 適切な書式で PDF を生成します

実装:

const PDFDocument = require('pdfkit');
const fs = require('fs');

function generateInvoice(invoiceData, outputPath) {
  const doc = new PDFDocument({ margin: 50 });
  const stream = fs.createWriteStream(outputPath);
  doc.pipe(stream);

  // Company header
  doc.fontSize(20)
     .text(invoiceData.company.name, 50, 50)
     .fontSize(10)
     .text(invoiceData.company.address, 50, 80)
     .text(invoiceData.company.email, 50, 95)
     .text(invoiceData.company.phone, 50, 110);

  // Company logo (if exists)
  if (invoiceData.company.logo) {
    doc.image(invoiceData.company.logo, 450, 50, { width: 100 });
  }

  // Invoice title and number
  doc.fontSize(24)
     .text('INVOICE', 400, 150, { align: 'right' })
     .fontSize(12)
     .text(`Invoice #: ${invoiceData.invoiceNumber}`, 400, 180, { align: 'right' })
     .text(`Date: ${invoiceData.date}`, 400, 195, { align: 'right' })
     .text(`Due: ${invoiceData.dueDate}`, 400, 210, { align: 'right' });

  // Bill to section
  doc.fontSize(12)
     .text('BILL TO:', 50, 150)
     .fontSize(10)
     .text(invoiceData.client.name, 50, 170)
     .text(invoiceData.client.address, 50, 185)
     .text(invoiceData.client.email, 50, 200);

  // Line items table
  const tableTop = 250;
  doc.fontSize(10);

  // Table header
  doc.fillColor('#2C3E50')
     .rect(50, tableTop, 500, 25)
     .fill()
     .fillColor('#FFFFFF')
     .text('Description', 60, tableTop + 8)
     .text('Quantity', 320, tableTop + 8)
     .text('Rate', 390, tableTop + 8)
     .text('Amount', 470, tableTop + 8);

  // Table rows
  let currentY = tableTop + 30;
  doc.fillColor('#000000');

  invoiceData.lineItems.forEach((item, index) => {
    const y = currentY + (index * 25);
    const bgColor = index % 2 === 0 ? '#FFFFFF' : '#F8F9FA';

    doc.fillColor(bgColor)
       .rect(50, y, 500, 25)
       .fill()
       .fillColor('#000000')
       .text(item.description, 60, y + 8, { width: 240 })
       .text(item.quantity, 320, y + 8)
       .text(`$${item.rate.toFixed(2)}`, 390, y + 8)
       .text(`$${(item.quantity * item.rate).toFixed(2)}`, 470, y + 8);
  });

  // Totals section
  const totalsY = currentY + (invoiceData.lineItems.length * 25) + 30;

  doc.text('Subtotal:', 380, totalsY)
     .text(`$${invoiceData.subtotal.toFixed(2)}`, 470, totalsY);

  if (invoiceData.tax) {
    doc.text(`Tax (${invoiceData.taxRate}%):`, 380, totalsY + 20)
       .text(`$${invoiceData.tax.toFixed(2)}`, 470, totalsY + 20);
  }

  if (invoiceData.discount) {
    doc.text('Discount:', 380, totalsY + 40)
       .text(`-$${invoiceData.discount.toFixed(2)}`, 470, totalsY + 40);
  }

  // Total
  doc.fontSize(14)
     .fillColor('#2C3E50')
     .rect(380, totalsY + 60, 170, 30)
     .fill()
     .fillColor('#FFFFFF')
     .text('TOTAL:', 390, totalsY + 68)
     .text(`$${invoiceData.total.toFixed(2)}`, 470, totalsY + 68);

  // Payment terms
  doc.fillColor('#000000')
     .fontSize(10)
     .text('Payment Terms:', 50, totalsY + 120)
     .fontSize(9)
     .text(invoiceData.paymentTerms, 50, totalsY + 135, { width: 500 });

  // Footer
  doc.fontSize(8)
     .fillColor('#7F8C8D')
     .text('Thank you for your business!', 50, doc.page.height - 80, {
       align: 'center',
       width: 500
     });

  doc.end();

  return new Promise((resolve, reject) => {
    stream.on('finish', () => resolve(outputPath));
    stream.on('error', reject);
  });
}
ワークフロー 2:請求書の合計額の計算

目的: 小計、税額、割引、最終合計を自動計算します

手順:

  1. すべての明細項目の金額を合計して小計を算出します
  2. 割引(割合または固定額)を適用します
  3. 割引後の小計に対する税額を計算します
  4. 最終合計を算出します
  5. 小数点以下 2 桁に丸めます
  6. 計算結果を検証します

実装:

function calculateInvoiceTotals(lineItems, options = {}) {
  // Calculate subtotal
  const subtotal = lineItems.reduce((sum, item) => {
    return sum + (item.quantity * item.rate);
  }, 0);

  // Apply discount
  let discount = 0;
  if (options.discountPercent) {
    discount = subtotal * (options.discountPercent / 100);
  } else if (options.discountAmount) {
    discount = options.discountAmount;
  }

  const afterDiscount = subtotal - discount;

  // Calculate tax
  let tax = 0;
  if (options.taxRate) {
    tax = afterDiscount * (options.taxRate / 100);
  }

  // Calculate total
  const total = afterDiscount + tax;

  return {
    subtotal: parseFloat(subtotal.toFixed(2)),
    discount: parseFloat(discount.toFixed(2)),
    discountedSubtotal: parseFloat(afterDiscount.toFixed(2)),
    tax: parseFloat(tax.toFixed(2)),
    taxRate: options.taxRate || 0,
    total: parseFloat(total.toFixed(2)),
    lineItemCount: lineItems.length
  };
}
ワークフロー 3:テンプレートからの生成

目的: 一貫性を保つため、ブランド入り請求書テンプレートを使用します

手順:

  1. プレースホルダーを含む HTML/Word テンプレートを読み込みます
  2. テンプレート変数にデータを割り当てます
  3. プレースホルダーを実際のデータに置き換えます
  4. 明細項目のループを処理します
  5. 合計を計算して挿入します
  6. PDF に変換するか、Word ドキュメントとして保存します
  7. 企業ブランディングを自動的に適用します

実装:

const Handlebars = require('handlebars');
const puppeteer = require('puppeteer');
const fs = require('fs');

async function generateFromTemplate(templatePath, invoiceData, outputPath) {
  // Load template
  const templateHtml = fs.readFileSync(templatePath, 'utf8');
  const template = Handlebars.compile(templateHtml);

  // Register helpers
  Handlebars.registerHelper('currency', function(value) {
    return `$${parseFloat(value).toFixed(2)}`;
  });

  Handlebars.registerHelper('multiply', function(a, b) {
    return (a * b).toFixed(2);
  });

  // Prepare data
  const data = {
    ...invoiceData,
    currentYear: new Date().getFullYear(),
    formattedDate: new Date(invoiceData.date).toLocaleDateString(),
    formattedDueDate: new Date(invoiceData.dueDate).toLocaleDateString()
  };

  // Render template
  const html = template(data);

  // Convert to PDF
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.setContent(html);
  await page.pdf({
    path: outputPath,
    format: 'A4',
    margin: { top: '20mm', right: '20mm', bottom: '20mm', left: '20mm' },
    printBackground: true
  });
  await browser.close();

  return outputPath;
}

// Template example (invoice-template.html):
/*
<html>
<head>
  <style>
    body { font-family: Arial, sans-serif; }
    .header { background: #2C3E50; color: white; padding: 20px; }
    .line-items { width: 100%; border-collapse: collapse; }
    .line-items th { background: #34495E; color: white; padding: 10px; }
    .line-items td { border-bottom: 1px solid #ddd; padding: 8px; }
    .totals { text-align: right; margin-top: 20px; }
  </style>
</head>
<body>
  <div class="header">
    <h1>{{company.name}}</h1>
    <p>{{company.address}}</p>
  </div>
  <h2>Invoice #{{invoiceNumber}}</h2>
  <p>Date: {{formattedDate}} | Due: {{formattedDueDate}}</p>
  <h3>Bill To:</h3>
  <p>{{client.name}}<br>{{client.address}}</p>
  <table class="line-items">
    <thead>
      <tr>
        <th>Description</th>
        <th>Qty</th>
        <th>Rate</th>
        <th>Amount</th>
      </tr>
    </thead>
    <tbody>
      {{#each lineItems}}
      <tr>
        <td>{{description}}</td>
        <td>{{quantity}}</td>
        <td>{{currency rate}}</td>
        <td>{{currency (multiply quantity rate)}}</td>
      </tr>
      {{/each}}
    </tbody>
  </table>
  <div class="totals">
    <p>Subtotal: {{currency subtotal}}</p>
    {{#if tax}}
    <p>Tax ({{taxRate}}%): {{currency tax}}</p>
    {{/if}}
    <h3>Total: {{currency total}}</h3>
  </div>
  <p>{{paymentTerms}}</p>
</body>
</html>
*/
ワークフロー 4:請求書の一括生成

目的: 異なるクライアント向けの複数の請求書を一度に生成します

手順:

  1. 請求書の詳細を含むクライアントリストを読み込みます
  2. 各クライアントについて、次の処理を行います:
    • 請求書データを準備します
    • 合計を計算します
    • 請求書(PDF またはその他の形式)を生成します
    • 一意のファイル名で保存します
  3. 生成の成功/失敗を記録します
  4. 生成したすべての請求書の概要レポートを作成します
  5. 必要に応じて、請求書をメールで送信します

実装:

async function batchGenerateInvoices(invoices, outputDir) {
  const results = [];

  for (const invoice of invoices) {
    try {
      // Calculate totals
      const totals = calculateInvoiceTotals(invoice.lineItems, {
        taxRate: invoice.taxRate,
        discountPercent: invoice.discountPercent
      });

      const invoiceData = {
        ...invoice,
        ...totals
      };

      // Generate filename
      const filename = `${outputDir}/invoice-${invoice.invoiceNumber}-${invoice.client.name.replace(/\s+/g, '-')}.pdf`;

      // Generate invoice
      await generateInvoice(invoiceData, filename);

      results.push({
        success: true,
        invoiceNumber: invoice.invoiceNumber,
        client: invoice.client.name,
        total: totals.total,
        filename: filename
      });
    } catch (error) {
      results.push({
        success: false,
        invoiceNumber: invoice.invoiceNumber,
        client: invoice.client.name,
        error: error.message
      });
    }
  }

  // Generate summary report
  const summary = {
    totalInvoices: invoices.length,
    successful: results.filter(r => r.success).length,
    failed: results.filter(r => !r.success).length,
    totalAmount: results
      .filter(r => r.success)
      .reduce((sum, r) => sum + r.total, 0),
    results: results
  };

  fs.writeFileSync(
    `${outputDir}/batch-summary.json`,
    JSON.stringify(summary, null, 2)
  );

  return summary;
}
ワークフロー 5:定期請求書の生成

目的: 月次/週次の定期請求書を自動化します

手順:

  1. 定期請求書のテンプレートを読み込みます
  2. 請求書生成の期日を確認します
  3. 期日を迎えた各請求書について、次の処理を行います:
    • テンプレートデータを複製します
    • 請求書番号を更新します(加算)
    • 日付(請求日、支払期日)を更新します
    • 新しい請求書を生成します
  4. 次回請求日を更新します
  5. 請求書の記録を保存します
  6. 必要に応じて、メール通知を送信します

実装:

function generateRecurringInvoices(recurringInvoices) {
  const today = new Date();
  const generated = [];

  recurringInvoices.forEach(template => {
    const nextBillingDate = new Date(template.nextBillingDate);

    if (nextBillingDate <= today) {
      // Generate invoice
      const invoiceData = {
        ...template,
        invoiceNumber: generateNextInvoiceNumber(template.invoiceNumberPrefix),
        date: today.toISOString().split('T')[0],
        dueDate: calculateDueDate(today, template.paymentTermsDays)
      };

      const totals = calculateInvoiceTotals(invoiceData.lineItems, {
        taxRate: template.taxRate
      });

      generateInvoice({ ...invoiceData, ...totals }, `invoices/invoice-${invoiceData.invoiceNumber}.pdf`);

      // Update next billing date
      template.nextBillingDate = calculateNextBillingDate(
        nextBillingDate,
        template.frequency // 'monthly', 'weekly', 'quarterly'
      );

      generated.push(invoiceData);
    }
  });

  return generated;
}

function calculateDueDate(fromDate, days) {
  const dueDate = new Date(fromDate);
  dueDate.setDate(dueDate.getDate() + days);
  return dueDate.toISOString().split('T')[0];
}

function calculateNextBillingDate(currentDate, frequency) {
  const nextDate = new Date(currentDate);
  switch (frequency) {
    case 'weekly':
      nextDate.setDate(nextDate.getDate() + 7);
      break;
    case 'monthly':
      nextDate.setMonth(nextDate.getMonth() + 1);
      break;
    case 'quarterly':
      nextDate.setMonth(nextDate.getMonth() + 3);
      break;
    case 'yearly':
      nextDate.setFullYear(nextDate.getFullYear() + 1);
      break;
  }
  return nextDate.toISOString().split('T')[0];
}

クイックリファレンス

アクションコマンド/トリガー
請求書を生成「[client] の請求書を作成」
テンプレートから作成「[client] に請求書テンプレートを使用」
合計を計算「請求書の合計を計算」
一括生成「[clients] の請求書を作成」
定期請求書「定期請求書を生成」
明細項目を追加「請求書に項目を追加」
割引を適用「[%] の割引を適用」
支払い済みにする「請求書 [number] を支払い済みにする」

ベストプラクティス

  • 請求書番号の付与: 接頭辞を付けた連番を使用します(例:INV-2025-001)
  • 支払期限: 支払条件(Net 30、Net 15 など)を明確に記載します
  • 明細: 各明細項目の詳細な説明を記載します
  • 税務コンプライアンス: 管轄区域に応じた適切な税率を適用します
  • 支払方法: 利用可能なすべての支払方法を明確に記載します
  • 連絡先情報: 請求に関する問い合わせ先として、メールアドレスと電話番号を記載します
  • 通貨: 通貨(USD、EUR など)を必ず明記します
  • バックアップ: 生成したすべての請求書のコピーを保管します
  • バージョン管理: 変更が必要な場合に備え、請求書の改訂履歴を記録します
  • ブランディング: すべての請求書で会社のブランドを一貫させます
  • 法的情報: 必要に応じて、事業者登録番号や納税者番号を記載します
  • 延滞料金: 該当する場合は、支払いが遅れた際の違約金を明記します
  • アクセシビリティ: PDF がスクリーンリーダーで読み取れることを確認します

よくあるパターン

フリーランス向け請求書:

const freelanceInvoice = {
  company: {
    name: 'Your Name',
    address: '123 Main St, City, State 12345',
    email: '[email protected]',
    phone: '(555) 123-4567'
  },
  client: {
    name: 'Client Co.',
    address: '456 Client Ave, City, State 67890',
    email: '[email protected]'
  },
  invoiceNumber: 'INV-2025-001',
  date: '2025-01-15',
  dueDate: '2025-02-14', // Net 30
  lineItems: [
    { description: 'Website Design - January 2025', quantity: 1, rate: 3000 },
    { description: 'Consultation (5 hours)', quantity: 5, rate: 150 }
  ],
  taxRate: 0, // No tax for services in some jurisdictions
  paymentTerms: 'Payment due within 30 days. Accepted methods: Bank transfer, PayPal, Check.'
};

プロジェクトベースの請求書:

const projectInvoice = {
  lineItems: [
    { description: 'Phase 1: Discovery & Planning', quantity: 1, rate: 5000 },
    { description: 'Phase 2: Design Mockups', quantity: 1, rate: 7500 },
    { description: 'Phase 3: Development', quantity: 1, rate: 15000 },
    { description: 'Additional Revisions (8 hours)', quantity: 8, rate: 150 }
  ],
  discountPercent: 10, // Early payment discount
  taxRate: 8.5
};

依存関係

必要なパッケージをインストールします:

npm install pdfkit          # For PDF generation
npm install handlebars      # For templating
npm install puppeteer       # For HTML to PDF conversion
npm install nodemailer      # For email sending (optional)

エラー処理

  • データの不足: 生成前に必須フィールドがすべて入力されていることを検証します
  • 無効な金額: すべての金額が有効な数値であることを確認します
  • 日付の検証: 日付形式と整合性(支払期限が請求日より後であること)を確認します
  • ファイル権限: 請求書の保存時に発生する書き込みエラーに対処します
  • テンプレートエラー: テンプレートのレンダリングエラーを適切に捕捉します
  • 通貨の精度: 通貨は必ず小数点以下 2 桁に丸めます

パフォーマンスのヒント

  • 会社のロゴやブランディング素材をキャッシュします
  • 一括生成では PDF/テンプレートのインスタンスを再利用します
  • 処理前にデータを事前検証します
  • 大規模な一括処理ではストリーミングを使用します
  • メール送信に備えて PDF を圧縮します

高度な機能

請求書のメール送信:

const nodemailer = require('nodemailer');

async function emailInvoice(invoiceData, pdfPath) {
  const transporter = nodemailer.createTransport({ /* config */ });

  await transporter.sendMail({
    from: invoiceData.company.email,
    to: invoiceData.client.email,
    subject: `Invoice ${invoiceData.invoiceNumber} from ${invoiceData.company.name}`,
    html: `
      <p>Dear ${invoiceData.client.name},</p>
      <p>Please find attached invoice ${invoiceData.invoiceNumber} for $${invoiceData.total}.</p>
      <p>Payment is due by ${invoiceData.dueDate}.</p>
      <p>Thank you for your business!</p>
    `,
    attachments: [
      {
        filename: `invoice-${invoiceData.invoiceNumber}.pdf`,
        path: pdfPath
      }
    ]
  });
}

支払いの追跡:

const invoiceStatus = {
  invoiceNumber: 'INV-2025-001',
  status: 'unpaid', // 'unpaid', 'paid', 'overdue', 'cancelled'
  paidDate: null,
  paidAmount: 0,
  paymentMethod: null
};

コメント