ดาวน์โหลดโปรแกรม RSS Reader ได้ที่นี่ ...

|
|
|
Visitors - Session views |       
7 ธันวาคม พ.ศ.2549 4 Users On-Line. |
|
Visitors - Page views |        1 กุมภาพันธ์ พ.ศ.2551 |
|
|
|
 |
|
แจกฟรี Source Code VB6+Access โปรแกรมระบบฐานข้อมูลครุภัณฑ์ ภาคเขียนโปรแกรม |
Category »
VB 6/VB.Net โดย : Webmaster เมื่อ 16/4/2552 เวลา: 12:20 | (อ่าน : 342069) | พี่น้องครับ ... ลำพังความคิด ความเชื่อ ตามหลักการต่างๆมันยังไม่เพียงพอ มันต้องมีการพิสูจน์ด้วยว่า ตารางข้อมูลที่ได้ออกแบบมานั้น มันมีความถูกต้อง และ แม่นยำจริงหรือไม่อย่างไร แน่นอนครับ ... มันต้องใช้การพิสูจน์ด้วยการลงมือปฏิบัติเท่านั้น และสิ่งที่ผมคิดไว้ และ ทำได้แล้ว ดังนั้น การถ่ายทอดความรู้ไปยังผู้อื่น "ของจริง คือ สื่อที่เข้าใจได้ง่าย และ ดีที่สุด" ... เอาล่ะครับ สำหรับผู้เรียนรู้ในระดับเริ่มต้นย่อมจะมองภาพออกยากมาก ส่วนระดับกลางๆก็พอจะมองออกล่ะว่า มันยุ่งยากลำบาก และ เป็นภาระต่อผู้พัฒนาโปรแกรมมากจนเกินไป พี่น้องครับ ... อย่าลืมว่า ผู้เขียนไม่ได้ใช้ ผู้ใช้ไม่ได้เขียน แต่ผู้เขียนต้องตามใจผู้ใช้งาน ไม่อย่างนั้นแล้วผู้ใช้งานเขา (หรือเธอ) ตามองค์กรต่างๆ จะมีความรู้สึกต่อต้านงานที่เราสร้างมันขึ้นมา ที่เห็นๆชัดเจนเลยก็คือ ผู้ใช้จะหันกลับไปใช้ Excel เก็บข้อมูลอย่างเดิมยังจะดีซ่ะกว่าอีก ... 55555+ ...
 |
 |

