クラウド会計、盛り上がっていますね!
競争激化でユーザーサイドとしては嬉しい限りです。
クラウド会計へ移行するにあたり、重要になってくるのが現行システムからの仕訳の移行処理。
たいてい、CSVファイルやEXCELファイルをインポートする形だと思います。
そこで今回、「MoneyForwardクラウド会計」へインポートするCSVファイルを作成するアプリをJavaで書いたのですが、要点だけを整理しておきたいと思います。
OpenCsv
CSVファイルと言っても、所詮はカンマ区切りのテキストファイル。 ライブラリーなど使わなくても、標準のファイル出力で実装できるのですが、楽してMF用仕訳モデルからCSVファイルを生成できる方法は無いかと探してみたら、ありました、ありました!
複数あったのですが、私がチョイスしたのは、OpenCsv。
Ver5も出ていたのですが、参考記事が多いVer4を選択。
dependencies { compile group: 'com.opencsv', name: 'opencsv', version: '4.6' }
アプリの流れとしては、次のようなフローになります。
3.の部分でのコード量を大幅に減らせそう。これは超便利や!!
と思っていたら、CSVファイル書き出しで問題が発生しました!
ヘッダのラベル名(項目名)をモデル側で@CsvBindByNameアノテーションで定義できるのですが、列の順序がラベル名順になってしまいました。
それならば、@CsvBindByPositionアノテーションで、列順序をフィールドごとに設定すれば良い!ということで試してみましたが、正しく機能せず。。。
どうやら、@CsvBindByNameと@CsvBindByPositionの同時利用はできないみたい・・・(ショック)。
マネーフォワードのクラウド会計のインポートでは、ヘッダ行は必須です。
ヘッダ行が無いとフィールドのマッピングがされないからです。
何としても、ヘッダを付けて、正しい列順にしたい!!
実際には列順が順不同でも、MF側ではヘッダのラベルを見てマッピングしてくれるので、@CsvBindByNameだけでも何の問題は無いのですが・・・(ただの私のこだわりです)
しかし、しばらくWeb検索していると、Strategyクラスでカスタマイズは可能という情報を発見!
ということで、下記のサンプルの書き方で、想定通りにCSVファイルを書き出すことに成功しました!!
オッケー、オッケー!!
サンプル
- Java8
MoneyForward仕訳伝票 Model
- インポート処理で不要な項目は、モデルからカットしてあります(ex:作成日時)。
package models; import com.opencsv.bean.CsvBindByName; import com.opencsv.bean.CsvBindByPosition; import com.opencsv.bean.CsvDate; import lombok.Data; import java.util.Date; /** * Money Forward 仕訳伝票 Model */ @Data public class MFJournalModel { /** * 取引No */ @CsvBindByName(column = "取引No") @CsvBindByPosition(position = 0) private int dealNum; /** * 取引日 */ @CsvBindByName(column = "取引日") @CsvBindByPosition(position = 1) @CsvDate("yyyy/MM/dd") private Date dealDate; /** * 借方勘定科目 */ @CsvBindByName(column = "借方勘定科目") @CsvBindByPosition(position = 2) private String debitsAccountName; /** * 借方補助科目 */ @CsvBindByName(column = "借方補助科目") @CsvBindByPosition(position = 3) private String debitsSubAccountName; /** * 借方税区分 */ @CsvBindByName(column = "借方税区分") @CsvBindByPosition(position = 4) private String debitsTaxDivName; /** * 借方部門 */ @CsvBindByName(column = "借方部門") @CsvBindByPosition(position = 5) private String debitsSectionCode; /** * 借方金額(円) */ @CsvBindByName(column = "借方金額(円)") @CsvBindByPosition(position = 6) private long debitsAmount; /** * 貸方勘定科目 */ @CsvBindByName(column = "貸方勘定科目") @CsvBindByPosition(position = 7) private String creditAccountName; /** * 貸方補助科目 */ @CsvBindByName(column = "貸方補助科目") @CsvBindByPosition(position = 8) private String creditSubAccountName; /** * 貸方税区分 */ @CsvBindByName(column = "貸方税区分") @CsvBindByPosition(position = 9) private String creditTaxDivName; /** * 貸方部門 */ @CsvBindByName(column = "貸方部門") @CsvBindByPosition(position = 10) private String creditSectionCode; /** * 貸方金額(円) */ @CsvBindByName(column = "貸方金額(円)") @CsvBindByPosition(position = 11) private long creditAmount; /** * 摘要 */ @CsvBindByName(column = "摘要") @CsvBindByPosition(position = 12) private String remarks; /** * タグ */ @CsvBindByName(column = "タグ") @CsvBindByPosition(position = 13) private String tag; /** * 仕訳メモ */ @CsvBindByName(column = "仕訳メモ") @CsvBindByPosition(position = 14) private String memo; }
Main
import converters.Converter; import daos.JournalDao; import models.JournalModel; import models.MFJournalModel; import strategies.CustomMappingStrategy; import java.util.List; import java.util.Objects; public final class Main { public static void main(String[] args) { // 1. DBより現行仕訳を抽出 List<JournalModel> jmList = JournalDao.getInstance().fetchJournalModels(); if (Objects.nonNull(jmList) && !jmList.isEmpty()) { // 2. MF仕訳用リスト格納 List<MFJournalModel> mfjmList = Converter.getInstance().convertMFJournals(jmList); // 3. CSVファイル作成 try { String filePath = "C:/temp/MFJournal.csv"; Writer writer = new OutputStreamWriter(new FileOutputStream(filePath), "MS932"); CustomMappingStrategy<MFJournalModel> strategy = new CustomMappingStrategy<>(); strategy.setType(MFJournalModel.class); StatefulBeanToCsv<MFJournalModel> beanToCsv = new StatefulBeanToCsvBuilder<MFJournalModel>(writer) .withMappingStrategy(strategy) //.withApplyQuotesToAll(false) //前後のダブルコーテーション無し .build(); beanToCsv.write(mfjmList); writer.close(); } catch (CsvDataTypeMismatchException | CsvRequiredFieldEmptyException | IOException ex) { ex.printStackTrace(ex); } } } }
CustomMappingStrategy Class
package strategies; import com.opencsv.bean.BeanField; import com.opencsv.bean.ColumnPositionMappingStrategy; import com.opencsv.bean.CsvBindByName; import com.opencsv.exceptions.CsvRequiredFieldEmptyException; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.reflect.FieldUtils; /** * CustomMappingStrategy Class * * @param <T> */ public class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> { @Override public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException { super.setColumnMapping(new String[FieldUtils.getAllFields(bean.getClass()).length]); final int numColumns = findMaxFieldIndex(); if (!isAnnotationDriven() || numColumns == -1) { return super.generateHeader(bean); } String[] header = new String[numColumns + 1]; BeanField<T> beanField; for (int i = 0; i <= numColumns; i++) { beanField = findField(i); String columnHeaderName = extractHeaderName(beanField); header[i] = columnHeaderName; } return header; } private String extractHeaderName(final BeanField<T> beanField) { if (beanField == null || beanField.getField() == null || beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class).length == 0) { return StringUtils.EMPTY; } final CsvBindByName bindByNameAnnotation = beanField.getField() .getDeclaredAnnotationsByType(CsvBindByName.class)[0]; return bindByNameAnnotation.column(); } }
まとめ
- OpenCsvは、とても便利!
- ヘッダ書き出しと列の出力順を制御したい場合には、Strategyクラスが必要。
- クラウド会計で、「2025年の崖」の乗り越えたい!