MFCで在庫管理システム作成 6

前回までの記事。

MFCで在庫管理システム作成 4
前回までの記事。在庫編集画面作るモノ前回、編集選択画面を作成しました。今回は、選択状況に応じて在庫状況を編集します。必要なコントロールは、・ラベルスタティックテキスト×6・商品ID、商品名、単価、在庫数、在庫アラートのエディットボックス・カ...
MFCで在庫管理システム作成 5
前回までの記事。テーブル変更今回、履歴テーブルにデータを追加する処理を追加するのですが、ここで1点。履歴テーブルに列を増やします。履歴で、どれだけの数が増減して操作後は在庫数いくらになるか、という情報を表示したいためです。設計甘々なので、ご...

前回から少しシステムを触って、
商品数と履歴を増やしました。

何回入庫した、出庫したかは重要ではありませんので、
真似して作りたい方は適当に操作してみてください。

商品削除処理

編集画面についてはこれが最後です。

削除処理。

と言っても、SQLでDELETEを実行するわけではありません。

商品テーブルにdiscontinueという列があり、
現状nullなはずです。

削除したいときは、この列に1をセットします。
対象行を消すのではなく。

そうすることで、
履歴を参照する際にちゃんと「削除」という操作が行われていることを確認します。

画面側処理

EditMain.cppを開きます。

編集処理と同様、
商品IDをキルフォーカスすると商品テーブルを検索し、
データを取ってくるようにします。

とは言っても、取得処理はもうできているんですよね。

なので、在庫数のエディットボックス表示だけ少し変更します。

商品IDキルフォーカス関数はOnEnKillfocusEditEditmainProductId。
bGetProductInformationを呼んだ際にTRUEが返ってきた場合の処理を少しだけ変更。

/**
 * @fn
 * @brief	商品IDエディットボックス キルフォーカス用
 * @param	なし
 * @return	なし
 */
void CEditMain::OnEnKillfocusEditEditmainProductId()
{
	// 略
	
	if (Manager.bGetProductInformation(m_strProductID, strProductNameTemp, strCategory, strUnitPriceTemp, strCurrentCount, strProductAlertTemp) == TRUE)
	{
		// 商品が検索出来た
		m_strProductName = strProductNameTemp;
		m_strUnitPrice = strUnitPriceTemp;
		m_strProductCount = _T("");	// ここのエディットボックスは初期化しておく
		m_strProductAlert = strProductAlertTemp;
		m_strCurrentStock = strCurrentCount;

		// カテゴリーセット
		m_ctrlComboCategory.SelectString(-1, strCategory);	//文字列に合う項目を探し、セット

		// ここから編集用と削除用で分岐
		if (m_enMode == enEdit)
		{
			// 編集処理の場合
			// 推移テキストセット
			CString strText = _T("");
			strText.Format(_T("現在%s⇒変更後XXXXX"), strCurrentCount);
			m_ctrlStaticTransition.SetWindowText(strText);
		}
		else if (m_enMode == enDelete)
		{
			// 削除処理の場合
			m_strProductCount = strCurrentCount;
		}
	}
	else
	{
		// 何かしらエラー
		// 特に何もしない
	}

	//コントロール更新
	UpdateData(FALSE);

}

26~30行目を素で書いていたのを、
編集用分岐に入れ、
削除の場合は現在の在庫数をエディットボックスに入れるようにします。

続きまして、OKボタンが押されたときに呼ばれる入力チェック。
メッセージだけ親切に出しておきましょう。

/**
 * @fn
 * @brief	入力チェック
 * @param	なし
 * @return	なし
 */
