ゲーム化!tomo_manaのブログ

ゲーム化!tomo-manaのブログ

Unityでゲームを作る方法について紹介しています

Unity×会計#9 価格変動を考慮した仕入、売上、在庫 (Unity 2019.4.4f1)

f:id:tomo_mana:20211220011108p:plain

仕入価格や売上価格が変化していくことを考慮したデータの持たせ方。


前回までの実装では、処理を単純にするため、仕入と売上の価格は一定、また仕入量と売上量も1個ずつにしていました。

データ構造の見直し

仕入価格や売上価格が、仕入や売上のタイミングで価格が変わる場合、仕入・売上毎のレコード(履歴)が必要になります。さらに商品在庫量を記録する別のレコードも必要になります。

f:id:tomo_mana:20210912135843p:plain
取引履歴を残す構造

実際の帳簿との比較

実際の帳簿も、取引単位で記帳するようになっています。

たとえば、仕入帳は、商品ごとの仕入個数と取引額を1行(=1レコード)にまとめます。

仕入帳)

日付 摘要 内訳 金額
4/1 A商店 現金 x x
a商品 10個 @30 x 300


また、商品有高帳は、増減(入庫/出庫)と残高(残個数)を1行(1レコード)にまとめます。

(商品有高帳)
a商品

日付 摘要 入庫 出庫 残高
4/1 前月繰越 5個(@30=150) x 5個(@30=150)
4/1 仕入 10個(@30=300) x 15個(@30=450)


データを段階的に作ることで、簿記の歴史をなぞっているようなワクワク感があるのが不思議です。補助簿のフォーマットが、今の形になるべくしてなったのですね、、、感慨深いです。

データアクセスの確認

データ構造が変更になることで、各データへのアクセス方法も変更になります。


前回までのデータアクセス

f:id:tomo_mana:20210924111643p:plain
前回までのデータ構造(データフロー図)


価格変化を考慮したデータアクセス(決算処理は次回)

f:id:tomo_mana:20210924113145p:plain
データ構造の変更(データフロー図)


データ名は、実際の簿記で使われる帳票(補助簿)に近い名前にしました。実際には、各帳簿の持つデータのごく一部だけを持っています。

現金出納帳および商品有高帳は、仕入・売上どちらからも更新されて、現金または商品個数の増減と残高を管理します。仕入および売上帳は、仕入数または売上数と仕入金額または売上金額を記帳していきます。単価は合計額から自動算出されるようにしています。

構造の記述(クラス図)

現金出納帳および商品有高帳は現金または商品数の増減を入力とし、残高を出力としたヒストリ(履歴)です。入力が1点のため、ここでは仮にSinglePoint(x)というクラス名にします。

class SinglePoint {
    private List<Tuple<int, int>> list;
    private int sum;
    // 登録
    public void Add(増減数)
    {
        sum += 増減数;
        list.Add( new Tuple<int, int>(増減数, sum) );
    }
    // 残数確認に使用
    public int Get()
    {
        return sum;
    }
};

一方、仕入および売上帳は個数と取引金額を入力とし、これまでの総個数、総取引金額を出力としたFIFOです。入力が2点のため、ここでは仮にDoublePoint(x, y)というクラス名します。

class DoublePoint {
    private List<Tuple<int, int>> list;
    private int sum0;
    private int sum1;
    
    public void Add(個数、金額)
    {
        sum0 += 個数;
        sum1 += 金額;
        list.Add( new Tuple<int, int>(個数, 金額) );
    }
    // 総個数、総額を取得
    public int Get(index)
    {
        if( index == 0 ){
            return sum0;
        } else if( index == 1 ){
            return sum1;
        } else {
            return 0;
        }
    }
}

クラス名は、もう少し良い名前を思いついたら変更しようと思います。最終的には、両者が総勘定元帳と同じフォーマットになってしまうかもしれませんが、今はこんな感じにしておきます。

処理は以下のようになります。

(修正前)

f:id:tomo_mana:20210924111643p:plain
前回までのデータ構造(データフロー図)
// 試算表
TrialBalance tb = new TrialBalance();
// 棚卸
InventoryDB iv = new InventoryDB();

public int ExecutePurchase()
{
    if( tb.cash >= unitPrice ){
        tb.cash -= unitPrice;
        tb.purchase += unitPrice;
        tb.q_remain++;
        tb.q_total++;
        iv.q_purchase++;
    }
    // 戻り値部分は省略
}

(修正後)

f:id:tomo_mana:20210924113145p:plain
データ構造の変更(データフロー図)
// 現金出納帳
SinglePoint cash = new SinglePoint();
// 商品有高帳
SinglePoint stock = new SinglePoint();
// 仕入帳
DoublePoint purchase = new DoublePoint();
// 売上帳
DoublePoint sales = new DoublePoint();

