システムの目的
貿易業、卸売業など、売上と仕入が同時に計上されるビジネスや、前渡金(債権)と未払金(債務)が複数同時に計上されるビジネスでの具体的な使用例です。
kintoneアプリストアに公開している「ツバイソERP取引先マスタリアルタイム連携アプリ」によりツバイソの取引先マスタを活用しています。
システム化の方針
今回は、ワークフローは省略し、商品の検収をトリガーに、売上、仕入の同時計上をリアルタイムに行うところに絞った例です。
kintoneアプリストアに公開している「ツバイソERP取引先マスタリアルタイム連携アプリ」を利用して、ツバイソの取引先マスタをkintoneのアプリでルックアップしているところも、高速開発のポイントです。
ツバイソで売上、仕入計上後の会計処理、入金、支払い(FBデータ作成)、自動消し込み、勘定科目別・取引先別の債権債務残高管理などの複雑な処理は、取引先マスタや原因マスタの設定に基づき自動化されます。また、kintoneにおける組織のコードをツバイソの部門マスタコードに一致させることで、部門別の管理会計も自動的に行っています。
シナリオ
商品は、仕入先から売り先に直送されます。仕入先からの出荷通知をもとに検収し、売上と仕入を同時に計上しています。
出来上がりのスクリーンショットとともにご確認ください。
●受注〜発注(ワークフロー省略)
①受注部門が申請書を作成し、契約番号で管理します。
②受注先と発注先は「ツバイソERP取引先マスタリアルタイム連携アプリ」からルックアップで入力します。
③原価率で仕入単価から販売単価を計算します。取引先マスタを加工し、原価率の項目を追加して、ルックアップで自動入力も可能です。
↓ ↓ ↓
●検収
仕入先の出荷通知をもとに検収します。
↓ ↓ ↓
●会計処理
検収日にて、申請部門の売上、仕入としてツバイソで自動計上されます。
《仕入・経費明細》
{仕訳}
《売上明細》
{仕訳}
●入金、支払い(FBデータ作成)、自動消し込み、勘定科目別・取引先別の債権債務残高管理はツバイソで自動化できます。
《入金確認》
{銀行明細}
〈仕訳〉
{入金消込}
↓ ↓ ↓
{残高確認}
〈取引先残高〉
《出金手続き》
{銀行明細}
〈仕訳〉
{出金消込}
〈仕訳〉
{残高確認}
〈取引先残高〉
kintoneアプリの作成
このページに添付している「売上・仕入2伝票同時登録サンプルセット.zip」をダウンロードして、ご自身のkintone環境に読み込んでください。読み込んだあと、アプリのテンプレートから「売上・仕入2伝票同時登録サンプル」と「ツバイソERP取引先マスターリアルタイム連携」を作成してください。
この後の Javascript 説明を参考に販売管理、購買管理の権限があるユーザーのツバイソAPI Tokenの設定や希望のアレンジをしてください。
ツバイソ連携用javascript
ツバイソへの連携は、プロセス管理の「検収する」というアクションをトリガーに行っています。
javascriptコードのポイントを説明します。
- エラー発生時の整合性を担保する。
複数の処理をシーケンスで実行する際、シーケンス中のエラー発生時の回復処理を考慮する必要があります。
今回は、1回の検収シーケンスで売上明細登録と仕入経費明細登録の2つのPOST処理があります。ですので、下記のエラー発生が考えられます。
- 売上明細の登録に失敗
- 仕入経費明細の登録に失敗
この内、1のケースはシーケンスをエラー処理に流して失敗要因を報告すればよいですが、2のケースではすでに売上明細の登録が成功しています。ですので、整合性を保つために登録した売上明細を削除した上で仕入経費明細登録の失敗要因を報告する必要があります。
// ---------------------------------------------------
// メイン処理
// kintoneのイベントハンドラを登録する
// ---------------------------------------------------
(function () {
"use strict";
kintone.events.on(['app.record.detail.process.proceed'], function(event) {
if (event.action['value'] == "検収する") {
var arPostRecord = convertToArJson(event.record);
var apPostRecord = convertToApJson(event.record);
var arPostId;
return getOrganizationCode(event.record['申請者']['value']['code'])
.then(function(deptCode) {
// 部門コードをkintoneの組織コードで上書き
arPostRecord['dept_code'] = apPostRecord['dept_code'] = deptCode;
return postTsubaisoRecord(API_AR_POST_URL, arPostRecord)
}).then(function(resp) {
arPostId = resp['id'];
return postTsubaisoRecord(API_AP_POST_URL, apPostRecord);
}).then(function(resp) {
return event;
}).catch(function(error) {
if (arPostId) {
// 売上明細のPOSTに成功していた時は、整合性を保つために明細を削除する
return deleteTsubaisoRecord(API_AR_DESTROY_URL, arPostId)
.then(function() {
event.error = error;
return event;
});
}
event.error = error;
return event;
});
}
});
})();
コード上では、売上明細(ar)のPOST結果からIDをarPostIdという変数に保存しておき、共通のエラー処理に流れた時にarPostIdに有効な値が入っていたら売上明細のDELETEを実行するようになっています。
売上明細のPOSTに成功していたらarPostIdには登録したIDが入るためDELETEが実行され、POSTに失敗していたらエラー報告のみが行われるエラー処理が実現できています。
- kintoneのrecordからツバイソAPIのJson形式に変換
ツバイソAPI向けJson形式に変換する方法は難しくありません。空のObject(hashのようなもの)を作っておいて、ツバイソAPIのパラメータをkeyに、それぞれのvalueはkintoneのrecordから対応させたいものを取得して代入します。
下記のように、1つのAPIに対して1つの変換関数を作成しておくと管理しやすいでしょう。
// ---------------------------------------------------
// ツバイソ形式のJsonに変換(売上明細)
// kitone形式のレコードをツバイソAPI形式に変換する
// ---------------------------------------------------
function convertToArJson(kintoneRecord)
{
var arJson = {};
arJson['price_including_tax'] = kintoneRecord['selling_amount_total_included_tax']['value'];
arJson['realization_timestamp'] = kintoneRecord['acceptance_date']['value'];
arJson['customer_master_code'] = kintoneRecord['customer']['value'];
arJson['reason_master_code'] = 'SALES';
arJson['dc'] = 'd'; // 増加固定
arJson['memo'] = //契約番号/担当者/商品名/販売単価/個数/備考
kintoneRecord['contract_num']['value'] + '/'
+ kintoneRecord['申請者']['value']['name'] + '/'
+ kintoneRecord['item']['value'] + '/'
+ '@' + kintoneRecord['selling_unit_price']['value'] + '円/'
+ kintoneRecord['quantity']['value'] + '個/'
+ kintoneRecord['remarks']['value'];
arJson['tax_code'] = 1007; // 一般売上(8%)
arJson['dept_code'] = 'COMMON';
return arJson;
}
// ---------------------------------------------------
// ツバイソ形式のJsonに変換(仕入経費明細)
// kitone形式のレコードをツバイソAPI形式に変換する
// memo required String メモ。値は空文字でも構いませんが必須項目です。 右になるように 契約番号/担当者/商品名/仕入単価/個数/備考
// ---------------------------------------------------
function convertToApJson(kintoneRecord)
{
var apJson = {};
apJson['price_including_tax'] = kintoneRecord['purchase_amount_total_included_tax']['value'];
apJson['accrual_timestamp'] = kintoneRecord['acceptance_date']['value'];
apJson['customer_master_code'] = kintoneRecord['order_destination']['value'];
apJson['reason_master_code'] = 'BUYING_IN';
apJson['dc'] = 'c'; // 増加固定
apJson['memo'] = //契約番号/担当者/商品名/仕入単価/個数/備考
kintoneRecord['contract_num']['value'] + '/'
+ kintoneRecord['申請者']['value']['name'] + '/'
+ kintoneRecord['item']['value'] + '/'
+ '@' + kintoneRecord['purchase_unit_price']['value'] + '円/'
+ kintoneRecord['quantity']['value'] + '個/'
+ kintoneRecord['remarks']['value'];
apJson['tax_code'] = 1001; // 課税売上分一般仕訳(8%)
apJson['port_type'] = 1; // 国内固定
apJson['dept_code'] = 'COMMON';
return apJson;
}
- ツバイソAPIのリクエスト〜レスポンス解析
変換したJson形式のレコードをAPIでPOSTします。
この時、ツバイソAPIの リクエストからレスポンス解析までは共通化できるコードが多いです。これらを共通化しておくと別のアプリをカスタマイズする時に使いまわせるので、開発効率があがります。
// ---------------------------------------------------
// ツバイソPOST API
// ---------------------------------------------------
function postTsubaisoRecord(url, record) {
var headers = {'Access-Token':ACCESS_TOKEN, 'Accept':'application/json', 'Content-Type':'application/json'};
return kintone.proxy(url, 'POST', headers, record)
.then(checkResponse);
};
// ---------------------------------------------------
// 共通Responseチェック処理
// ---------------------------------------------------
function checkResponse(resp) {
var body = resp[0];
var status = resp[1];
if (200 <= status && status <= 299) {
// HTTPステータスコードが2xx(Success)
console.log('status Success');
var params = JSON.parse( body.replace(/^\s+|\s+$/g,'') );
return kintone.Promise.resolve(params);
}
else {
console.log('status error');
return kintone.Promise.reject(tsubaisoErrorHandle(status, body));
}
};
// ---------------------------------------------------
// ツバイソエラーメッセージのパターンを吸収する
// ---------------------------------------------------
function tsubaisoError(errorMessage)
{
var error;
if (errorMessage.error) {
error = errorMessage.error;
}
else if(errorMessage.errors) {
console.log(errorMessage);
error = errorMessage.errors;
}
else {
console.log(errorMessage);
error = errorMessage;
}
return error;
}
// ---------------------------------------------------
// ツバイソエラーハンドリング
// ---------------------------------------------------
function tsubaisoErrorHandle(status, body)
{
var errorMessage;
switch (status) {
case 401 : errorMessage = "アクセストークンが正しくありません。" ; break;
case 403 : errorMessage = "権限がありません。"; break;
case 404 : errorMessage = "リソースが見つかりません。"; break;
case 422 : errorMessage = tsubaisoError(JSON.parse(body.replace(/^\s+|\s+$/g,''))); break;
case 500 : errorMessage = "サーバーで何らかのエラーが発生しました。"; break;
case 503 : errorMessage = "しばらく時間をおいてからリトライしてください"; break;
default : errorMessage = "未知のエラーが発生しました。"; console.log(status); console.log(body); break;
}
return errorMessage;
}
上記のようにしておけば、上流のシーケンスはpostTsubaisoRecordをコールしておけばよく、成功、失敗に応じて結果を簡易に受け取ることができます。
最後に、ソースコード全文を載せておきます。
ソースコード全文
var ACCESS_TOKEN = "*************************************";
var API_AR_POST_URL = "https://tsubaiso.net/ar/create";
var API_AP_POST_URL = "https://tsubaiso.net/ap_payments/create";
var API_AR_DESTROY_URL = "https://tsubaiso.net/ar/destroy";
// ---------------------------------------------------
// メイン処理
// kintoneのイベントハンドラを登録する
// ---------------------------------------------------
(function () {
"use strict";
kintone.events.on(['app.record.detail.process.proceed'], function(event) {
if (event.action['value'] == "検収する") {
var arPostRecord = convertToArJson(event.record);
var apPostRecord = convertToApJson(event.record);
var arPostId;
return getOrganizationCode(event.record['申請者']['value']['code'])
.then(function(deptCode) {
// 部門コードをkintoneの組織コードで上書き
arPostRecord['dept_code'] = apPostRecord['dept_code'] = deptCode;
return postTsubaisoRecord(API_AR_POST_URL, arPostRecord)
}).then(function(resp) {
arPostId = resp['id'];
return postTsubaisoRecord(API_AP_POST_URL, apPostRecord);
}).then(function(resp) {
return event;
}).catch(function(error) {
if (arPostId) {
// 売上明細のPOSTに成功していた時は、整合性を保つために明細を削除する
return deleteTsubaisoRecord(API_AR_DESTROY_URL, arPostId)
.then(function() {
event.error = error;
return event;
});
}
event.error = error;
return event;
});
}
});
})();
// ---------------------------------------------------
// 組織コード取得
// UserCodeからユーザー情報を取得し
// 取得結果の「優先する組織ID」から対象の組織を取得する
// 組織がない場合は'COMMON'を返す
// ---------------------------------------------------
function getOrganizationCode(userCode)
{
return kintone.api('/v1/users', 'GET', {'codes' : [userCode]} )
.then(function(resp) {
if ( (resp['users'].length != 0)
&& (resp['users'][0]['primaryOrganization'] != null) ) {
var primaryOrganizationId = resp['users'][0]['primaryOrganization'];
return kintone.api('/v1/organizations', 'GET', {'ids' : [primaryOrganizationId]} );
}
return kintone.Promise.reject();
}).then(function(resp) {
if (resp['organizations'].length != 0) {
return kintone.Promise.resolve(resp['organizations'][0]['code']);
}
return kintone.Promise.reject();
}).catch(function(error) {
if (error) {
return kintone.Promise.reject(error);
}
return kintone.Promise.resolve('COMMON');
});
}
// ---------------------------------------------------
// ツバイソ形式のJsonに変換(売上明細)
// kitone形式のレコードをツバイソAPI形式に変換する
// ---------------------------------------------------
function convertToArJson(kintoneRecord)
{
var arJson = {};
arJson['price_including_tax'] = kintoneRecord['selling_amount_total_included_tax']['value'];
arJson['realization_timestamp'] = kintoneRecord['acceptance_date']['value'];
arJson['customer_master_code'] = kintoneRecord['customer']['value'];
arJson['reason_master_code'] = 'SALES';
arJson['dc'] = 'd'; // 増加固定
arJson['memo'] = //契約番号/担当者/商品名/販売単価/個数/備考
kintoneRecord['contract_num']['value'] + '/'
+ kintoneRecord['申請者']['value']['name'] + '/'
+ kintoneRecord['item']['value'] + '/'
+ '@' + kintoneRecord['selling_unit_price']['value'] + '円/'
+ kintoneRecord['quantity']['value'] + '個/'
+ kintoneRecord['remarks']['value'];
arJson['tax_code'] = 1007; // 一般売上(8%)
arJson['dept_code'] = 'COMMON';
return arJson;
}
// ---------------------------------------------------
// ツバイソ形式のJsonに変換(仕入経費明細)
// kitone形式のレコードをツバイソAPI形式に変換する
// memo required String メモ。値は空文字でも構いませんが必須項目です。 右になるように 契約番号/担当者/商品名/仕入単価/個数/備考
// ---------------------------------------------------
function convertToApJson(kintoneRecord)
{
var apJson = {};
apJson['price_including_tax'] = kintoneRecord['purchase_amount_total_included_tax']['value'];
apJson['accrual_timestamp'] = kintoneRecord['acceptance_date']['value'];
apJson['customer_master_code'] = kintoneRecord['order_destination']['value'];
apJson['reason_master_code'] = 'BUYING_IN';
apJson['dc'] = 'c'; // 増加固定
apJson['memo'] = //契約番号/担当者/商品名/仕入単価/個数/備考
kintoneRecord['contract_num']['value'] + '/'
+ kintoneRecord['申請者']['value']['name'] + '/'
+ kintoneRecord['item']['value'] + '/'
+ '@' + kintoneRecord['purchase_unit_price']['value'] + '円/'
+ kintoneRecord['quantity']['value'] + '個/'
+ kintoneRecord['remarks']['value'];
apJson['tax_code'] = 1001; // 課税売上分一般仕訳(8%)
apJson['port_type'] = 1; // 国内固定
apJson['dept_code'] = 'COMMON';
return apJson;
}
// ---------------------------------------------------
// ツバイソPOST API
// ---------------------------------------------------
function postTsubaisoRecord(url, record) {
var headers = {'Access-Token':ACCESS_TOKEN, 'Accept':'application/json', 'Content-Type':'application/json'};
return kintone.proxy(url, 'POST', headers, record)
.then(checkResponse);
};
// ---------------------------------------------------
// ツバイソDELETE API
// ---------------------------------------------------
function deleteTsubaisoRecord(url, id) {
var headers = {'Access-Token':ACCESS_TOKEN, 'Accept':'application/json', 'Content-Type':'application/json'};
return kintone.proxy(API_AR_DESTROY_URL + '/' + id, 'POST', headers, {})
.then(function(resp) {
var body = resp[0];
var status = resp[1];
if (200 <= status && status <= 299) {
// HTTPステータスコードが2xx(Success)
console.log('status Success');
return kintone.Promise.resolve();
}
else {
console.log('status error');
return kintone.Promise.reject(tsubaisoErrorHandle(status, body));
}
});
};
// ---------------------------------------------------
// 共通Responseチェック処理
// ---------------------------------------------------
function checkResponse(resp) {
var body = resp[0];
var status = resp[1];
if (200 <= status && status <= 299) {
// HTTPステータスコードが2xx(Success)
console.log('status Success');
var params = JSON.parse( body.replace(/^\s+|\s+$/g,'') );
return kintone.Promise.resolve(params);
}
else {
console.log('status error');
return kintone.Promise.reject(tsubaisoErrorHandle(status, body));
}
};
// ---------------------------------------------------
// ツバイソエラーメッセージのパターンを吸収する
// ---------------------------------------------------
function tsubaisoError(errorMessage)
{
var error;
if (errorMessage.error) {
error = errorMessage.error;
}
else if(errorMessage.errors) {
console.log(errorMessage);
error = errorMessage.errors;
}
else {
console.log(errorMessage);
error = errorMessage;
}
return error;
}
// ---------------------------------------------------
// ツバイソエラーハンドリング
// ---------------------------------------------------
function tsubaisoErrorHandle(status, body)
{
var errorMessage;
switch (status) {
case 401 : errorMessage = "アクセストークンが正しくありません。" ; break;
case 403 : errorMessage = "権限がありません。"; break;
case 404 : errorMessage = "リソースが見つかりません。"; break;
case 422 : errorMessage = tsubaisoError(JSON.parse(body.replace(/^\s+|\s+$/g,''))); break;
case 500 : errorMessage = "サーバーで何らかのエラーが発生しました。"; break;
case 503 : errorMessage = "しばらく時間をおいてからリトライしてください"; break;
default : errorMessage = "未知のエラーが発生しました。"; console.log(status); console.log(body); break;
}
return errorMessage;
}