Wednesday, July 25, 2007

ก้าว(bug)แรกใน OFBiz

หลังจากได้ความรู้จากน้อง sand ที่บรรยายในงาน NJUG #3 มาแล้ว
ก็ถึงเวลาที่ต้องทดลองเสียหน่อย (โหลดมาทิ้งไว้นานแล้ว แต่ไม่ได้เริ่มสักที)

การ install และ run คงไม่ต้องพูดถึง เพราะค่อนข้างง่าย
เริ่มเล่าบรรยากาศที่ได้จากการใช้ดีกว่า
ผมเริ่มด้วยการใช้ module accounting/Chart of Accounts ก่อน
เรื่องแรกที่พยายามจะทำก็คือ การบันทึก Journal Entry
ดูจากหน้าตาที่ OFBiz ให้มาแล้ว ช่างเป็น minimum feature เสียนี่กระไร
ให้ป้อน entry ได้แค่แบบ 1 debit ต่อ 1 credit เท่านั้น



ทดลองใส่ข้อมูล ก็พบว่ามันเกิด error แดงเถือก



อ่าน message ดูแล้ว ก็พบว่ามันเป็น error ที่ระดับ database

violation of foreign key constraint 'ACCTTXENT_GLACOG' for key (121900,Company).

ตามไปดูใน model definition (applications/accounting/entitydef/entitymodel.xml)
ก็จะเห็นดังนี้
<relation type="one" fk-name="ACCTTXENT_GLACOG" rel-entity-name="GlAccountOrganization">
<key-map field-name="glAccountId"/>
<key-map field-name="organizationPartyId"/>
</relation>

จะเห็นว่ามัน link กับ entity GlAccountOrganization
ถ้าตามไปดูต่อ ก็จะเห็น definition ดังนี้
<entity entity-name="GlAccountOrganization"
package-name="org.ofbiz.accounting.ledger"
title="GL Account Organization Entity">
<field name="glAccountId" type="id-ne"></field>
<field name="organizationPartyId" type="id-ne"></field>
<field name="roleTypeId" type="id-ne"></field>
<field name="fromDate" type="date-time"></field>
<field name="thruDate" type="date-time"></field>
<field name="postedBalance" type="currency-amount"></field>
...

อ้าฮ้า มันคือ table ที่เก็บความสัมพันธ์ระหว่างหน่วยงาน กับ รหัสบัญชี
ลอง select ข้อมูลดูโดยใช้ webtool ก็พบว่า มันมีข้อมูลมาให้รายการเดียว

ดังนั้นเป้าหมายแรก ก็คือต้องเพิ่มรายการลงไป
อันนี้หาง่ายหน่อย อยู่ในกลุ่มงาน Chart of Account
ชื่อหัวข้องานว่า Assign GL Account



ส่วนหน้าจอ listing ก็อยู่ในข้อข้องานว่า List GL Organize



หลังจากใส่ข้อมูลแล้ว ก็ insert ข้อมูลผ่านฉลุย

ขั้นถัดไป ก็คือ อยากทดลอง add บัญชีใหม่ๆเข้าไป
ตรงนี้พบว่า ไม่ว่าจะ click หาหัวข้อใน menu ไหน ก็หาไม่เจอ
สุดท้ายก็ต้องตามไปแกะที่ applications/accounting/webapp/accounting/WEB-INF/controller.xml
พบว่ามีการ define uri ไว้ดังนี้
<request-map uri="AddGlAccount">
<security https="true" auth="true"/>
<response name="success" type="view" value="AddGlAccount"/>
</request-map>

ก็เลยลองเอาไปใส่ url ตรงๆดู
ได้ผลแฮะ ได้หน้าจอมาหนึ่งหน้าจอ




ทดลองป้อนข้อมูลดู กด save
ได้ error มาใหม่อีกหนึ่งตัว
คราวนี้มันบอกว่า
GlAccountId ไม่ได้ระบุ
อ้าว ก็ไม่มี field ให้ป้อนนี่น่า แล้วมันจะระบุที่ไหนหล่ะ

ตามไปดู view definition
<view-map name="AddGlAccount" type="screen" page="component://accounting/widget/AccountingScreens.xml#AddGlAccount"/>

เห็นว่าชี้ไปที่ applications/accounting/widget/AccountingScreens.xml#AddGlAccount
ตามไปดูก็จะเห็น define ไว้แบบนี้

<screen name="AddGlAccount">
<section>
<actions>
<set field="titleProperty" value="PageTitleAddGlAccount"/>
<set field="tabButtonItem" value=""/>
<set field="labelTitleProperty" value=""/>
</actions>

