タイガー!タイガー!じれったいぞー!(SE編)

AS400, Java, JavaEE, JSF等の開発、習慣など。日々の気づきをまとめたブログ(備忘録)

【Java, Opencsv】MoneyForwardクラウド会計 インポート用CSVファイル作成

クラウド会計、盛り上がっていますね!
競争激化でユーザーサイドとしては嬉しい限りです。

クラウド会計へ移行するにあたり、重要になってくるのが現行システムからの仕訳の移行処理。

たいてい、CSVファイルやEXCELファイルをインポートする形だと思います。
そこで今回、「MoneyForwardクラウド会計」へインポートするCSVファイルを作成するアプリをJavaで書いたのですが、要点だけを整理しておきたいと思います。

OpenCsv

CSVファイルと言っても、所詮はカンマ区切りのテキストファイル。 ライブラリーなど使わなくても、標準のファイル出力で実装できるのですが、楽してMF用仕訳モデルからCSVファイルを生成できる方法は無いかと探してみたら、ありました、ありました!

複数あったのですが、私がチョイスしたのは、OpenCsv。

opencsv.sourceforge.net

Ver5も出ていたのですが、参考記事が多いVer4を選択。

dependencies {
    compile group: 'com.opencsv', name: 'opencsv', version: '4.6'
}

アプリの流れとしては、次のようなフローになります。

  1. データ格納(DBやファイル等 → 現行システム仕訳モデル)
  2. データ格納(現行システム仕訳モデル → MF仕訳モデル)
  3. CSVファイル書き出し(MF仕訳モデル → CSVファイル)

3.の部分でのコード量を大幅に減らせそう。これは超便利や!!


と思っていたら、CSVファイル書き出しで問題が発生しました!

ヘッダのラベル名(項目名)をモデル側で@CsvBindByNameアノテーションで定義できるのですが、列の順序がラベル名順になってしまいました。

それならば、@CsvBindByPositionアノテーションで、列順序をフィールドごとに設定すれば良い!ということで試してみましたが、正しく機能せず。。。

どうやら、@CsvBindByNameと@CsvBindByPositionの同時利用はできないみたい・・・(ショック)。

マネーフォワードのクラウド会計のインポートでは、ヘッダ行は必須です。
ヘッダ行が無いとフィールドのマッピングがされないからです。

何としても、ヘッダを付けて、正しい列順にしたい!!
実際には列順が順不同でも、MF側ではヘッダのラベルを見てマッピングしてくれるので、@CsvBindByNameだけでも何の問題は無いのですが・・・(ただの私のこだわりです)

しかし、しばらくWeb検索していると、Strategyクラスでカスタマイズは可能という情報を発見!

stackoverflow.com

ということで、下記のサンプルの書き方で、想定通りにCSVファイルを書き出すことに成功しました!!

オッケー、オッケー!!

サンプル

  • Java8

MoneyForward仕訳伝票 Model

support.biz.moneyforward.com

  • インポート処理で不要な項目は、モデルからカットしてあります(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年の崖」の乗り越えたい!