public int ExecutePurchase(int amount, int price)
{

    if( tb.cash >= price ){
        cash.Add( -price );  // 現金出納帳
        ledger.Add( amount, price );  // 仕入・売上帳
        stock.Add( amount );  // 商品有高帳
    }
    // 戻り値部分は省略
}

入力フォームの作成

仕入、売上の個数と取引額を入力できるフォームを追加します。

f:id:tomo_mana:20210922080259p:plain
個数と取引額を入力できるフォームの追加

仕入額、売上額は単価でなく総額です。これは、単価が割り切れないケースを考慮しています。
tomo-mana.hatenablog.com

フォームのHierarchyは以下のようにしました。

f:id:tomo_mana:20210923203152p:plain
Hierarchy - 入力フィールド

入力フィールドの内容が分かるように、ラベル(Label)を追加しています。また前回までと同様に、画面に取引の内容(JournalLog)を表示します。
取引の内容表示とボタンが押された時の処理も同じファイルにしていましたが、コード量が多くなったのでファイル分割します(⇒JournalLog.cs)。

尚、今の時点ではDropdownは使用しませんが、次々回あたりに使用するため、あらかじめ追加しておきます。


上記の仕入処理(PurchaseInput)、売上処理(SalesInput)には、それぞれ以下のスクリプトコンポーネントとして追加します。これまでデータ構造が単純だったために全部のボタンを一挙まとめて処理していましたが、コード量が多くなって来たので、フォーム毎に処理を分割します。

コード

(データ処理部:SalesTransaction2.cs)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

// 台帳(増減あり)
public class SinglePoint {
    List<Tuple<int, int>> l;
    int rsum;
    public void Add( int a )
    {
        rsum += a;
        l.Add( new Tuple<int, int>(a, rsum) );
    }
    public SinglePoint()
    {
        l = new List<Tuple<int, int>>();
        rsum = 0;
    }
    public int Get()
    {
        return rsum;
    }
}

// 台帳(増加のみ)
public class DoublePoint {
    List<Tuple<int, int>> l;
    public void Add( int a, int b )
    {
        rsum1 += a;
        rsum2 += b;
        l.Add( new Tuple<int, int>(a, b) );
    }
    int rsum1;
    int rsum2;
    public DoublePoint()
    {
        rsum1 = rsum2 = 0;
        l = new List<Tuple<int, int>>();
    }
    public int Get(int id)
    {
        if( id == 0 ){
            return rsum1;
        } else if( id == 1 ){
            return rsum2;
        } else {
            return 0;
        }
    }
}

public class SalesTransaction2 : MonoBehaviour
{
    // 試算表
    TrialBalance tb = new TrialBalance();
    
    // 資本
    int firstCash = 100000;
    
    public TrialBalance GetTrialBalance()
    {
        tb.cash = cash.Get();
        tb.purchase = purchase.Get(1);
        tb.sales = sales.Get(1);
        tb.q_remain = stock.Get();
        tb.q_total = purchase.Get(0);
        if( purchase.Get(0) != 0 ){
            tb.unit = purchase.Get(1) / purchase.Get(0);
        } else {
            tb.unit = 0;
        }
        
        return tb;
    }
    
    public void InitializeJournal()
    {
        cash.Add(firstCash);
    }
    
    // 現金出納帳
    SinglePoint cash = new SinglePoint();
    // 商品有高帳
    SinglePoint stock = new SinglePoint();
    // 仕入帳
    DoublePoint purchase = new DoublePoint();
    // 売上帳
    DoublePoint sales = new DoublePoint();
    
    public int ExecutePurchase(int amount, int price)
    {
        int e = 0;
        
        if( cash.Get() >= price ){
            cash.Add( -price );
            purchase.Add( amount, price );
            stock.Add( amount );
        } else {
            e = 1;
        }
        return e;
    }
    
    public int ExecuteSale(int amount, int price)
    {
        int e = 0;
        if( inventory.Get() >= amount ){
            cash.Add( price );
            sales.Add( amount, price );
            stock.Add( -amount );
        } else {
            e = 2;
        }
        return e;
    }
    
    // Start is called before the first frame update
    void Start()
    {
        InitializeJournal();
    }
}

(入力フォーム:PurchaseInput.cs)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;

public class PurchaseInput : MonoBehaviour, ILangSwitchHandler
{
    enum BUTTON_NAME {
        BUTTON_PURCHASED = 0,
        BUTTON_SALED,
        BUTTON_SETTLE,
        BUTTON_RESET,
    };
    