BOOL CEditMain::bInutCheck()
{
	// 略

	if (m_enMode == enCreate)
	{
		// 略 作成画面の場合
	}
	else if (m_enMode == enEdit)
	{
		// 略 編集画面の場合
	}
	else if (m_enMode == enDelete)
	{
		// 削除画面の場合
		CString strMessage = _T("");
		strMessage.Format(_T("商品ID:%sを廃番にします。\r\nよろしいですか?"), m_strProductID);
		int iResponse = AfxMessageBox(strMessage, MB_YESNO | MB_ICONQUESTION);
		if (iResponse == IDNO)
		{
			//「いいえ」が押された
			return FALSE;
		}
	}

	return TRUE;
}

最後に、OKボタンが押された際の関数。

/**
 * @fn
 * @brief	OKボタンが押されると呼応される
 * @param	なし
 * @return	なし
 */
void CEditMain::OnBnClickedOk()
{
	// TODO: ここにコントロール通知ハンドラー コードを追加します。
	if (bInutCheck() == TRUE)
	{
		// 略
		if (m_enMode == enCreate)
		{
			// 略 作成処理
		}
		else if (m_enMode == enEdit)
		{
			// 略 編集処理
		}
		else if (m_enMode == enDelete)
		{
			if (Manager.bProductTableDelete(m_strProductID) == TRUE)
			{
				// 削除完了
				AfxMessageBox(_T("削除処理が完了しました。\r\n履歴テーブルを確認してください。"), MB_ICONINFORMATION);
				CDialogEx::OnOK();
			}
			else
			{
				// 失敗した
				return;
			}
		}	
	}
}

CMySQLManager側の関数を呼びます。
必要な情報は、商品IDのみ。

SQL処理

その削除処理bProductTableDeleteについて。

何度も書きますが、
本当にDELETEで削除するわけではなく、
削除フラグを立てるだけです。

/**
 * @fn
 * @brief	商品テーブル 削除関数
 * @param	CString         strProductID    商品ID
 * @return	BOOL    TRUE    削除フラグ更新成功
 *                  FALSE   削除フラグ更新失敗
 */
BOOL CMySQLManager::bProductTableDelete(CString strProductID)
{
    try
    {
        // スキーマセット
        m_con->setSchema(SCHEMA);

        sql::SQLString sqlStrId = strConverter(strProductID);

        //UPDATE文を作る
        sql::SQLString strSqlQuery = "UPDATE " + TABLE_PRODUCT + " SET " + COL_PRODUCT_DISCONTINUE + " = 1 WHERE " + COL_PRODUCT_ID + " = '" + sqlStrId + "'";

        //クエリ実行
        std::unique_ptr<sql::PreparedStatement> pstmt(m_con->prepareStatement(strSqlQuery));
        pstmt->executeUpdate();

        // ここまで処理が来たら、登録成功している
        // 履歴テーブル登録
        if (bLogTableInsert(strProductID, LOG_OPERATION_DELETE, _T("0"), _T("0")) == FALSE)
        {
            AfxMessageBox(_T("履歴テーブルの登録に失敗しました。\r\n管理者へ連絡を!!"), MB_ICONERROR);
            return FALSE;
        }
    }
    catch (sql::SQLException& e)
    {
        ErrSQLException(e);
        return FALSE;
    }

    return TRUE;
}

今までと同じ動きです。
discontinue列に1を挿入。
更新で来たら、履歴テーブルも登録。
操作内容は「削除」、
在庫数はもう必要ないので、0にします。
※デフォルト引数の予定でしたが、
 明示的に入れないとダメでした…。

動作確認

ビルドして動作確認です。

一番最初に示しましたが、
今ID=22222、33333、44444が商品テーブルにあります。

ID=33333を削除画面で入力し、キルフォーカスすると。

これはいいでしょう。

続いてOKを押すと、確認メッセージが出てきます。
OKを押すと。

テーブルを見て見ましょう。

廃棄フラグが立っています。

履歴テーブルはというと。

削除の履歴があります。

