フリーランスにとって請求書を発行することは日常茶飯事ですが,みなさんはどのように作っているでしょうか。
私の場合は表計算ソフトで値段を管理しておいて,最終的に
クラウド請求書・見積書・納品書管理サービス Misoca(みそか)
で入力して仕上げています。
というのもMisocaは本当にシンプルで,請求書作成のルールに疎くてもすぐ実用レベルのものが作成できるからです。
さて具体的な方法ですが,まず表計算ソフトでは下の表のようにファイルパスと値段をセットにしておきます。
@filepath | price |
---|---|
~/Desktop/myFolder/fig1.ai | 8000 |
~/Desktop/myFolder/fig2.ai | 8000 |
~/Desktop/myFolder/fig3.ai | 12000 |
最終的に請求書にするときには,次のように価格ごとに個数を集計して1行ずつ書き込みます。
品番・品名 | 数量 | 単位 | 単価 |
---|---|---|---|
図版 種類1 | 2 | 点 | 8000 |
図版 種類2 | 1 | 点 | 12000 |
表計算ソフトのピボットテーブルを利用して価格ごとの個数を出し,それを見ながらMisocaに手入力をするという流れです。
ただこの方法はピボットテーブルを使うのが大げさで,手入力も無駄に感じ,だんだん面倒くさくなってきました。もう少し手軽なものがほしいです。
幸いなことにMisocaにはCSVから請求書を一括生成する機能があります。そのため値段ごとの個数をスクリプトで集計し,CSVにすればあらかたの作業は終わりそうです。
そこで今回は選択している価格リストをもとに値段ごとの個数を計算し,Misocaインポート用CSVとして保存するAutomatorサービスを紹介します。
こちらのファイルをダウンロードしてください。JXA(JavaScript for Automation)で記述しているので,macOS 10.10から対応します。
価格リストをMisoca用CSVに書き出し.workflow
Finderでダブルクリックするなどしてworkflowファイルを開くと,インストールするかどうか訊かれます。インストールすれば使えるようになります。
使いかた
Excel・Numbers・Libre Office Calc・Googleスプレッドシート・テキストエディタなど何を使ってもいいので,価格の部分を選択してください(縦方向に並んでいる必要があります)。
次にメニューバーの [アプリケーション名]:サービス:一般:価格リストをMisoca用CSVに書き出し を選びます。画像のような状態です。
するとすぐに集計され,保存ダイアログが開きます。お好きなフォルダとファイル名を指定してください。
実際にCSVができていたら成功です。このデータでは請求先は「名称未設定」,件名は当日の日付(例「請求書-2018-06-23」),品名は「種別1」など適当なものが入っています。なので直接CSVファイルを編集するか,Misocaにインポートしてから追加入力するなどして完成させてください。
インポートは,請求書:一括作成/郵送 の「CSVファイルのアップロード」でファイルを選び「アップロードする」ボタンを押すと完了します。
実際にできる請求書はこんな感じ。必要十分です!
これでまた少し仕事が速くなりました。今日もさっさと仕事を切り上げて好きなことをしましょう!
コードはこちら
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
/** * @file 改行区切りの値段リストを同じ価格ごとに集計し,misocaインポート用csvとして保存する * @version 1.0.0 * @author sttk3.com * @copyright (c) 2018 sttk3.com */ function run() { return main() ; } function main() { let res ; const stdApp = Application.currentApplication() ; stdApp.includeStandardAdditions = true ; const seApp = Application('System Events') ; // 選択部分をコピーしてテキストを取り出す seApp.keystroke('c', {using : ['command down']}) ; let str = stdApp.theClipboard({as : 'text'}) ; if(str == null || str == '') {return ;} // 文字列先頭と末尾の空白文字(半角全角スペース,改行など),数値区切りのカンマを取り除く str = str.trim().replace(/,/g, '') ; // 同じ値段ごとに集計する const lines = str.split(/[\r\n]+/) ; let sum = {} ; let keyStr ; lines.forEach((v) => { keyStr = v.toString() ; if(sum[keyStr] == null) { sum[keyStr] = 1 ; } else { sum[keyStr] += 1 ; } }) ; // csvのhead, bodyを生成 const headStr = '"請求日","請求番号","件名","備考","取引先管理コード","請求先名称","敬称","担当者","送付先郵便番号","送付先住所1","送付先住所2","送付先名前1","送付先名前2","送付先名前3","送付先名前4","送付先敬称","消費税設定","お支払い期限"' ; let head = headStr.split(/,/g) ; let headIndex = {} ; head.forEach((v, i) => { headIndex[v] = i ; }) ; let body = new Array(head.length) ; // 日付情報 const today = new Date() ; const dateFmt = '%Y-%m-%d' ; const creationDate = dateStr(dateFmt, today) ; const issueDate = dateStr(dateFmt, lastDay(today, 0)) ; // 今月末日扱い const paymentDueOn = dateStr(dateFmt, lastDay(today, 1)) ; // 翌月末日期限 body[headIndex['"請求日"']] = issueDate ; body[headIndex['"お支払い期限"']] = paymentDueOn ; // 仮件名 body[headIndex['"件名"']] = '請求書-' + creationDate ; // 請求先情報 body[headIndex['"請求先名称"']] = '名称未設定' ; body[headIndex['"敬称"']] = '様' ; // 値段ごとに行作成 const itemNamePrefix = '種別' ; const priceList = Object.keys(sum).sort((a, b) => {return Number(a) - Number(b)}) ; const rowLength = priceList.length ; const maxLength = 40 ; const btns = ['キャンセル'] ; if(rowLength > maxLength) { stdApp.displayDialog(`品目が ${rowLength} 個ありますが,最大で ${maxLength} 個までです。終了します。`, {buttons : btns, defaultButton : btns[0]}) ; return ; } const colNames = ['品目', '数量', '単位', '単価', '非課税フラグ'] ; let colName, index ; priceList.forEach((aPrice, rowIndex) => { if(isNaN(Number(aPrice))) { stdApp.displayDialog('価格に数値以外のものが紛れています。終了します。', {buttons : btns, defaultButton : btns[0]}) ; return ; } index = rowIndex + 1 ; colNames.forEach((aName) => { colName = `"${aName}${index}"` ; head.push(colName) ; }) ; body.push(`"${itemNamePrefix + index}"`) ; // 品目 body.push(sum[aPrice]) ; // 数量 body.push('"点"') ; // 単位 body.push(aPrice) ; // 単価 body.push('') ; // 非課税フラグ }) ; // ファイルに書き出す const newStr = [head.join(','), body.join(',')].join('\n') ; const destPath = stdApp.chooseFileName({withPrompt : 'csv保存先', defaultName : (creationDate + '.csv')}) ; if(!destPath) {return ;} res = writeText(destPath, newStr) ; const myName = stdApp.properties()['name'] ; let msg = '書き出しが完了しました。' ; if(!res) { msg = '書き出しに失敗しました。' ; } stdApp.displayNotification(msg, {withTitle : myName}) ; return res ; } /** * dateコマンド風フォーマット指定でDateを文字列化する * @param {text} fmt フォーマット文字列。例えば'%Y-%m%d'で'2018-0609' * @param {date} [date] 日付データ。省略時は現在の時刻を使う * @return {text} */ function dateStr(fmt, date = new Date()) { let res = fmt ; const fmtPattern = /%([a-z%])/gi ; let year, month, day, hours, minutes, seconds, milliSeconds ; year = date.getFullYear() ; month = date.getMonth() + 1 ; day = date.getDate() ; hours = date.getHours() ; minutes = date.getMinutes() ; seconds = date.getSeconds() ; milliSeconds = date.getMilliseconds() ; const zeroPadding = (src, digit) => { const zero = Array(digit).join('0') ; return (zero + src).slice(-digit) ; } ; const replaceTable = { 'Y' : year.toString(), 'y' : zeroPadding(year, 2), 'm' : zeroPadding(month, 2), 'd' : zeroPadding(day, 2), 'e' : day.toString(), 'H' : zeroPadding(hours, 2), 'k' : hours.toString(), 'M' : zeroPadding(minutes, 2), 'S' : zeroPadding(seconds, 2), 'N' : zeroPadding(milliSeconds, 3), // 本来nanoseconds用のオプションだけどないのでmillisecondsを返す '%' : '%' } ; res = fmt.replace(fmtPattern, (m0, m1) => { return replaceTable[m1] || m0 ; }) ; return res ; } /** * 月の最終日を返す * @param {date} [date] 対象の日付。省略時は現在の時刻を使う * @param {integer} [monthOffset] 今月を0としてnヶ月前・nヶ月後 * @return {date} */ function lastDay(date = new Date(), monthOffset = 0) { return new Date(date.getFullYear(), date.getMonth() + 1 + monthOffset, 0) ; } /** * テキストをファイルに書き込む * @param {text} destPath 保存先pathの文字列 * @param {text} str 書き込むtext * @param {encoding} [encoding] 文字エンコーディング。$.NSShiftJISStringEncodingでshift-jis,$.NSUnicodeStringEncodingでutf16。省略時はutf8 * @return {path} */ function writeText(destPath, str, encoding = $.NSUTF8StringEncoding) { let res ; const path = $(destPath.toString()).stringByStandardizingPath ; const contents = $(str).dataUsingEncoding(encoding) ; const attr = $() ; res = $.NSFileManager.defaultManager.createFileAtPathContentsAttributes(path, contents, attr) ; if(res) { return destPath ; } else { return res ; } } |
このサイトで配布しているスクリプトやその他のファイルを,無断で転載・配布・販売することを禁じます。
それらの使用により生じたあらゆる損害について,私どもは責任を負いません。
スクリプトやファイルのダウンロードを行った時点で,上記の規定に同意したとみなします。