การนำข้อมูลจากตารางกริดมาแสดงผล - โปรแกรมย่อย RecordToScreen
เพิ่มเติม
- เท่าที่ผมพบเห็นตามหนังสือ ตามเว็บ หรือ โปรแกรมที่ขายในราคาหลักร้อย หลักพัน (หลักหมื่นก็มี) ล้วนแล้วแต่ใช้วิธีการเก็บข้อมูลแบบ Text แทบทั้งสิ้น ในกรณีแบบนี้ ข้อมูลที่แสดงใน ComboBox หากมีค่าซ้ำกัน เขาก็ใช้คำสั่ง SELECT DISTINCTROW เพื่อลดจำนวนการแสดงผล จากนั้นก็จะนำค่าที่เป็น Text เก็บลงตารางหลักไปเลย ... อนึ่ง แม้ว่าเราพัฒนาโปรแกรมขึ้นมา และไม่ได้เป็นคนใช้งานก็ตามที ก็อย่าได้ลืมเรื่องของการ Maintenance ระบบฐานข้อมูล การเพิ่มเติม ปรับปรุงประสิทธิภาพในวันข้างหน้าเอาไว้ด้วย ... ดังนั้นเมื่อพี่น้องได้เข้ามาอ่านบทความนี้แล้ว ก็ต้องใช้วิจารณญาณในการตัดสินใจเลือกเอาเองล่ะกันครับ แบบไหนที่มันมีประสิทธิภาพมากกว่ากัน
เริ่มต้นกระบวนการทำงาน
' ส่วนของการนำข้อมูลมาแสดงผล
Sub RecordToScreen()
Set RS = New Recordset
' นำข้อมูลจากตารางมาแสดงผล
' ไม่ต้องมาขยันนั่งพิมพ์เองหรอกครับ
' ไปใช้แบบสอบถาม (Query) ใน MS Access และทำการตัดเข้ามาเลย ...
Statement = "SELECT tblAsset.AssetPK, tblAsset.AssetID, tblAsset.SerialNumber, " & _
" tblAsset.Class, tblAsset.Model, tblAsset.DateReceived, tblAsset.UnitPrice, " & _
" tblAsset.Reference, tblAsset.Memo, tblAsset.DateAdded, " & _
" tblAsset.DateModified, tblAssetName.AssetName, tblBrandName.BrandName, " & _
" tblGroup.GroupName, tblUnit.UnitName, tblSource.SourceName, " & _
" tblStatus.StatusName, tblLocation.LocationName " & _
" FROM (tblSource INNER JOIN ((tblGroup INNER JOIN " & _
" (tblBrandName INNER JOIN (tblAssetName INNER JOIN " & _
" (tblUnit INNER JOIN tblAsset ON tblUnit.UnitPK = tblAsset.UnitFK) ON " & _
" tblAssetName.AssetNamePK = " & _
" tblAsset.AssetNameFK) ON tblBrandName.BrandNamePK = " & _
" tblAsset.BrandNameFK) ON tblGroup.GroupNamePK = tblAsset.GroupNameFK) " & _
" INNER JOIN tblLocation ON tblAsset.LocationFK = tblLocation.LocationPK) " & _
" ON tblSource.SourcePK = tblAsset.SourceFK) INNER JOIN tblStatus ON " & _
" tblAsset.StatusFK = tblStatus.StatusPK " & _
" WHERE [tblAsset.AssetPK] = " & PK & _
" ORDER BY [tblAsset.AssetPK] "
RS.Open Statement, ConnDB, adOpenForwardOnly, adLockReadOnly, adCmdText
' กำหนดการแสดงผลข้อมูลบนหน้าจอ
txtAssetID.Text = "" & RS("AssetID")
' ต้องเก็บค่าเดิมของทะเบียนครุภัณฑ์ไว้ก่อน (อ่านรายละเอียดที่ cmdSave_Click)
' ใช้เทคนิคง่ายๆ หมูๆ ... เอาค่าใน Text ไปเก็บไว้ในหาง เอ้ย Tag
' พี่น้องคงจะได้รู้ประโยชน์ของการใช้งาน Tag แหละคราวนี้ ... ความลับที่ซุกซ่อนใน VB มาแสนนาน
' มันมีคุณสมบัติประจำตัว Tag เอาไว้ทำอะไร ... 55555+ ... จุ๊กกรู๊
txtAssetID.Tag = txtAssetID.Text
' การโหลดค่าจากตารางย่อย (tblBrandName) เข้าสู่ ComboBox
' พิจารณาการเชื่อมโยงตารางยี่ห้อ (tblBrandName)
' ไปโปรแกรมย่อยในการโหลดรายการต่างๆของตารางย่อย (Detail) เข้าสู่ ComboBox ค่าที่ส่งไปมี
' ชื่อ ComboBox, ชื่อตาราง, ชื่อ Field ที่เป็น Primary Key, ชื่อฟิลด์ที่เป็นรายการ
Call LoadComboBox( _
cmbBrandName, _
"tblBrandName", _
"BrandNamePK", _
"BrandName" _
)
' เอาค่าที่อยู่ในตารางข้อมูล เทียบค่าให้ตรงกันในรายการ (List) ของ ComboBox
cmbBrandName.Text = RS("BrandName")
' ===========================================================================
' ส่วนอื่นๆ ให้ไปดูที่โค้ดโปรแกรมได้เลย
' .........................
' .........................
End Sub
' Load รายการเข้าสู่ ComboBox ค่าที่ต้องส่งมา 4 ชุด คือ
' ชื่อ ComboBox, ชื่อตาราง, ชื่อฟิลด์ Primary Key และ ชื่อฟิลด์รายการ
Sub LoadComboBox( _
cmb As ComboBox, _
tblName As String, _
FieldPK As String, _
FieldName As String _
)
Set DS = New ADODB.Recordset
SQLStmt = "SELECT * FROM " & tblName & " ORDER BY " & FieldName
Set DS = ConnDB.Execute(SQLStmt, , adCmdText)
cmb.Clear
Do Until DS.EOF
cmb.AddItem "" & DS(FieldName)
DS.MoveNext
Loop
DS.Close: Set DS = Nothing
End Sub
|
การค้นหาข้อมูลใน ComboBox - โปรแกรมย่อย SearchComboBox
การจำกัดความยาวข้อมูลใน ComboBox - โปรแกรมย่อย MaxComboBox
' จะอยู่ในเหตุการณ์ของการกดแป้นคีย์บอร์ด
Private Sub cmbBrandName_KeyPress(KeyAscii As Integer)
If KeyAscii = vbKeyReturn Then
KeyAscii = 0
SendKeys "{TAB}"
Else
' ไปโปรแกรมย่อยในการค้นหาคำใน ComboBox โดยส่งค่าไป 2 ตัว
' ชื่อของ ComboBox และ KeyAscii ที่กดลงไป
Call SearchComboBox(cmbBrandName, KeyAscii)
' จำกัดความยาวของการพิมพ์คำใน ComboBox
Call MaxComboBox(cmbBrandName, 80, KeyAscii)
End If
End Sub
' =========================================================
' โปรแกรมย่อยในการค้นหาคำในรายการของ ComboBox
Private Sub SearchComboBox(cmb As ComboBox, KeyAscii As Integer)
' =========================================================
Dim strKey As String, iRet As Long, LenKey As Long
cmb.SelText = ""
strKey = cmb.Text & Chr$(KeyAscii)
iRet = SendMessage(cmb.hWnd, CB_FINDSTRING, -1, ByVal strKey)
If iRet <> CB_ERR Then
LenKey = Len(strKey)
cmb.Text = cmb.List(iRet)
cmb.ListIndex = iRet
KeyAscii = 0
cmb.SelStart = LenKey
cmb.SelLength = Len(cmb.Text) - LenKey
End If
End Sub
' =========================================================
' =========================================================
' ฟังค์ชั่นที่ช่วยจำกัดความยาวข้อมูลสำหรับ ComboBox
Private Sub MaxComboBox(cmb As ComboBox, MaxChar As Integer, KeyAscii As Integer)
' =========================================================
If Len(cmb.Text) >= MaxChar Then ' ถ้าหากมีความยาวมากกว่า หรือ เท่ากับที่ได้ตั้งไว้
If KeyAscii <> vbKeyBack Then ' เป็นการกดคีย์ Back Space หรือไม่
KeyAscii = 0 ' ไม่ใช่ให้ถือว่าไม่ได้กดคีย์ใดๆเลย
End If
End If
End Sub
' =========================================================
|
การบันทึกข้อมูล แบ่งออกได้ 2 ลักษณะ คือ
- การเพิ่มข้อมูลใหม่ ส่วนนี้สาระสำคัญ คือ การหา Primary Key ตัวใหม่ และ AssetID ต้องไม่ไปซ้ำกับของเดิม
- การแก้ไขข้อมูล สาระสำคัญต้องไม่ให้การแก้ไขแล้ว AssetID มีค่าซ้ำกับของเดิม
Private Sub cmdSave_Click()
' ค่า AssetID หรือ ทะเบียนครุภัณฑ์ จำเป็นต้องป้อนเข้ามา
If Trim(txtAssetID.Text) = "" Or Len(Trim(txtAssetID.Text)) = 0 Then
MsgBox "กรุณาป้อนทะเบียนครุภัณฑ์ให้เรียบร้อยก่อนด้วย.", vbOKOnly + vbExclamation, "รายงานสถานะ"
txtAssetID.SetFocus
Exit Sub
End If
'
' ตรวจสอบการซ้ำกันของรหัสทะเบียนครุภัณฑ์
' =================================================================
' มันมีโอกาสเป็นได้ 2 กรณี คือ
' เพิ่มข้อมูลใหม่ - ทำให้ txtAssetID.Text จะไม่ตรงกันกับ txtAssetID.Tag (ค่านี้จะต้องว่าง)
' แก้ไขข้อมูล - มีโอกาสได้ 2 ทาง คือ
' 1. ไม่มีการแก้ไขค่าใน txtAssetdID.Text จะทำให้ txtAssetID.Text = txtAssetID.Tag
' ดังนั้นไม่ต้องไปเสียเวลาทำการเปรียบเทียบค่าเดิมในฐานข้อมูล
' 2. มีการแก้ไขค่าใน txtAssetID.Text ดังนั้น txtAssetID.Text <> txtAssetID.Tag ทำให้
' ต้องนำค่าไปตรวจสอบว่ามีค่า txtAssetID.Text (ที่เปลี่ยนไป) ไปซ้ำกับค่าเดิมในฐานข้อมูลหรือไม่
' เขียน VB มานับ 10 ปี ... เทคนิคง่ายๆนี้ ผมก็ยังใช้งานได้ไม่เปลี่ยนแปลงทั้ง VB6 หรือ VB.Net
' =================================================================
If txtAssetID.Text <> txtAssetID.Tag Then
If CheckNewCode > 0 Then
MsgBox "มีทะเบียนครุภัณฑ์: " & Trim(txtAssetID.Text) & " เรียบร้อยแล้ว กรุณาแก้ไขใหม่ด้วย.", _
vbOKOnly + vbExclamation, "รายงานสถานะ"
txtAssetID.SetFocus
Exit Sub
End If
End If
' ================================
' ไปบันทึกข้อมูลได้เลย
Call SaveData
' ================================
End Sub
' =================================================================
' ฟังค์ชั่นตรวจสอบการซ้ำกันของทะเบียนครุภัณฑ์ (หรืออื่นๆ) กรณีข้อมูลเป็น Text
' จากนั้นส่งค่ากลับ หากเป็น 0 แสดงว่าไปไม่เกิดการซ้ำกันของข้อมูล
' ค่าส่งกลับมากกว่า 0 ... เกิดการซ้ำกัน จะต้องบังคับไม่สามารถเพิ่ม หรือ แก้ไขข้อมูลได้
' =================================================================
Function CheckNewCode() As Long
Set DS = New Recordset
SQLStmt = "SELECT * FROM tblAsset WHERE [AssetID] = " & "'" & Trim(txtAssetID.Text) & "'" & _
" ORDER BY [AssetPK] "
' หากไม่ระบุเป็น adUseClient จะใช้ค่าเดิมที่ตั้งต้น (Default) เป็นแบบ adUseServer
' การใช้แบบ adUseClient เพื่อต้องการให้ใช้เมธอดของการนับ Record ได้ นั่นคือ
' DS.RecordCount
DS.CursorLocation = adUseClient
DS.Open SQLStmt, ConnDB, adOpenForwardOnly,adLockReadOnly, adCmdText
CheckNewCode = DS.RecordCount
DS.Close: Set DS = Nothing
End Function
' =================================================================
' โปรแกรมย่อยในการบันทึกข้อมูล ไม่ว่าจะเป็นการเพิ่ม หรือ การแก้ไขข้อมูล
' =================================================================
Private Sub SaveData()
Set RS = New Recordset
' มันเป็นเทคนิคของการลดจำนวนโค้ดลง ผมใช้มานับ 10 ปีแล้ว ... ไม่เปลี่ยน
' กรณีเป็นการเพิ่มข้อมูลใหม่
If NewData Then
' ค้นหาค่า PK ก่อน
Call SetupNewData
'
Statement = "SELECT * FROM tblAsset ORDER BY AssetPK"
RS.Open Statement, ConnDB, adOpenKeyset, adLockOptimistic, adCmdText
' ผมมันติด AddNew มาตั้งแต่ใช้ DAO แล้วครับ ... ส่วนนี้คือการใช้ INSERT น่ะครับ
RS.AddNew
RS("AssetPK") = PK
RS("DateAdded") = FormatDateTime(Now(), vbShortDate)
RS("DateModified") = FormatDateTime(Now, vbShortDate)
'========== แก้ไขข้อมูล ============
Else
'
Statement = "SELECT * FROM tblAsset WHERE AssetPK = " & PK
RS.Open Statement, ConnDB, adOpenKeyset, adLockOptimistic, adCmdText
End If
' กรณีของ Text เพื่อป้องกันค่าว่าง ให้ใส่เครื่องหมาย Double Quote ไว้ด้านหน้าของ TextBox เสมอ
RS("AssetID") = "" & Trim(txtAssetID.Text)
RS("SerialNumber") = "" & Trim(txtSerialNumber.Text)
RS("Model") = "" & Trim(txtModel.Text)
RS("Class") = "" & Trim(txtClass.Text)
'
' ตรวจสอบค่าใน ComboBox
' ยี่ห้อ - BrandName
' ชื่อครุภัณฑ์ โดยการส่งค่าไปตรวจสอบหาค่า Primary Key ของตารางย่อย (Detail) ค่าที่ส่งไป มี
' ชื่อ ComboBox, ชื่อตาราง, Field ที่เป็น PK, Field ที่เป็นรายการ (ค่าที่ต้องทดสอบหา Primary Key)
' ค่าที่ส่งกลับมาจะเป็น Primary Key ของแต่ละตารางย่อยนั่นเอง
' และ Primary Key ตัวนี้ก็คือ Foreign Key ในตารางหลัก (tblAsset)
' อย่างที่ได้บอกไปตอนออกแบบข้อมูล เราจะเก็บค่า Foreign Key (BrandNameKF) นี้ลงในตารางหลักเท่านั้น
RS("BrandNameFK") = VerifyComboBox( _
cmbBrandName, _
"tblBrandName", _
"BrandNamePK", _
"BrandName" _
)
' กรณีของ ComboBox ตัวอื่นๆก็เช่นเดียวกัน ดูได้จากโค้ดโปรแกรมจริงๆ
' ........................
' ........................
RS.Update
RS.Close: Set RS = Nothing
'
NewData = False
' ส่งค่าไปบอกฟอร์มหลักให้ Refresh
FormUpdate = True
MsgBox "บันทึกข้อมูลเรียบร้อย", vbOKOnly + vbInformation, "รายงานสถานะ"
Unload Me
End Sub
' ===================== สร้าง Record ใหม่ ==========================
' ต้องคำนวณหาค่า Primary Key ให้เรียบร้อยก่อน
Sub SetupNewData()
' ==========================================================
Dim Rec As Long
Set DS = New Recordset
' นำข้อมูลจากตารางมาคำนวณหาค่า Primary Key สูงสุด
SQLStmt = "SELECT Max(tblAsset.AssetPK) As MaxPK FROM tblAsset "
' กรณีการอ่านข้อมูลต้องใช้ adOpenForwardOnly คู่กับ adLockReadOnly เสมอ เพื่อการอ่านข้อมูลได้เร็วกว่า
DS.Open SQLStmt, ConnDB, adOpenForwardOnly, adLockReadOnly, adCmdText
' ตัวแปร PK เป็นตัวแปรแบบ Public มองเห็นได้ทั่วทั้งฟอร์มนี้
PK = DS("MaxPK") + 1
DS.Close: Set DS = Nothing
End Sub
' ==========================================================
' ฟังค์ชั่นที่ใช้ในการตรวจสอบค่าที่อยู่ใน ComboBox เพื่อค้นหาค่า Primary Key ในตารางย่อย
' หากหาข้อมูลไม่พบ ก็สามารถบันทึกค่าที่คีย์เข้าไปใหม่ได้เลย โดยไม่จำเป็นต้องออกไปเพิ่มข้อมูลใหม่แต่อย่างใด
' ==========================================================
Function VerifyComboBox( _
cmb As ComboBox, _
tblName As String, _
FieldPK As String, _
FieldName As String _
) As Integer
Dim CountRec As Integer ' ไว้นับจำนวนของตารางย่อย
' ตรวจสอบว่ามีการป้อนข้อมูลหรือไม่ หากไม่มีให้กำหนดค่า Default เป็น 0
' จากนั้น Return ค่ากลับ และออกจากฟังค์ชั่นไปเลยครับพี่น้อง ... เพื่อเป็นการไม่เสียเวลา
If cmb.Text = "" Or Len(cmb.Text) = 0 Or cmb.Text = "-" Then
VerifyComboBox = 0
Exit Function
End If
Set DS = New Recordset
SQLStmt = "SELECT * FROM " & tblName & " WHERE [" & FieldName & "] = " _
& "'" & Trim(cmb.Text) & "'" & _
" ORDER BY " & FieldPK
' ======================================================================
' หลายคนมักทำผิด และมองข้ามมันไป สำหรับการเขียน SQL Statement
' SQL Statement ... การค้นหาค่าโดยการเปรียบเทียบกับข้อมูลชนิดข้อความ Text หรือ String
' SELECT * FROM ... WHERE [ฟิลด์แบบข้อความ] = '1020' ... (อ่านว่า หนึ่ง ศูนย์ สอง ศูนย์)
' เวลาเขียน Statement จะต้องเขียนค่าที่นำมาเปรียบเทียบให้อยู่ภายใต้เครื่องหมาย Single Quote (') เช่น
' "SELECT * FROM ... WHERE [AssetID] = " & "'" & txtAssetID.Text & "'" ... จดจำรูปแบบนี้ให้ดี
' ส่วนกรณีของตัวเลขไม่ต้องมีเครื่องหมาย Single Quote เช่น
' SELECT * FROM ... WHERE AssetPK = 1020 (อ่านว่า หนึ่งพันยี่สิบ) เช่น
' "SELECT * FROM ... WHERE [AssetPK] = " & txtAssetPK.Text
' ======================================================================
DS.CursorLocation = adUseClient
DS.Open SQLStmt, ConnDB, adOpenForwardOnly, adLockReadOnly, adCmdText
CountRec = DS.RecordCount
' แสดงว่าไม่มีในรายการ ดังนั้นเราต้องเพิ่มรายการเข้าไปใหม่ในตารางย่อย
If CountRec <= 0 Then
Set DS = New Recordset
SQLStmt = "SELECT Max(" & tblName & "." & FieldPK & ") As MaxPK " & " FROM " & tblName
DS.CursorLocation = adUseClient
DS.Open SQLStmt, ConnDB, adOpenForwardOnly, adLockReadOnly, adCmdText
' เพิ่มค่า Primary Key ของตารางย่อย (Detail) ขึ้นอีก 1
CountRec = DS("MaxPK") + 1
' การที่ผมไม่สั่งปิดตารางข้อมูล DS.Close ก็เพราะคำสั่ง Set DS = New Recordset
' มันจะตัดการชื่อมต่อเดิมออกไปในทันทีได้เลยครับ ... ไม่ต้องห่วง
Set DS = New Recordset
SQLStmt = "SELECT * FROM " & tblName & " ORDER BY " & FieldPK
' การบันทึกข้อมูล จะใช้ adOpenKeyset คู่กับ adLockOptimistic เสมอครับ
DS.Open SQLStmt, ConnDB, adOpenKeyset, adLockOptimistic, adCmdText
' ผมมันติด AddNew มาตั้งแต่ใช้ DAO แล้วครับ ... ส่วนนี้คือการใช้ INSERT น่ะครับ
DS.AddNew
DS(FieldPK) = CountRec
DS(FieldName) = cmb.Text
DS.Update
' ส่งค่า PK กลับไปเพื่อบันทึกข้อมูล
VerifyComboBox = CountRec
' มีข้อมูลเดิมอยู่แล้ว
Else
' ส่งค่า PK กลับไปเพื่อบันทึกข้อมูล
VerifyComboBox = DS(FieldPK)
End If
DS.Close: Set DS = Nothing
End Function
' เคลียร์ข้อมูลต่างๆใหม่
Sub SetupScreen()
' ============= เสริมเทคนิคการเคลียร์ค่าต่างๆของ Control ในฟอร์ม ==============
Dim Ctl As Control
' สำหรับ Control ทุกๆตัวที่วางแปะลงบน Form
For Each Ctl In Me
' ถ้า Control ตัวนั้นมันเป็น TextBox ก็ทำการใส่ค่าว่างให้มันซ่ะ
If TypeOf Ctl Is TextBox Then Ctl.Text = ""
' ถ้า Control ตัวนั้นมันเป็น ComboBox ก็ทำการเคลียร์ค่าว่างให้มันซ่ะ
If TypeOf Ctl Is ComboBox Then Ctl.Clear
Next ' Control ตัวถัดไป
End Sub
|
Conclusion: ผมก็คาดหวังเล็กๆว่า คงพอที่จะทำให้พี่น้องหลายท่านได้แนวคิด ได้มุมมองแปลกๆ ใหม่ๆ เอาไว้ในอ้อมกอด อ้อมใจ กันบ้างพอสมควรน่ะครับ ผมตระหนักดีว่าแค่การสร้างความสัมพันธ์ของตารางข้อมูลแบบ 1 : 1 เนี่ย พอมาลงโค้ดจริงๆมันก็วุ่นวายกันพอสมควรแล้ว (บางคนอาจจะบอกว่ามันยากไปซ่ะด้วยซ้ำ) รอบหน้าผมจะเอาตัวอย่างงานจริงของการเบิกจ่ายวัสดุสิ้นเปลืองมาเป็นแนวทางให้ชม แน่นอนว่ามันจะต้องเป็นลักษณะของความสัมพันธ์แบบ 1 : M ... อย่าพึ่งท้อกันก่อนล่ะครับ ... พี่น้อง
|
|