なんとかできたぁε-(´∀`*)

在庫テーブル表示

ようやく、メイン画面の処理を実施します。

今回は商品テーブルにあるデータを取得。

チェックボックス「全件検索」の場合は全件、
カテゴリーが設定されている場合はそのカテゴリーのみです。

後で具体例を述べますが、
お好みで条件を追加させるのもいいですよね。

SQL処理作成

今回は珍しく、SQLの方から作ります。

今回、ちょっと挑戦的に試したいことがあります。

  • メイン画面でwhileループし、毎回SQL文を呼ぶ
  • SQL文では「1行だけ」取得、データをメイン画面に戻す
  • 次のwhileで次の1行を取得、同じくメイン画面に渡す

…という操作を行います。

全件取得して一気に渡すのもいいですが(というか通常がそれ?)、
色々ありまして…。

なので、SQL文の作り方を少し工夫します。

まずはヘッダー。
MySQLManager.hで定義します。

public:
	// 商品テーブルデータ取得
	BOOL bGetProductData(CString& strProductID, 
						CString& strProductName, 
						CString& strCategory, 
						CString& strUnitPrice, 
						CString& strProductCount, 
						CString& strProductAlert,
						int iRowNum,
						CString strCategoryCondition);

引数の説明

  • 商品ID。取得した値を参照渡し。
  • 商品名。取得した値を参照渡し。
  • カテゴリー。取得した値を参照渡し。
  • 単価。取得した値を参照渡し。
  • 在庫数。取得した値を参照渡し。
  • 在庫アラート。取得した値を参照渡し。
  • 行番号(whileでインクリメント)
  • カテゴリー条件。全件の場合は_T(“*”)がわたる。

続いてSQL文作成。
ORDER句を利用し、毎度同じ順番にデータを取ってき、
iRowNumでどの行を取得するか決めます。

MySQLManager.cppで処理記載。

/**
 * @fn
 * @brief	商品テーブル一覧ゲット
 * @param	CString&        strProductID        商品ID
 * @param   CString&        strProductName      商品名
 * @param   CString&        strCategory         カテゴリー
 * @param   CString&        strUnitPrice        単価
 * @param   CString&        strProductCount     在庫数
 * @param   CString&        strProductAlert     在庫アラート
 * @param   int             iRowNum             検索行
 * @param   CString         strCategoryCondition    カテゴリー条件
 * @return	BOOL    TRUE    検索成功
 *                  FALSE   検索失敗
 */
BOOL CMySQLManager::bGetProductData(CString& strProductID,
                                    CString& strProductName,
                                    CString& strCategory,
                                    CString& strUnitPrice,
                                    CString& strProductCount,
                                    CString& strProductAlert,
                                    int iRowNum,
                                    CString strCategoryCondition)
{
    try
    {
        // スキーマセット
        m_con->setSchema(SCHEMA);

        CString strRowNum = _T("");
        strRowNum.Format(_T("%d"), iRowNum); // CString化
        sql::SQLString strSqlRowNum = strConverter(strRowNum);

        sql::SQLString strSqlCategoryCondition = strConverter(strCategoryCondition);


        sql::SQLString sqlStrWithPhase = 
            "WITH NumberedRows AS ( SELECT *, ROW_NUMBER() OVER ( ORDER BY " + COL_LOGS_PRODUCT_ID + " ) AS row_num FROM " + TABLE_PRODUCT;
        // WITH NumberedRows AS ( SELECT *, ROW_NUMBER() OVER ( ORDER BY product_id) AS row_num FROM product)
        sql::SQLString sqlStrWithWherePhaseDis = " WHERE " + COL_PRODUCT_DISCONTINUE + " IS NULL";
        sql::SQLString sqlStrWithWherePhaseCat = " AND " + COL_PRODUCT_CATEGORY + "= '" + strSqlCategoryCondition + "'";

        sql::SQLString sqlStrSelectPhase = " ) SELECT * FROM NumberedRows WHERE row_num = " + strSqlRowNum;
       
        // 最終的なSQL作成
        sql::SQLString strSqlQuery = sqlStrWithPhase + sqlStrWithWherePhaseDis;
        if (strCategoryCondition != _T("*"))
        {
            // 全件検索ではない⇒カテゴリー検索条件を付ける
            strSqlQuery += sqlStrWithWherePhaseCat;
        }
        strSqlQuery += sqlStrSelectPhase;

        //クエリ実行
        std::unique_ptr<sql::PreparedStatement> pstmt(m_con->prepareStatement(strSqlQuery));

        std::unique_ptr<sql::ResultSet> res(pstmt->executeQuery());

        if (!res->next())
        {
            // ここに入れば、もう検索するものはない...
            return FALSE;
        }
        else
        {
            // 該当行発見
            
            // 商品ID
            std::string strCol = res->getString(COL_PRODUCT_ID);
            strProductID = strConvertFromUTF8ToUTF16(strCol);

            // 商品名
            strCol = res->getString(COL_PRODUCT_NAME);
            strProductName = strConvertFromUTF8ToUTF16(strCol);

            // カテゴリー
            strCol = res->getString(COL_PRODUCT_CATEGORY);
            strCategory = strConvertFromUTF8ToUTF16(strCol);

            // 単価
            strCol = res->getString(COL_PRODUCT_UP);
            strUnitPrice = strConvertFromUTF8ToUTF16(strCol);

            // 在庫数
            strCol = res->getString(COL_PRODUCT_COUNT);
            strProductCount = strConvertFromUTF8ToUTF16(strCol);

            // 在庫アラート
            strCol = res->getString(COL_PRODUCT_ALERT);
            strProductAlert = strConvertFromUTF8ToUTF16(strCol);
        }
    }
    catch (sql::SQLException& e)
    {
        ErrSQLException(e);
        return FALSE;
    }

    return TRUE;
}

SQL作成箇所以外は、基本以前から作っているのと同じ処理です。
検索後、列のデータを参照渡しです。

SQL文について。
そこだけ抜き取るとこんな感じ。

WITH NumberedRows AS 
     ( SELECT *, ROW_NUMBER() OVER ( ORDER BY product_id) AS row_num FROM product WHERE discontinue IS NULL AND category = 'カテゴリー')
       SELECT * FROM NumberedRows WHERE row_num = 行番号 

これで、商品テーブルを商品ID順に並べ、
並べ替えたテーブル(NumberdRows)の上から〇行目のデータを取ってくる、
という作業です。

discontinueがNULLなのは、廃棄されていない商品番号のみ抽出するからです。

注意は、プログラムって基本0からのインクリメントですが、
これは1番最初の行は1にしないとダメです。

これを何度も呼び、一番最後まで行って、その次の行はもう検索できないので、
その時は61行目に入ってくれます。

こうなれば、呼び出し元のwhileを抜ける、でいいでしょう。

さぁ、これの呼び出し元を作ります。

メイン画面 リストコントロール定義

まずヘッダーに、リストコントロールを操る変数定義。
InventoryManagementSystemDlg.hに定義します。

また、リストコントロールを描画する関数も定義。

private:
	CListCtrl m_ctrlListCtrlProduct;// リストコントロール

private:
	void ShowProductList();	// リストコントロール描画処理

続いて、InventoryManagementSystemDlg.cppを開いて、
IDと先ほどの変数を紐づけ。

void CInventoryManagementSystemDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialogEx::DoDataExchange(pDX);

	DDX_Control(pDX, IDC_CHECK_MAIN_ALL, m_ctrlCheckSearchAll);		// チェックボックス
	DDX_Control(pDX, IDC_COMBO_MAIN_CATEGORY, m_ctrlComboCategory);	// コンボボックス
	DDX_Control(pDX, IDC_LIST_MAIN_PRODUCT, m_ctrlListCtrlProduct);	// リストコントロール 新規追加
}

7行目を追加です。

次に、OnInitDialogで、
リストコントロールの列を指定します。
ついでに、描画処理関数も挿入。

BOOL CInventoryManagementSystemDlg::OnInitDialog()
{
	CDialogEx::OnInitDialog();

	// 略

	m_ctrlListCtrlProduct.InsertColumn(0, _T("商品ID"), LVCFMT_LEFT, 100);
	m_ctrlListCtrlProduct.InsertColumn(1, _T("商品名"), LVCFMT_LEFT, 150);
	m_ctrlListCtrlProduct.InsertColumn(2, _T("カテゴリー"), LVCFMT_LEFT, 150);
	m_ctrlListCtrlProduct.InsertColumn(3, _T("単価"), LVCFMT_LEFT, 80);
	m_ctrlListCtrlProduct.InsertColumn(4, _T("在庫数"), LVCFMT_LEFT, 80);
	m_ctrlListCtrlProduct.InsertColumn(5, _T("在庫アラート"), LVCFMT_LEFT, 110);

	ShowProductList();	// 描画処理

	return TRUE;  // フォーカスをコントロールに設定した場合を除き、TRUE を返します。
}

前に、SetComboStatusを呼応する処理を入れたので、
その直後あたりで大丈夫でしょう。

因みに、この状態だとレイアウトはこんな感じになります。

これはお好みに合わせて、で。

メイン画面 リスト描画

ShowProductListの処理を作ります。

やることは、
①全件検索?カテゴリー条件アリ?
②条件の下、さっき作ったSQL関数をループ呼応。
③1回のループごとで商品IDなどが取得できるので、リストに追加。

のような感じ。

/**
 * @fn
 * @brief	リストコントロール再描画処理
 * @param	なし
 * @return	なし
 */
void CInventoryManagementSystemDlg::ShowProductList()
{
	// 変数更新
	UpdateData(TRUE);

	CString strCategoryCondition = _T("*");	// 全件検索しておく

	if (m_ctrlCheckSearchAll.GetCheck() == BST_UNCHECKED)
	{
		// 全件検索のチェックが外れている
		// ⇒カテゴリー条件アリ
		GetDlgItemText(IDC_COMBO_MAIN_CATEGORY, strCategoryCondition);	// カテゴリー取得
	}

	CMySQLManager Manager;
	Manager.GetConnection();

	LVITEM lvItem{};
	lvItem.mask = LVIF_TEXT;
	int iRoop = 0;

	while (TRUE)
	{
		iRoop++;	// 行番号インクリメント

		CString strProductID = _T("");		// 商品ID
		CString strProductName = _T("");	// 商品名
		CString strCategory = _T("");		// カテゴリー
		CString strUnitPrice = _T("");		// 単価
		CString strProductCount = _T("");	// 在庫数
		CString strProductAlert = _T("");	// 在庫アラート

		if (Manager.bGetProductData(strProductID, strProductName, strCategory, strUnitPrice, strProductCount, strProductAlert, iRoop, strCategoryCondition) == TRUE)
		{
			lvItem.iItem = iRoop - 1;	// リストコントロールの行番号

			// 商品ID列
			lvItem.iSubItem = 0;
			lvItem.pszText = strProductID.GetBuffer();
			m_ctrlListCtrlProduct.InsertItem(&lvItem);

			// 商品名列
			lvItem.iSubItem = 1;
			lvItem.pszText = strProductName.GetBuffer();
			m_ctrlListCtrlProduct.SetItem(&lvItem);

			// カテゴリー列
			lvItem.iSubItem = 2;
			lvItem.pszText = strCategory.GetBuffer();
			m_ctrlListCtrlProduct.SetItem(&lvItem);

			// 単価列
			lvItem.iSubItem = 3;
			lvItem.pszText = strUnitPrice.GetBuffer();
			m_ctrlListCtrlProduct.SetItem(&lvItem);

			// 在庫数列
			lvItem.iSubItem = 4;
			lvItem.pszText = strProductCount.GetBuffer();
			m_ctrlListCtrlProduct.SetItem(&lvItem);

			// 在庫アラート列
			lvItem.iSubItem = 5;
			lvItem.pszText = strProductAlert.GetBuffer();
			m_ctrlListCtrlProduct.SetItem(&lvItem);
		}
		else
		{
			// この場合は、これ以上データをリストに追加できない
			// SQLExceptionも含める
			break;
		}
	}
}

12~18行目は、カテゴリー条件を作成中。
28行目からはwhileでグルグル回します。
39行目で、先の関数を呼応。
TRUEならデータを取ってきます。
43~71行目に関しては、リストコントロールにデータを入れます。

自分で作っておいてなんですが、
無限ループしそうな作りで怖いですね(;´・ω・)

最後に、この描画処理が走るタイミング。
先ほどOnInitDialogで呼応させたので、初期処理は大丈夫でしょう。
後は、チェックボックスが操作されたタイミングと、
カテゴリーが変更されたタイミング、
そして編集画面を閉じたタイミング。
関数自体は昔自動生成しています。

/**
 * @fn
 * @brief	チェックボックスが変更されるたびに呼ばれる
 * @param	なし
 * @return	なし
 */
void CInventoryManagementSystemDlg::OnBnClickedCheckMainAll()
{
	// TODO: ここにコントロール通知ハンドラー コードを追加します。
	SetComboStatus();	// コンボボックスの状態を変更
	ShowProductList();	// リストコントロール再描画処理
}


/**
 * @fn
 * @brief	コンボボックスが編超されるたびに呼ばれる
 * @param	なし
 * @return	なし
 */
void CInventoryManagementSystemDlg::OnCbnSelchangeComboMainCategory()
{
	// TODO: ここにコントロール通知ハンドラー コードを追加します。
	ShowProductList();	// リストコントロール再描画処理
}

/**
 * @fn
 * @brief	編集ボタンが押された
 * @param	なし
 * @return	なし
 */
void CInventoryManagementSystemDlg::OnBnClickedButtonMainEdit()
{
	// 略

	if (bSelectOK == TRUE)
	{
		// 編集画面を表示させる
		CEditMain dlgEditMain;
		dlgEditMain.SetEditSelectMode(enSelectMode);	// モードを設定
		dlgEditMain.DoModal();

		ShowProductList();
	}
}

11行目と、24行目と、44行目追加。

これで、実行してみます。

動作確認

商品テーブルですが、3つだけじゃ心もとないので、
いくつがデータを増やします。

どんな会社だ…

さて、まずは起動。
初期表示は全件表示になるはずです。

注意なのは、ID=33333。
廃番になっているので、表示されてはいけません

素晴らしい👏

全件検索を外して、カテゴリー条件を付けます。

カテゴリーも変更してみる。
すべて、変更されたタイミングでリストが変わるはずです。

全件検索にチェックを戻します。

商品を作成してみます。

OKボタン押下後、メイン画面に戻ります。

反映されてるー(*’▽’)
(この時点で結構テンション上がってます)

最後に、22222を廃番にします。

これまたOKボタンを押して、削除完了メッセージが出ると。

完璧( ̄▽ ̄)
ID=22222が消えました。

終わりに

メイン画面に映る、商品在庫一覧処理を作りました。

ここまでくれば、あと一息。
履歴確認も同じような作りです。

我ながら、結構いい感じのシステムになっているとは実感しています。

因みに、色々条件を付けてみるのもよいかも、という話ですが。

もう処理紹介は書きませんが、
例えば「在庫が在庫アラートを下回る商品を抽出」とか作ってもよいですかね。

MFCで在庫管理システム作成 7
前回までの記事。履歴検索条件画面今回は履歴検索。検索をかけるために必要な条件を設定する画面を作成します。画面追加4度目の画面追加。リソースビューからDialogを右クリック、新規追加します。プロパティから、ダイアログIDはIDD_DIALO...

コメント

タイトルとURLをコピーしました