<widgets>
<decorator-screen name="main-decorator" location="${parameters.mainDecoratorLocation}">
<decorator-section name="body">
<include-form name="EditGlAccount" location="component://accounting/webapp/accounting/chartofaccounts/GlAccountForms.xml"/>

<!-- include-screen screen-name="ListGlAccounts" name="ListGlAccounts" / -->

<!-- tree name="GlAccountTree" location="component://accounting/widget/AccountingTrees.xml"/ -->
</decorator-section>
</decorator-screen>
</widgets>
</section>
</screen>

จะเห็นว่ามันเป็นแค่ decorator
ตัว form จริงๆอยู่ใน applications/accounting/webapp/accounting/chartofaccounts/GlAcountForms.xml
<form name="EditGlAccount" type="single" target="updateGlAccount" title="" default-map-name="glAccount">
<alt-target use-when="glAccount==null" target="createGlAccount"/>
<auto-fields-service service-name="updateGlAccount" map-name=""/>

<field use-when="glAccount!=null" name="glAccountId" tooltip="${uiLabelMap.AccountingNotModificationRecrationGlAccount}"><display/></field>
<field use-when="glAccount==null&amp;&amp;glAccountId!=null" name="glAccountId" tooltip="${uiLabelMap.AccountingCouldNotFindGlAccount} [${glAccountId}]"><text size="20" maxlength="20"/></field>
<!-- this to be taken care of with auto-fields-service as soon as it uses entity field info too -->
<field use-when="glAccount==null&amp;&amp;glAccountId==null" name="glAccountId"><text size="20" maxlength="20"/></field>

<field name="glAccountTypeId">
<drop-down allow-empty="false">
<entity-options entity-name="GlAccountType" description="${description}">
<entity-order-by field-name="description"/>
</entity-options>
</drop-down>
</field>
...

อืมม์มัน reuse ใช้ form ทั้งหน้าจอ insert และ update
จะเห็นว่ามันตั้งเงื่อนไขในการแสดง field glAccountId ไว้ดังนี้
1. กรณี glAccount ไม่เป็น null แสดงว่าเป็น mode update
ก็ให้แสดงค่า glAccountId โดยใช้ display tag และอนุญาติให้ดูได้อย่างเดียว ห้ามแก้ไข
2. กรณี glAccount เป็น null และมีหรือไม่มีค่า glAccountId
หน้าจอจะแสดง textfield ให้ใส่

อืมม์ case ของเรามันน่าจะตกข้อ 2, แต่มันทำไม่ไม่แสดง textfield ให้เราใส่
ผมก็เลยไปลองดูในหน้าจอ update ดูว่ามันทำงานอย่างไร
โดยดูใน applications/accounting/widget/AccountingScreens.xml#GlAccountNavigate
ปรากฎว่า flow ก่อนที่จะเข้าหน้าจอ update
มันทำแบบนี้
<screen name="GlAccountNavigate">
<section>
<actions>
...
<!-- parameters includes requestAttributes and parameter map -->
<!-- requestParameters is just the parameter map -->
<set field="glAccountId" from-field="requestParameters.glAccountId"/>
...
<entity-one entity-name="GlAccount" value-name="glAccount"/>
</actions>
<widgets>
...
</widgets>
</section>
</screen>

ลองกลับไปดู flow ในหน้าจอ AddGlAccount มันไม่ได้ set entity นี่หน่า
ทดลองใส่บรรทัดนี้เพิ่มลงไป
<screen name="AddGlAccount">
<section>
<actions>
<set field="titleProperty" value="PageTitleAddGlAccount"/>
<set field="tabButtonItem" value=""/>
<set field="labelTitleProperty" value=""/>
<entity-one entity-name="GlAccount" value-name="glAccount"/>

</actions>
...

ลอง restart OFBiz แล้วเปิด browser ดู
พบว่าได้ผลแฮะ มี textfield ให้ป้อนแล้ว
ทดลองป้อนแล้ว submit ดู ก็พบว่ามันบันทึกลง Database ได้แล้ว

น่าสนใจมาก ใช้ OFBiz ครั้งแรกก็เจอ bug เลย
แต่คิดว่าเป็น bug เนื่องจากเป็นหน้าจอนี้เป็นหน้าจอที่ไม่มีคนใช้ (สังเกตได้จากไม่มี menu ให้เลือก)

Related link from Roti

Monday, July 23, 2007

Max sum ของ Sub-array

zdk post โจทย์ข้อนี้ ใน codenone
เพื่อเป็นการฝึกฝน functional language ผมจึงลองทำด้วย haskell ดู
ทำแล้ว หน้าตาออกมาดูไม่ได้ ก็เลยไป search หาดูว่า บรรดาผู้เชี่ยวชาญ haskell
เข้าทำไว้อย่างไรบ้าง ซึ่งก็สมหวัง เพราะมีการ post ไว้ใน mailing list นี้ Link

