{
  "name": "Turn order mails into freee invoice drafts from the quotation, checked by code",
  "nodes": [
    {
      "id": "c8000000-0000-4000-8000-000000000001",
      "name": "New order mail",
      "type": "n8n-nodes-base.gmailTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ],
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "simple": false,
        "filters": {
          "q": "subject:(order OR 発注 OR 注文) -subject:\"[n8n order]\""
        },
        "options": {}
      }
    },
    {
      "id": "c8000000-0000-4000-8000-000000000002",
      "name": "Read the mail",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ],
      "parameters": {
        "jsCode": "// Sender, subject and the plain text of the mail.\nconst m = $json;\nconst from = (m.from && m.from.value && m.from.value[0]) || {};\nconst text = String(m.text || m.textAsHtml || m.snippet || '').replace(/\\r/g, '').trim();\nreturn [{ json: { source: 'gmail', from_email: String(from.address || '').toLowerCase(), subject: m.subject || '', text } }];"
      }
    },
    {
      "id": "c8000000-0000-4000-8000-000000000003",
      "name": "Paste an order",
      "type": "n8n-nodes-base.formTrigger",
      "typeVersion": 2.6,
      "position": [
        0,
        300
      ],
      "parameters": {
        "formTitle": "Make a freee invoice from an order",
        "formDescription": "Paste a customer's order (for example the reply to your quotation). An invoice is created in freee as unsent, from the customer's quotation; nobody receives anything until you send it from freee.",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Customer e-mail",
              "fieldName": "Email",
              "fieldType": "email",
              "requiredField": false,
              "placeholder": "buyer@example.com"
            },
            {
              "fieldLabel": "Order",
              "fieldName": "Order",
              "fieldType": "textarea",
              "requiredField": true,
              "placeholder": "We would like to order as quoted (Q-0000000004), but 800 brochures instead of 1,000.\n\nExample Trading Co., Ltd. / Sato"
            }
          ]
        },
        "responseMode": "lastNode",
        "options": {
          "path": "order-to-freee-invoice"
        }
      },
      "webhookId": "5e1b7c3a-9d24-4f68-a0e3-2c8d6f4b1a97"
    },
    {
      "id": "c8000000-0000-4000-8000-000000000004",
      "name": "Read the form",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        300
      ],
      "parameters": {
        "jsCode": "return [{ json: { source: 'form', from_email: String($json.Email || '').trim().toLowerCase(), subject: 'Order from the form', text: String($json.Order || '').replace(/\\r/g, '').trim() } }];"
      }
    },
    {
      "id": "c8000000-0000-4000-8000-000000000005",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        440,
        150
      ],
      "parameters": {
        "mode": "manual",
        "includeOtherFields": true,
        "assignments": {
          "assignments": [
            {
              "id": "c01",
              "name": "freee_company_id",
              "value": "PASTE_YOUR_FREEE_COMPANY_ID",
              "type": "string"
            },
            {
              "id": "c02",
              "name": "staff_email",
              "value": "staff@example.com",
              "type": "string"
            },
            {
              "id": "c03",
              "name": "payment_terms",
              "value": "end_of_next_month",
              "type": "string"
            },
            {
              "id": "c04",
              "name": "payment_days",
              "value": 30,
              "type": "number"
            },
            {
              "id": "c05",
              "name": "max_quotations",
              "value": 5,
              "type": "number"
            },
            {
              "id": "c06",
              "name": "timezone",
              "value": "Asia/Tokyo",
              "type": "string"
            }
          ]
        }
      }
    },
    {
      "id": "c8000000-0000-4000-8000-000000000006",
      "name": "Read freee partners",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        150
      ],
      "parameters": {
        "url": "=https://api.freee.co.jp/api/1/partners?company_id={{ $json.freee_company_id }}&limit=3000",
        "authentication": "genericCredentialType",
        "genericAuthType": "oAuth2Api",
        "options": {
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        }
      }
    },
    {
      "id": "c8000000-0000-4000-8000-000000000007",
      "name": "Find the customer",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        150
      ],
      "parameters": {
        "jsCode": "// The customer must be an existing freee partner: the sender's e-mail, or exactly one partner name written in the mail.\nconst c = $('Config').first().json;\nif ($json.statusCode !== 200) return [{ json: { ok: false, problem: 'Could not read the partners in freee (HTTP ' + $json.statusCode + '). Check the freee credential, the app permissions and freee_company_id.' } }];\nif (!c.text) return [{ json: { ok: false, problem: 'The order is empty.' } }];\nconst norm = (s) => String(s ?? '').normalize('NFKC').toLowerCase().replace(/\\s/g, '');\nconst partners = ($json.body.partners || []).filter((p) => p.available !== false);\nlet partner = c.from_email ? partners.find((p) => String(p.email || '').toLowerCase() === c.from_email) : null;\nlet by = partner ? 'e-mail ' + c.from_email : '';\nif (!partner) {\n  const text = norm(c.text);\n  const hits = partners.filter((p) => [p.name, p.long_name].some((v) => v && norm(v).length >= 4 && text.includes(norm(v))));\n  if (hits.length === 1) { partner = hits[0]; by = 'company name \"' + partner.name + '\" in the mail'; }\n}\nif (!partner) return [{ json: { ok: false, problem: 'The customer was not found among the freee partners' + (c.from_email ? ' (e-mail: ' + c.from_email + ')' : '') + '. No invoice was made.' } }];\nreturn [{ json: { ok: true, partner: { id: partner.id, name: partner.name, by } } }];"
      }
    },
    {
      "id": "c8000000-0000-4000-8000-000000000008",
      "name": "Customer found?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1100,
        150
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "ok",
              "leftValue": "={{ $json.ok }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "c8000000-0000-4000-8000-000000000009",
      "name": "Read their quotations",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1320,
        50
      ],
      "parameters": {
        "url": "=https://api.freee.co.jp/iv/quotations?company_id={{ $('Config').first().json.freee_company_id }}&partner_ids={{ $json.partner.id }}&cancel_status=uncanceled&limit=50",
        "authentication": "genericCredentialType",
        "genericAuthType": "oAuth2Api",
        "options": {
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        }
      }
    },
    {
      "id": "c8000000-0000-4000-8000-000000000010",
      "name": "Pick the candidates",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1540,
        50
      ],
      "parameters": {
        "jsCode": "// The newest open quotations of this customer; one the mail names (Q-...) comes first.\nconst c = $('Config').first().json;\nif ($json.statusCode !== 200) return [{ json: { ok: false, problem: 'Could not read quotations in freee Invoice (HTTP ' + $json.statusCode + '). Check the app permission for invoices and that freee Invoice is in use.' } }];\nconst all = ($json.body.quotations || []).slice().sort((a, b) => String(b.quotation_date).localeCompare(String(a.quotation_date)) || b.id - a.id);\nif (!all.length) return [{ json: { ok: false, problem: 'This customer has no open quotation in freee, so no invoice was made.' } }];\nconst named = (String(c.text).normalize('NFKC').match(/Q-?\\d{3,}/gi) || []).map((s) => s.toUpperCase());\nconst first = all.filter((q) => named.some((x) => x.replace('-', '') === String(q.quotation_number).toUpperCase().replace('-', '')));\nconst pick = [...first, ...all.filter((q) => !first.includes(q))].slice(0, Number(c.max_quotations));\nreturn pick.map((q) => ({ json: { ok: true, id: q.id } }));"
      }
    },
    {
      "id": "c8000000-0000-4000-8000-000000000011",
      "name": "Any quotation?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1760,
        50
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "ok",
              "leftValue": "={{ $json.ok }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "c8000000-0000-4000-8000-000000000012",
      "name": "Read each quotation",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1980,
        -50
      ],
      "parameters": {
        "url": "=https://api.freee.co.jp/iv/quotations/{{ $json.id }}?company_id={{ $('Config').first().json.freee_company_id }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "oAuth2Api",
        "options": {
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        }
      }
    },
    {
      "id": "c8000000-0000-4000-8000-000000000013",
      "name": "Read their invoices",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2200,
        -50
      ],
      "parameters": {
        "url": "=https://api.freee.co.jp/iv/invoices?company_id={{ $('Config').first().json.freee_company_id }}&partner_ids={{ $('Find the customer').first().json.partner.id }}&limit=100",
        "authentication": "genericCredentialType",
        "genericAuthType": "oAuth2Api",
        "options": {
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        }
      },
      "executeOnce": true
    },
    {
      "id": "c8000000-0000-4000-8000-000000000014",
      "name": "Prepare the prompt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2420,
        -50
      ],
      "parameters": {
        "jsCode": "// The candidates with their lines (no prices) and the mail, for Claude.\nconst c = $('Config').first().json;\nconst qs = $('Read each quotation').all().map((i) => i.json).filter((r) => r.statusCode === 200).map((r) => r.body.quotation);\nif (!qs.length) return [{ json: { ok: false, problem: 'Could not read the quotations in freee.' } }];\nconst inv = $json;\nif (inv.statusCode !== 200) return [{ json: { ok: false, problem: 'Could not read the invoices in freee (HTTP ' + inv.statusCode + ').' } }];\nconst list = qs.map((q) => q.quotation_number + ' | ' + q.subject + ' | dated ' + q.quotation_date + (q.expiration_date ? ', valid until ' + q.expiration_date : '') + '\\n' +\n  (q.lines || []).filter((l) => l.type !== 'text').map((l) => '  - ' + l.description + ': ' + Number(l.quantity) + ' ' + (l.unit || '')).join('\\n')).join('\\n\\n');\nreturn [{ json: { ok: true, quotations: qs, invoices: inv.body.invoices || [], list, text: c.text.slice(0, 8000) } }];"
      }
    },
    {
      "id": "c8000000-0000-4000-8000-000000000015",
      "name": "Ready?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2640,
        -50
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "ok",
              "leftValue": "={{ $json.ok }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "c8000000-0000-4000-8000-000000000016",
      "name": "Claude Haiku 4.5",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "typeVersion": 1.5,
      "position": [
        2860,
        170
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-haiku-4-5-20251001",
          "cachedResultName": "Claude Haiku 4.5"
        },
        "options": {}
      }
    },
    {
      "id": "c8000000-0000-4000-8000-000000000017",
      "name": "Read the order",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.9,
      "position": [
        2860,
        -50
      ],
      "parameters": {
        "promptType": "define",
        "text": "={{ 'Open quotations of this customer (number | subject | dates, then lines with quantities):\\n' + $json.list + '\\n\\nMail from the customer:\\n' + $json.text }}",
        "messages": {
          "messageValues": [
            {
              "type": "SystemMessagePromptTemplate",
              "message": "You read a customer's mail and decide whether it places an order based on one of their quotations. You never set or mention prices.\nReturn:\n- \"is_order\": \"yes\" only if the customer clearly orders (for example \"please proceed\", \"we order as quoted\"); \"no\" if it is not an order (a question, a new request, a cancellation); otherwise \"unclear\".\n- \"order_quote\": the exact words from the mail, copied character by character, that show the order (or null).\n- \"quotation_number\": the number of the quotation the order is for, exactly as listed, or null if you cannot tell which one.\n- \"changes\": quantity changes the customer asks for: [{\"line\": \"the line description exactly as listed\", \"new_quantity\": number written in the mail, \"quote\": \"exact words from the mail with the new quantity\"}]. Do not calculate quantities.\n- \"additions\": items the customer adds that are not in the quotation: [{\"requested\": \"as written\", \"quantity\": number or null, \"unit\": \"as written or null\", \"quote\": \"exact words\"}].\n- \"reason\": one short sentence, in the language of the mail, saying why you chose \"no\" or \"unclear\" (empty for \"yes\").\nReturn only JSON: {\"is_order\": \"yes\", \"order_quote\": \"...\", \"quotation_number\": \"...\", \"changes\": [], \"additions\": [], \"reason\": \"\"}"
            }
          ]
        }
      }
    },
    {
      "id": "c8000000-0000-4000-8000-000000000018",
      "name": "Check and build",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3080,
        -50
      ],
      "parameters": {
        "jsCode": "// Code decides. Prices, tax rates and the line list come from the quotation in freee, never from the AI.\nconst p = $('Prepare the prompt').first().json;\nconst c = $('Config').first().json;\nconst cust = $('Find the customer').first().json.partner;\nconst norm = (s) => String(s ?? '').normalize('NFKC').toLowerCase().replace(/[\\s,、。「」\"'“”]/g, '');\nconst numIn = (text, q) => new RegExp('(^|[^\\\\d.])' + String(q).replace('.', '\\\\.') + '([^\\\\d.]|$)').test(String(text).normalize('NFKC').replace(/,/g, ''));\nlet a;\ntry { a = JSON.parse(String($json.text ?? '').match(/\\{[\\s\\S]*\\}/)[0]); }\ncatch (e) { return [{ json: { ok: false, problem: 'Could not read the AI answer as JSON. Try again.' } }]; }\nconst text = norm(p.text);\nconst inText = (s) => norm(s).length >= 2 && text.includes(norm(s));\nconst why = a.reason ? ' The AI: \"' + String(a.reason).slice(0, 300) + '\"' : '';\nif (a.is_order === 'no') return [{ json: { ok: false, problem: 'This mail is not an order, so no invoice was made.' + why } }];\nif (a.is_order !== 'yes') return [{ json: { ok: false, problem: 'The AI could not tell clearly what is ordered' + (inText(a.order_quote) ? ' (order words: \"' + a.order_quote + '\")' : '') + ', so no invoice was made. Make it by hand.' + why } }];\nif (!inText(a.order_quote)) return [{ json: { ok: false, problem: 'The words the AI read as an order are not in the mail, so no invoice was made.' } }];\n// Which quotation: a number written in the mail wins; without one, only if the customer has a single open quotation\nconst named = (String(p.text).normalize('NFKC').match(/Q-?\\d{3,}/gi) || []).map((s) => s.toUpperCase().replace('-', ''));\nconst key = (q) => String(q.quotation_number).toUpperCase().replace('-', '');\nlet q = p.quotations.find((x) => x.quotation_number === a.quotation_number);\nif (named.length && !(q && named.includes(key(q)))) q = p.quotations.find((x) => named.includes(key(x)));\nif (!q) return [{ json: { ok: false, problem: 'Could not tell which quotation the order is for. Write the quotation number in the reply, or make the invoice by hand.' } }];\nif (!named.length && p.quotations.length > 1) return [{ json: { ok: false, problem: 'This customer has ' + p.quotations.length + ' open quotations and the mail names none of them (the AI guessed ' + q.quotation_number + '). Make the invoice by hand, or ask the customer for the number.' } }];\n// Already invoiced from this quotation?\nconst dup = p.invoices.find((i) => i.cancel_status !== 'canceled' && String(i.memo || '').includes(q.quotation_number));\nif (dup) return [{ json: { ok: false, problem: 'An invoice from ' + q.quotation_number + ' already exists (' + dup.invoice_number + '). No second invoice was made.' } }];\nconst today = DateTime.now().setZone(c.timezone);\nconst warnings = [];\nif (q.expiration_date && q.expiration_date < today.toFormat('yyyy-MM-dd')) warnings.push(q.quotation_number + ' expired on ' + q.expiration_date + '. Check that the prices still apply.');\n// Quantity changes: the line must be in the quotation, the new number must be written in the mail\nconst items = (q.lines || []).map((l) => ({ ...l }));\nconst applied = [], check = [];\nfor (const ch of Array.isArray(a.changes) ? a.changes : []) {\n  const line = items.find((l) => l.type !== 'text' && norm(l.description) === norm(ch.line)) || items.find((l) => l.type !== 'text' && norm(ch.line).length >= 2 && norm(l.description).includes(norm(ch.line)));\n  const qty = Number(ch.new_quantity);\n  if (line && Number.isFinite(qty) && qty > 0 && inText(ch.quote) && numIn(ch.quote, qty)) { applied.push({ line: line.description, from: Number(line.quantity), to: qty, quote: ch.quote }); line.quantity = qty; }\n  else check.push({ what: (ch.line || '?') + ' → ' + ch.new_quantity, why: !line ? 'no such line in ' + q.quotation_number : !inText(ch.quote) ? 'the quoted words are not in the mail' : 'the new quantity is not written in the mail' });\n}\nfor (const ad of Array.isArray(a.additions) ? a.additions : []) {\n  if (!inText(ad.quote)) continue;\n  // The same kind of item may already be in the quotation: say so, and leave the total quantity to a person\n  const same = items.find((l) => l.type !== 'text' && norm(ad.requested).length >= 2 && norm(l.description).includes(norm(ad.requested)));\n  check.push({ what: ad.requested + (ad.quantity ? ' ' + ad.quantity + (ad.unit || '') : ''), why: same ? q.quotation_number + ' already has \"' + same.description + '\" (' + Number(same.quantity) + (same.unit || '') + '); the mail asks for this in addition — decide the quantity' : 'not in ' + q.quotation_number + ' (no price)' });\n}\nconst lines = items.map((l) => l.type === 'text' ? { type: 'text', description: l.description }\n  : { type: 'item', description: l.description, quantity: Number(l.quantity), unit: l.unit || undefined, unit_price: String(l.unit_price), tax_rate: l.tax_rate, ...(l.reduced_tax_rate ? { reduced_tax_rate: true } : {}) })\n  .concat(check.map((x) => ({ type: 'text', description: ('※要確認 / To check: ' + x.what + ' (' + x.why + ')').slice(0, 255) })));\n// Our own totals (tax rounded down per rate, as tax_fraction \"omit\")\nconst sub = {}; let net = 0;\nfor (const l of lines.filter((x) => x.type === 'item')) { const v = Math.round(l.quantity * Number(l.unit_price)); net += v; sub[l.tax_rate] = (sub[l.tax_rate] || 0) + v; }\nconst tax = Object.entries(sub).reduce((s, [r, v]) => s + Math.floor(v * Number(r) / 100), 0);\nconst due = c.payment_terms === 'end_of_next_month' ? today.plus({ months: 1 }).endOf('month') : today.plus({ days: Number(c.payment_days) });\nconst body = {\n  company_id: Number(c.freee_company_id), billing_date: today.toFormat('yyyy-MM-dd'), payment_date: due.toFormat('yyyy-MM-dd'), payment_type: 'transfer',\n  subject: (String(q.subject).replace(/s*(の)?(お見積(もり|り)?|御見積(り|書)?|見積(もり|り|書)?|quotation|quote)s*/gi, '').trim() || q.subject).slice(0, 255), tax_entry_method: q.tax_entry_method, tax_fraction: q.tax_fraction, withholding_tax_entry_method: q.withholding_tax_entry_method || 'out',\n  partner_id: q.partner_id, partner_title: q.partner_title || '御中',\n  memo: ('From quotation ' + q.quotation_number + ' (n8n, order mail from ' + (c.from_email || c.source) + ')').slice(0, 2000), lines,\n};\nreturn [{ json: { ok: true, quotation: { number: q.quotation_number, total: q.total_amount }, customer: cust, applied, check, warnings, order_quote: a.order_quote, expected: { net, tax, total: net + tax }, body } }];"
      }
    },
    {
      "id": "c8000000-0000-4000-8000-000000000019",
      "name": "Can make an invoice?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3300,
        -50
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "ok",
              "leftValue": "={{ $json.ok }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "c8000000-0000-4000-8000-000000000020",
      "name": "Create the invoice in freee",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3520,
        -150
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.freee.co.jp/iv/invoices",
        "authentication": "genericCredentialType",
        "genericAuthType": "oAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "options": {
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        }
      }
    },
    {
      "id": "c8000000-0000-4000-8000-000000000021",
      "name": "Write the notice",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3740,
        150
      ],
      "parameters": {
        "jsCode": "// One notice for every outcome.\nconst c = $('Config').first().json;\nconst chk = $('Check and build').isExecuted ? $('Check and build').all(0)[0].json : null;\nconst created = $('Create the invoice in freee').isExecuted ? $('Create the invoice in freee').all(0)[0].json : null;\nconst yen = (v) => Number(v).toLocaleString('ja-JP') + ' yen';\nconst lines = [];\nlet title, ok = false;\nif (created && created.statusCode === 201) {\n  const iv = created.body.invoice;\n  ok = true;\n  title = '[n8n order] Invoice draft in freee: ' + iv.partner_name + ' ' + yen(iv.total_amount);\n  lines.push('An invoice was created in freee as UNSENT, from ' + chk.quotation.number + '. Check it (and the billing date after delivery) and send it from freee.', iv.report_url || '', '');\n  lines.push(iv.invoice_number + ' / ' + iv.subject + ' / ' + iv.partner_name + ' (' + chk.customer.by + ')');\n  lines.push('Order: \"' + chk.order_quote + '\"');\n  chk.applied.forEach((x) => lines.push('Changed: ' + x.line + ' ' + x.from + ' -> ' + x.to + '  (from: \"' + x.quote + '\")'));\n  lines.push('Net ' + yen(iv.amount_excluding_tax) + ', tax ' + yen(iv.amount_tax) + ', total ' + yen(iv.total_amount) + ' (quotation: ' + yen(chk.quotation.total) + '), due ' + iv.payment_date);\n  if (iv.amount_excluding_tax !== chk.expected.net || iv.amount_tax !== chk.expected.tax) lines.push('!! freee calculated different amounts than this workflow (net ' + yen(chk.expected.net) + ', tax ' + yen(chk.expected.tax) + '). Check the invoice.');\n} else {\n  title = '[n8n order] No invoice was made';\n  if (created) lines.push('freee refused the invoice (HTTP ' + created.statusCode + '): ' + JSON.stringify(created.body && (created.body.errors || created.body)).slice(0, 400));\n  else lines.push($json.problem || 'Unknown problem.');\n}\nif (chk && chk.warnings && chk.warnings.length) { lines.push(''); chk.warnings.forEach((w) => lines.push('!! ' + w)); }\nif (chk && chk.check && chk.check.length) { lines.push('', 'To check (in the invoice as \"※要確認\" lines, no price):'); chk.check.forEach((x) => lines.push('- ' + x.what + ': ' + x.why)); }\nlines.push('', 'Mail from ' + (c.from_email || c.source) + ': ' + c.subject);\nreturn [{ json: { ok, title, message: lines.join('\\n'), source: c.source, to: c.staff_email } }];"
      }
    },
    {
      "id": "c8000000-0000-4000-8000-000000000022",
      "name": "Answer where asked",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        3960,
        150
      ],
      "parameters": {
        "mode": "expression",
        "numberOutputs": 2,
        "output": "={{ $json.source === 'form' ? 0 : 1 }}"
      }
    },
    {
      "id": "c8000000-0000-4000-8000-000000000023",
      "name": "Show the result",
      "type": "n8n-nodes-base.form",
      "typeVersion": 2.5,
      "position": [
        4180,
        50
      ],
      "parameters": {
        "operation": "completion",
        "respondWith": "text",
        "completionTitle": "={{ $json.title }}",
        "completionMessage": "={{ $json.message }}"
      },
      "webhookId": "8b4f2e6d-3a71-4c95-b0d8-7e2a5c9f1b36"
    },
    {
      "id": "c8000000-0000-4000-8000-000000000024",
      "name": "Tell the staff",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        4180,
        250
      ],
      "parameters": {
        "resource": "message",
        "operation": "send",
        "sendTo": "={{ $json.to }}",
        "subject": "={{ $json.title }}",
        "emailType": "text",
        "message": "={{ $json.message }}",
        "options": {
          "appendAttribution": false
        }
      }
    },
    {
      "parameters": {
        "content": "## Turn order mails into freee invoice drafts, from the quotation\nA customer replies \"we order as quoted (Q-0000000004), but 800 brochures instead of 1,000\". **The invoice is created in freee Invoice as unsent, from that quotation.** Prices, tax rates and lines are copied from the quotation by code; the AI never sets a price.\n\n**Claude** only reads the mail: is it an order, for which quotation, which quantities change, quoting the words behind each. **Code** checks: the quotation exists and is open, it was not invoiced before (memo \"From quotation ...\"), the new quantities are written in the mail. Additions and anything unclear become \"※要確認 / To check\" lines without a price, or no invoice at all.\n\n### Setup\n1. A freee app with permissions for partners (read) and [請求書] all document types (read/write); freee Invoice in use.\n2. n8n **OAuth2 API** credential for freee (auth URL https://accounts.secure.freee.co.jp/public_api/authorize, token URL .../public_api/token, authentication: Body). Also Gmail and Anthropic.\n3. Fill **Config** and the Gmail search in **New order mail**. Try the form first.",
        "height": 1100,
        "width": 600,
        "color": 1
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -660,
        -320
      ],
      "id": "c8000000-0000-4000-9000-000000000024",
      "name": "Overview"
    },
    {
      "parameters": {
        "content": "## 1. An order comes in\nGmail (the search decides which mails count) or the form.",
        "height": 620,
        "width": 400,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -20,
        -120
      ],
      "id": "c8000000-0000-4000-9000-000000000025",
      "name": "Section 1"
    },
    {
      "parameters": {
        "content": "## 2. The customer\nAn existing freee partner: the sender e-mail, or exactly one partner name written in the mail. Partners are never created.",
        "height": 620,
        "width": 840,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        420,
        -120
      ],
      "id": "c8000000-0000-4000-9000-000000000026",
      "name": "Section 2"
    },
    {
      "parameters": {
        "content": "## 3. Their open quotations and invoices\nThe newest open quotations (a number named in the mail first), each with its lines, and the invoices already made for this customer.",
        "height": 220,
        "width": 1300,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1280,
        -320
      ],
      "id": "c8000000-0000-4000-9000-000000000027",
      "name": "Section 3"
    },
    {
      "parameters": {
        "content": "## 4. Claude reads, code decides\nOrder or not (with the words), which quotation, quantity changes (with the words). Code stops when it is not an order, the quotation is unclear, or it was invoiced already; it copies every price from the quotation.",
        "height": 820,
        "width": 880,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2600,
        -320
      ],
      "id": "c8000000-0000-4000-9000-000000000028",
      "name": "Section 4"
    },
    {
      "parameters": {
        "content": "## 5. freee invoice (unsent) and the notice\nPOST to freee Invoice, due date from Config (end of next month by default). The staff get the link, what changed and from which words, what to check, or why nothing was made.",
        "height": 820,
        "width": 900,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        3500,
        -320
      ],
      "id": "c8000000-0000-4000-9000-000000000029",
      "name": "Section 5"
    }
  ],
  "connections": {
    "New order mail": {
      "main": [
        [
          {
            "node": "Read the mail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Paste an order": {
      "main": [
        [
          {
            "node": "Read the form",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the mail": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the form": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "Read freee partners",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read freee partners": {
      "main": [
        [
          {
            "node": "Find the customer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find the customer": {
      "main": [
        [
          {
            "node": "Customer found?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Customer found?": {
      "main": [
        [
          {
            "node": "Read their quotations",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Write the notice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read their quotations": {
      "main": [
        [
          {
            "node": "Pick the candidates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick the candidates": {
      "main": [
        [
          {
            "node": "Any quotation?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Any quotation?": {
      "main": [
        [
          {
            "node": "Read each quotation",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Write the notice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read each quotation": {
      "main": [
        [
          {
            "node": "Read their invoices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read their invoices": {
      "main": [
        [
          {
            "node": "Prepare the prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare the prompt": {
      "main": [
        [
          {
            "node": "Ready?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ready?": {
      "main": [
        [
          {
            "node": "Read the order",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Write the notice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Haiku 4.5": {
      "ai_languageModel": [
        [
          {
            "node": "Read the order",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Read the order": {
      "main": [
        [
          {
            "node": "Check and build",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check and build": {
      "main": [
        [
          {
            "node": "Can make an invoice?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Can make an invoice?": {
      "main": [
        [
          {
            "node": "Create the invoice in freee",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Write the notice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create the invoice in freee": {
      "main": [
        [
          {
            "node": "Write the notice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write the notice": {
      "main": [
        [
          {
            "node": "Answer where asked",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Answer where asked": {
      "main": [
        [
          {
            "node": "Show the result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Tell the staff",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {}
}