    [SerializeField]
    BUTTON_NAME buttonName = BUTTON_NAME.BUTTON_PURCHASED;
    
    [SerializeField]
    JournalLog log = default;
    [SerializeField]
    SalesTransaction2 ts = default;
    
    LANG selected;
    
    void UpdateLog(int error)
    {
        log.UpdateLog( error );
    }
    
    void ExecutePurchase(int amount, int price)
    {
        int e = 0;

        if( !ReferenceEquals(ts, null) ){
            e = ts.ExecutePurchase( amount, price );
        }
        if( e != 0 ){
            e = 1;
        }
        UpdateLog( e );
    }
    
    void ExecuteSale(int amount, int price)
    {
        int e = 0;

        if( !ReferenceEquals(ts, null) ){
            e = ts.ExecuteSale( amount, price );
        }
        if( e != 0 ){
            e = 2;
        }
        UpdateLog( e );
    }
    
    public struct InputLine {
        public TMP_InputField inputAmount;      // 個数
        public TMP_InputField inputNetPrice;   // 単価
    }
    InputLine[] inputLine = new InputLine[1];
    
    void ButtonClicked(int buttonNo)
    {
        int a, p;
        
        if( int.TryParse( inputLine[0].inputAmount.text, out a ) ){
            if( int.TryParse( inputLine[0].inputNetPrice.text, out p ) ){
                if( buttonName == BUTTON_NAME.BUTTON_PURCHASED ){
                    ExecutePurchase( a, p );
                } else
                if( buttonName == BUTTON_NAME.BUTTON_SALED ){
                    ExecuteSale( a, p );
                }
            }
        }
    }
    
    public void LangSelected(LANG selectedLang)
    {
        this.selected = selectedLang;
        UpdateLog( -1 );
    }

    // Start is called before the first frame update
    void Start()
    {
        Button button;
        foreach( Transform child in gameObject.transform ){
            Transform gc = child;
            // InputLine に属するオブジェクトの参照を保持
            if( gc.GetComponent<TMP_InputField>() != null ){
                if( gc.gameObject.name == "InputAmount" ){
                    inputLine[0].inputAmount = gc.GetComponent<TMP_InputField>();
                    // デフォルト値をフィールド初期値に設定
                    inputLine[0].inputAmount.text = "5";
                } else
                if( gc.gameObject.name == "InputNetPrice" ){
                    inputLine[0].inputNetPrice = gc.GetComponent<TMP_InputField>();
                    // デフォルト値をフィールド初期値に設定
                    inputLine[0].inputNetPrice.text = "500";
                }
            } else
            if( gc.GetComponent<Button>() ){
                button = gc.GetComponent<Button>();
                int ii = (int)buttonName;
                button.onClick.AddListener(() => ButtonClicked(ii));
            }
        }
    }
}

(ログ出力:JournalLog.cs)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;

public class JournalLog : MonoBehaviour, ILangSwitchHandler
{
    [SerializeField]
    TextMeshProUGUI log = default;
    [SerializeField]
    SalesTransaction2 ts = default;
        
    string[] msg_j = new string[]{
        "成功!",
        "現金不足!",
        "在庫不足!",
        "START!"
    };
    string[] msg_e = new string[]{
        "SUCCEED!",
        "LACK OF CASH!",
        "LACK OF REMAINS!",
        "START!"
    };
    
    LANG selected;
    
    int error_hold;
    public void UpdateLog(int error)
    {
        TrialBalance tb = default;
        
        if( !ReferenceEquals(ts, null) ){
            tb = ts.GetTrialBalance();
        }
        if( !ReferenceEquals(tb, null) ){
            if( error == -1 ){
                error = error_hold;
            } else {
                error_hold = error;
            }
            if( !ReferenceEquals(log, null) ){
                //現金:xxxx/仕入:xxxx/売上:xxxx/在庫:xx/xx(@100)
                if( selected == LANG.LANG_ENG ){
                    log.text = msg_e[error] + "\nCash:" + tb.cash + "\nPurchases:" + tb.purchase + "\nSales:" + tb.sales + "\nRemain:" + tb.q_remain + "/" + tb.q_total + "(@" + tb.unit + ")";
                } else {
                    log.text = msg_j[error] + "\n現金:" + tb.cash + "\n仕入:" + tb.purchase + "\n売上:" + tb.sales + "\n在庫:" + tb.q_remain + "/" + tb.q_total + "(@" + tb.unit + ")";
                }
            }
        }
    }
    
    public void LangSelected(LANG selectedLang)
    {
        this.selected = selectedLang;
        UpdateLog( -1 );
    }
}

動作テスト

ゲームのURL(前回までと同じURLです)
tomo-mana.hatenablog.com

(以上)