Xin chào các Bạn,
Trong bài này xin trao đổi với các Bạn về thiết kế Unbound Form có chứa Subform. SubForm là Form nằm bên trong 1 Form khác.
Như tôi đã trao đổi tại bài #18, trái ngược với Bound Form luôn gắn liền với 1 nguồn dữ liệu xác định (được khai báo tại thuộc tính Record Source), Unbound Form là Form không gắn với một nguồn dữ liệu nào cả.
Đó chính là căn nguyên khiến 1 Unbound Form tránh được xung đột dữ liệu trong quá trình có nhiều người cùng truy xuất dư liệu, hoặc tuy chỉ có mỗi mình ên mần công chuyện với dữ liệu đó nhưng hổng dè đã “mở” nó ra mà quên “đóng” nó lại.
Những điều cần chú ý khi thiết kế 1 Unbound Form tôi đã trình bày tại #18, trong bài này chỉ tập trung vào việc thiết kế 1 Unbound Form nhưng lại có SubForm.
Một ví dụ điển hình cho nhu cầu này là thiết kế Form nhập chứng từ nhập xuất (dưới đây gọi là Main Form), với 1 Subform trình bày chi tiết các mặt hàng phát sinh.
1.
Việc đầu tiên ta cần làm là làm sao để nạp thông tin của 1 chứng từ xác định xuống ô dữ liệu tương ứng trên Form khi cần (vì Unbound Form không duy trì thường trực 1 nguồn dữ liệu gắn kết với nó mà).
Đây chính là trường hợp ta cần làm việc với thông tin của 1 chứng từ xác định đã lập trước.
Trong file minh hoạ, công việc này được thực hiện thông qua các thủ tục:
+ LoadInvoiceInfoToForm: Nạp thông tin chứng từ lên Form
Sub LoadInvoiceInfoToForm(SoCtuSt, Optional NoSetSourceRecForSubForm)
Dim sqlSt As String, SQLrec As ADODB.Recordset
Dim KHrec As ADODB.Recordset
Dim tblName As String, MSKH, vIdKH As Long
‘Xác định nguồn dữ liệu chứa thông tin của chứng từ cần nạp
tblName = "tblctunx"
If IsNull(SoCtuSt) Then Exit Sub
sqlSt = "SELECT * FROM " & GetSchemaTable(tblName) & "." & tblName
sqlSt = sqlSt & " WHERE soctu ='" & SoCtuSt & "'"
Set SQLrec = ProcessRecordset(sqlSt)
'Nếu chứng từ hiện hữu, cho ghi thông tin chi tiết của chứng từ lên các ô dữ liệu tương ứng của Form
If SQLrec.RecordCount > 0 Then
Set objKhachHang = New clsDanhba
With Me
.txtId = SQLrec!id
.txtNgay = SQLrec!Ngay
.cmbNghiepvu = SQLrec!MSNV
.txtTsuat = SQLrec!tsuatvat
.txtNguoiGiaodich = SQLrec!NguoiGiaodich
MSKH = SQLrec!MSKH
sqlSt = "SELECT * FROM " & GetSchemaTable("tblDanhsach") & ".tblDanhsach"
sqlSt = sqlSt & " WHERE MSKH = '" & MSKH & "'"
Set KHrec = ProcessRecordset(sqlSt)
objKhachHang.PopulatePropertiesFromRecordset KHrec
.cmbKhachhang.RowSourceType = "Value List"
.cmbKhachhang.RowSource = objKhachHang.HoChulot & ";" & objKhachHang.MaKhachHang
.cmbKhachhang = objKhachHang.MaKhachHang
.txtDiachi = objKhachHang.Diachi
.txtPhone = objKhachHang.Dtvp
.txtMasoThue = objKhachHang.Msthue
KHrec.Close
Set KHrec = Nothing
' Và nạp nguồn dữ liệu chi tiết các mặt hàng phát sinh cho Subform
If IsMissing(NoSetSourceRecForSubForm) Then SetSourceRecForSubForm Me, "frmCtuNXCT"
End With
End If
'
SQLrec.Close
Set SQLrec = Nothing
End Sub
+
Để nạp nguồn dữ liệu chi tiết các mặt hàng phát sinh cho Subform ta dùng thủ tục SetSourceRecForSubForm
Sub SetSourceRecForSubForm(mForm As Form, sForm As String)
Dim sqlSt As String
Dim SQLrec As ADODB.Recordset
Dim tblName As String
Dim vSoCtu, stChema As String
‘
vSoCtu = mForm!cmbSoCtu
‘Xác định nguồn dữ liệu chứa thông tin chi tiết hàng hoá phát sinh của chứng từ cần nạp
If Not IsNull(vSoCtu) Then
tblName = "tblctunxct"
stChema = GetSchemaTable(tblName)
sqlSt = "SELECT " & stChema & ".tbldmhanghoa.tenhanghoa, " & stChema & ".tblctunxct.*"
sqlSt = sqlSt & " FROM " & stChema & ".tbldmhanghoa INNER JOIN " & stChema & ".tblctunxct"
sqlSt = sqlSt & " ON " & stChema & ".tbldmhanghoa.mahang=" & stChema & ".tblctunxct.mahang"
sqlSt = sqlSt & " WHERE " & stChema & ".tblctunxct.soctu = '" & vSoCtu & "'"
Set SQLrec = ProcessRecordset(sqlSt)
Set mForm(sForm).Form.Recordset = SQLrec
‘Nạp thông tin chi tiết lên các ô dữ liệu tương ứng trên SubForm
With mForm(sForm).Form
.Requery
!txtId.ControlSource = "id"
!txtMahang.ControlSource = "mahang"
!txtTenHanghoa.ControlSource = "tenhanghoa"
!txtCapDvt.ControlSource = "dvt"
!txtDvt.ControlSource = "=IIF(not isnull(dvt),flookup('kihieu','tbldonvitinh','cap=' & [dvt]),'')"
!txtSoluong.ControlSource = "soluong"
!txtDongia.ControlSource = "dongia"
!chkCKTL.ControlSource = "lacktyle"
!txtMucCK.ControlSource = "mucck"
End With
SQLrec.Close
Set SQLrec = Nothing
End If
End Sub
Như vậy, khi ta chọn 1 số chứng từ xác định, ứng dụng sẽ cho chạy các thủ tục nêu trên để nạp nguồn dữ liệu tương ứng cho Main Form và SubForm.
Ta gán các thủ tục cần thực hiện với sự kiện ngay sau khi số chứng từ được cập nhật (cmbSoCtu_AfterUpdate)
Private Sub cmbSoCtu_AfterUpdate()
Dim vSoCtu
ClearInputCTHH ‘Xoá trống các ô nhập chi tiết hàng hoá
vSoCtu = Trim(Me.cmbSoCtu.Text)
LoadInvoiceInfoToForm vSoCtu ‘Nạp thông tin chứng từ đã chọn lên MainForm và SubForm
End Sub
Vậy khi cần hiệu chỉnh chi tiết chứng từ đã lập và đang hiển thị trên Form thì làm sao?
Thật đơn giản các Bạn ạ:
+ Đối với thông tin là chi tiết hàng hoá phát sinh: ta chỉ cần chuyển con trỏ đến dòng ghi mặt hàng cần hiệu chỉnh là ứng dụng sẽ copy các thông tin đó lên các ô có nền sẩm màu sẵn sàng cho ta hiệu chỉnh (hoặc xoá). Hiệu chỉnh xong ta bấm nút lệnh ghi bên phải (có hình chiếc đĩa mềm) để cho ghi lại nội dung vừa ddiiefu chỉnh.
Việc này được thực hiện thông qua thủ tục SaveToInvoiceDetailFromForm
Sub SaveToInvoiceDetailFromForm(Optional InVoiceDetailId)
'Luu thong tin tren form vao tblctunxCT
'UpdateInvoiceDetail
On Error GoTo HandleError
Dim sqlSt As String, tblName As String
Dim vId
Dim MucCK As Double, CKTL As Byte
Call OpenMyConnection
tblName = "tblctunxct"
With Me
vId = Me.txtDetailId
MucCK = Nz(.txtMucCK)
If IsNull(.chkCKTL) Then
CKTL = 0
Else
If .chkCKTL.Value = True Then
CKTL = 1
Else
CKTL = 0
End If
End If
‘Phân biệt là trường hợp nhập mới hay hiệu chỉnh lại dữ liệu đã có.
If Not IsNull(vId) Then
sqlSt = "UPDATE " & GetSchemaTable(tblName) & "." & tblName & " SET "
sqlSt = sqlSt & " soctu ='" & Trim(.cmbSoCtu) & "',"
sqlSt = sqlSt & " mahang ='" & .cmbMSHH & "',"
sqlSt = sqlSt & " dvt =" & .cmbDvt & ","
sqlSt = sqlSt & " soluong =" & .txtSoluong & ","
sqlSt = sqlSt & " dongia =" & .txtDongia & ","
sqlSt = sqlSt & " lacktyle =" & CKTL & ","
sqlSt = sqlSt & " mucck =" & MucCK
sqlSt = sqlSt & " WHERE ("
sqlSt = sqlSt & " soctu='" & Trim(Me.cmbSoCtu) & "'"
sqlSt = sqlSt & " AND id=" & InVoiceDetailId
sqlSt = sqlSt & ")"
Else
sqlSt = "INSERT INTO " & GetSchemaTable(tblName) & "." & tblName
sqlSt = sqlSt & "(soctu, mahang, dvt, soluong, dongia, lacktyle, mucck)"
sqlSt = sqlSt & " VALUES ("
sqlSt = sqlSt & " '" & Trim(.cmbSoCtu) & "',"
sqlSt = sqlSt & " '" & .cmbMSHH & "',"
sqlSt = sqlSt & " " & .cmbDvt & ","
sqlSt = sqlSt & " " & .txtSoluong & ","
sqlSt = sqlSt & " " & .txtDongia & ","
sqlSt = sqlSt & " " & CKTL & ","
sqlSt = sqlSt & " " & Nz(MucCK)
sqlSt = sqlSt & ")"
End If
End With
MyConn.Execute sqlSt
Call CloseMyConnection ‘Dùng xong rồi thì đóng lại cho đỡ tốn tài nguyên và khỏi gặp xung đột dữ liệu đó các Bạn.
HandleError:
If Err > 0 Then
GeneralErrorHandler Err.Number, Err.Description, NhapXuat_FORM, "SaveToInvoiceDetailFromForm"
Exit Sub
End If
End Sub
Ở đây chúng ta chú ý: có 2 trường hợp cần phân biệt là nhập mới và hiệu chỉnh thông tin đang có.
Xem trong thủ tục trên chúng ta thấy thủ tục có phân biệt 2 trường hợp này bằng cách xet giá trị của ô txtDetailId, đây là ô chứa giá trị Id của chi tiết hàng hoá phát sinh. Trong thiết kế, ta cho ô này ẩn đi (bằng cách khai báo thuộc tính Visible = False). Nếu ô này có chứa nội dung xác định thì là trường hợp hiệu chỉnh dữ liệu đang có, ngược lại nếu nó rổng không (IsNull) là trường hợp nhập mới.
Trường hợp muốn xoá dòng ghi chi tiết hàng phát sinh xác định: ta cho nạp dòng ghi chi tiết hàng hoá đó lên các ô sẩm màu rồi bấm nút lệnh Xoá (có hình gạch chéo) nằm bên trái dòng của các ô sẩm màu này.
Thủ tục tương ứng như sau:
Dim vHoi As Long, sqlSt As String, tblName As String
Dim DetailId
DetailId = Me.txtDetailId
If Not IsNull(DetailId) Then
vHoi = Eval("msgbox('" & "Ban vua ra lenh cho xoa dong ghi mat hang nay" & vbCrLf & "Co phai Ban chac chan muon Xoa hay khong?" & "@" & "Bam YES de xoa, bam NO de huy bo lenh nay" & "@" & "',36,'Xoa chi tiet hang hoa')")
If vHoi = vbYes Then
tblName = "tblctunxct"
sqlSt = "DELETE " & GetSchemaTable(tblName) & "." & tblName
sqlSt = sqlSt & " WHERE Id =" & DetailId
'
Call OpenDbConnection
ExecuteSQLCommand sqlSt
Call CloseDbConnection
'Xoá xong thì cho cập nhật lại nội dung hiển thị trên SubForm
SetSourceRecForSubForm Me, "frmCtuNXCT"
End If
End If
+ Đối với thông tin chung của chứng từ: cũng tương tự như trên, ta hiệu chỉnh thông tin tại các ô tương ứng; và cũng phân biệt 2 trường hợp: nhập mới và hiệu chỉnh thông tin đang có. Việc ghi lại các thông tin đã cập nhật vào bảng dữ liệu ghi chứng từ phát sinh được thực hiện bằng thủ tục SaveToInvoiceFromForm
Sub SaveToInvoiceFromForm(Optional InvoiceId, Optional NoLoadInfo)
'Luu thong tin tren form vao tblctunx
'UpdateOrInsert:
'+ True: Luu thong tin thay doi vao mau tin dang hien huu
'+ Flase: Them mau tin moi
'InvoiceId: so chung tu
'
On Error GoTo HandleError
Dim sqlSt As String, tblName As String
Dim vId
Call OpenMyConnection
tblName = "tblctunx"
With Me
vId = Me.txtId
If Not IsNull(vId) Then
If IsNull(InvoiceId) Then Exit Sub
sqlSt = "UPDATE " & GetSchemaTable(tblName) & "." & tblName & " SET "
sqlSt = sqlSt & " soctu ='" & .cmbSoCtu & "',"
sqlSt = sqlSt & " ngay ='" & Format$(.txtNgay, "dd-mmm-yy") & "',"
sqlSt = sqlSt & " msnv ='" & .cmbNghiepvu & "',"
sqlSt = sqlSt & " mskh ='" & .cmbKhachhang & "'"
If Not IsNull(.txtNguoiGiaodich) Then sqlSt = sqlSt & ", nguoigiaodich ='" & .txtNguoiGiaodich & "'"
If Not IsNull(.txtTsuat) Then sqlSt = sqlSt & ", tsuatvat =" & .txtTsuat
sqlSt = sqlSt & " WHERE ("
sqlSt = sqlSt & " soctu='" & InvoiceId & "'"
sqlSt = sqlSt & ")"
Else
If IsNull(.cmbSoCtu) Then Exit Sub
sqlSt = "INSERT INTO " & GetSchemaTable(tblName) & "." & tblName
sqlSt = sqlSt & "(soctu, ngay, msnv, mskh"
If Not IsNull(.txtNguoiGiaodich) Then sqlSt = sqlSt & ", nguoigiaodich"
If Not IsNull(.txtTsuat) Then sqlSt = sqlSt & ", tsuatvat"
sqlSt = sqlSt & ")"
sqlSt = sqlSt & " VALUES ("
sqlSt = sqlSt & " '" & .cmbSoCtu & "',"
sqlSt = sqlSt & " '" & Format$(.txtNgay, "dd-mmm-yy") & "',"
sqlSt = sqlSt & " '" & .cmbNghiepvu & "',"
sqlSt = sqlSt & " '" & .cmbKhachhang & "'"
If Not IsNull(.txtNguoiGiaodich) Then sqlSt = sqlSt & ", '" & .txtNguoiGiaodich & "'"
If Not IsNull(.txtTsuat) Then sqlSt = sqlSt & ", " & .txtTsuat
sqlSt = sqlSt & ")"
End If
End With
MyConn.Execute sqlSt
‘Lưu xong thì cho cập nhật lại các thông tin đã lưu lên Form.
If IsMissing(NoLoadInfo) Then
LoadInvoiceInfoToForm Me.cmbSoCtu
Else
LoadInvoiceInfoToForm Me.cmbSoCtu, True
End If
Call CloseMyConnection ‘Mở ra xài xong thì đóng lại
HandleError:
If Err > 0 Then
GeneralErrorHandler Err.Number, Err.Description, NhapXuat_FORM, "SaveToInvoiceFromForm"
Exit Sub
End If
End Sub
Trong thủ tục trên ta chú ý đoạn
If IsMissing(NoLoadInfo) Then
LoadInvoiceInfoToForm Me.cmbSoCtu
Else
LoadInvoiceInfoToForm Me.cmbSoCtu, True
End If
Cho Lưu xong thì cho cập nhật lại các thông tin đã lưu lên Form. Cái này cần để ghi bổ sung những thông tin chỉ phát sinh khi dữ liệu được ghi vào bảng dữ liệu, chẳng hạn như chỉ số Id tự động của bản ghi, hoặc các giá trị tính toán cần thiết khác.
Khi ghi dữ liệu vào bảng dữ liệu, chúng ta cần chú ý đến 1 thực tế là có những thông tin chi tiết của chứng từ không nhất thiết lúc nào cũng có. Do vậy khi ta viết các thủ tục cập nhật phải chú ý đến các trường hợp này. Các Bạn có thể thấy điều này được thể hiện ở những dòng sau đây trong thủ tục nêu trên:
If Not IsNull(.txtNguoiGiaodich) Then sqlSt = sqlSt & ", '" & .txtNguoiGiaodich & "'"
If Not IsNull(.txtTsuat) Then sqlSt = sqlSt & ", " & .txtTsuat
Ở đây tôi xác định các chi tiết: Người trực tiếp giao dịch, thuế suất VAT là những chi tiết thông tin không phải lúc nào cũng bắt buộc phải có khi lập chứng từ nên đã dự liệu bằng các statement IF... THEN ...
Bài đã dài. Xin hẹn các Bạn trong bài sau.