ปัญหานี้ ถ้าคิดแบบง่ายสุดก่อน(ไม่คำนึงเรื่อง performance) ก็คือ
1. หา sublist ทั้งหมด
2. คำนวณแปะค่า sum เข้าไปไว้ที่หน้าสุดของ sublist แต่ละตัว
3. หา maximum sum
4. return ค่าที่ได้ โดยโยนค่า sum ทิ้งไป

เริ่มด้วยการหา sublist
ความต้องการคือ
sublist [1,2,3] ต้องได้ [[1],[1,2],[1,2,3],[2],[2,3],[3]] (ไม่จำเป็นต้องเรียงลำดับ)
ดู code ที่ผมเขียน เสียก่อน
sublists xs = sublists' xs [] 
sublists' [] acc = acc
sublists' xs acc = sublists' (tail xs) (tmp ++ acc)
where tmp = snd $ mapAccumL (\acc x -> (x:acc, x:acc)) [] xs

น่าเกลียดมาก เมื่อนำไปเทียบกับมือเก๋าๆแล้ว

เริ่มจากบรรทัดนี้ก่อน เป้าหมายบรรทัดนี้คือ ถ้ามี input เป็น [1,2,3] ต้องได้ [[1],[1,2],[1,2,3]] ออกมา
where tmp = snd $ mapAccumL (\acc x -> (x:acc, x:acc)) [] xs

สามารถใช้ List comprehension แทนได้ดังนี้
where tmp = [ys | n <- [1..length xs], ys <- [(take n xs)]]

ยังยังไม่พอ มี guru มาเฉลยอีกว่า ยาวไป ใช้แค่นี้พอ
where tmp = inits xs

อาฮ้า มี function นี้ให้ใช้ด้วย ตั้งชื่อไม่สื่ออย่างนี้ จะหาเจอได้อย่างไร

ขั้นถัดไปคือปรับตรงบรรทัดนี้
sublists xs = sublists' xs []
sublists' [] acc = acc
sublists' xs acc = sublists' (tail xs) (tmp ++ acc)

เยิ่นเย้อสุดๆ เหลือแค่นี้ก็พอ
sublists [] = []
sublists xs = tmp ++ sublists (tail xs)

อืมม์ได้เท่านี้ก็หรูแล้ว เจอแบบนี้เข้าไปสั้นกว่า
(function tails โผล่ออกมาอีกแล้ว มันมี function แบบนี้ด้วยวุ้ย
tails [1,2,3] = [[1,2,3],[2,3],[3],[]]
)
sublists xs = [zs | ys <- inits xs, zs <- tails ys]

เจ๋งแล้ว แต่มีสั้นกว่านี้อีก
sublists = concatMap tails . inits

แต่การใช้ inits,tails ก็มีปัญหาตรงที่ว่ามันมี empty array เข้ามาปนด้วย
มีคนเสนอ function unfoldr (เอาอีกแล้ว มี function unfoldr ด้วยหรือนี่)
ซึ่งดูดีกว่า ตรงที่ไม่มีปัญหาเรื่อง empty array
แถมยังเป็นการฉลาดที่นำ higher order function ที่มีอยู่แล้วมา apply ใช้
sublists xs = unfoldr f xs
where f [] = Nothing
f xs = Just ([ys | n <- [1..length xs], ys <- [(take n xs)]], tail xs)

หลังจากได้ sublist แล้ว ก็มีถึงขั้นการแปะ sum ลงไปข้างหน้า
ทำง่ายๆโดย
annotate_sum = map (\xs -> (sum xs, xs)) 

จากนั้นก็จัดการหา maximum โดยใช้ function maximumBy
ซึ่งผมเขียนแบบนี้
findMax xs = maximumBy (\a b -> compare (fst a) (fst b)) xs

ก็มีคนมาแสดงให้ดูว่า ทำแบบนี้ได้นะ
findMax xs = maximumBy (\(s1,_) (s2,_) -> compare s1 s2) xs

แล้วก็มีคนมาบอกอีกว่า มันมี function comparing ให้ใช้นะ
(แต่ผมหา function นี้ไม่เจอ คาดว่าคงอยู่ใน version ใหม่กว่าที่มี)
findMax xs = maximumBy (comparing fst)

เมื่อนำมาประกอบกัน ก็ได้ดังนี้
*Main> (snd . findMax . annotate_sum . sublists) [-1,2,5,-1,3,-2,1]
[2,5,-1,3]

Related link from Roti