Showing posts with label ofbiz. Show all posts
Showing posts with label ofbiz. Show all posts

Wednesday, June 11, 2008

merge อย่างไรดี

ปัญหาหนึ่งที่ Orangegears เจอก็คือ เนื่องจากเรา fork ออกมาจาก Ofbiz
แต่ก็ยังต้องการ sync กับ Ofbiz อย่างไกล้ชิด (ไม่เหมือน Opentap ที่แยกออกไปอย่างชัดเจน)
ทำให้เราต้องคอย merge code จาก Ofbiz เข้ามายัง Orangegears เป็นระยะๆ

วิธีการเดิมที่น้อง sand ใช้ ก็คือใช้ kdiff3 ทำการ merge
ซึ่งก็สะดวกดี เพราะมี UI สวยงาม
แต่ผมก็เจอปัญหาว่า changed ของผมมักจะหายไปบ่อยๆ
ผมก็เลยมองหาวิธีใหม่ๆมาเรื่อยๆ

วันก่อนหลังจากลองเล่น multiple branches ใน git ดู
ก็พบว่า git ยืดหยุ่นพอที่เราจะทำ multiple remote branch จาก svn repository มากกว่า 1 ที่ได้
ผมก็เลยทดลอง merge ด้วยวิธีนี้ดู

วิธีการก็คือ
เริ่มด้วยการสร้าง git repository ที่มี link ชี้ไปยัง project ofbiz ก่อน

git svn init http://svn.apache.org/repos/asf/ofbiz/trunk

หลังจากสั่งคำสั่งนี้ git จะสร้าง working directory เปล่าๆให้ (ยังไม่ fetch code มาให้)
โดย file .git/config จะมีหน้าตาแบบนี้

[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[svn-remote "svn"]
url = http://svn.apache.org/repos/asf/ofbiz/trunk
fetch = :refs/remotes/trunk

เราก็ทำการจัดแจงแก้ไขชื่อให้เหมาะสม จะได้ไม่งงภายหลัง
และทำการเพิ่ม repository ของ Ofbiz เข้าไป

[svn-remote "ofbiz"]
url = http://svn.apache.org/repos/asf/ofbiz/trunk
fetch = :refs/remotes/ofbiz_trunk
[svn-remote "orangegears"]
url = https://orangegears.svn.sourceforge.net/svnroot/orangegears
fetch = trunk:refs/remotes/og_trunk

จากนั้นก็ทำการ fetch ข้อมูลทีละ repository
เริ่มจาก ofbiz ก่อน เอา revision ล่าสุดเลย ไม่เอา history
Note1: เลข revision ของ ofbiz นี่ล่อไปหลักหกแสนแล้ว
ทั้งนี้เพราะ repo ของ apache เขาใช้ share ร่วมกันทุก project
Note2: ผมลอง fetch orangegears ก่อน ปรากฎว่า fetch ofbiz ไม่ได้ (ไม่มี error ด้วย)

git svn fetch -r 666189:HEAD ofbiz

ตามด้วย orangegears โดยเริ่มต้นที่ revision 142 เลย ไม่ต้องย้อนอดีตมากนัก

git svn fetch -r 142:HEAD orangegears

ลองสั่ง git branch -r จะเห็นว่ามี og_trunk กับ ofbiz_trunk เกิดขึ้น

pphetra@mypann:~/projects/java/t$ git branch -r
ofbiz_trunk
og_trunk

โดย default trunk ของ ofbiz จะถูก map เข้ามาเป็น master ใน local branch
ทำการเปลี่ยนชื่อให้เรียบร้อย จะได้ไม่งงว่าใครเป็นใคร

git branch -m master ofbiz

ส่วน og_trunk นั้นยังไม่มี local branch ต้องทำการสั่ง

git checkout -b og og_trunk


ขั้นตอนการ merge
fetch code จาก ofbiz ให้ up-to-date ก่อน

git checkout -f ofbiz
git svn rebase

fetch code ของ orangegears ให้ up-to-date เช่นเดียวกัน

git checkout -f og
git svn rebase

เตรียม branch สำหรับ merge โดยสร้าง branch แยกออกจาก og (ไม่จำเป็น แต่ก็ควรทำเป็นนิสัย)

git checkout -b merge-ofbiz

สั่ง merge

git merge ofbiz

ที่ยากก็คือเวลาเกิด conflict ก็ต้องตามแก้ไขให้เรียบร้อยก่อน
Note1: สำหรับผม ผมเลือกใช้ emacs + git-emacs mode ซึ่งมันจะเรียกใช้ ediff ในการ solve conflict
Note2: คิดว่าการ merge ครั้งแรก จะมี conflict เยอะหน่อย
แต่พอเป็นการ merge ครั้งที่ 2.. จะมี conflict น้อยลง เพราะว่า branch ทั้งสองมี history ที่เชื่อมกันแล้ว
(เป็นผลดีต่อ three-way merge)

switch กลับไปยัง og และทำการ merge

git checkout -f og
git merge merge-ofbiz

จากนั้นก็สั่ง commit กลับขึ้น svn repository

git svn dcommit

สุดท้ายก็ลบ temporary branch ทิ้ง

git branch -D merge-ofbiz


ไว้จะทดลองทำสักระยะหนึ่ง (โดยยังไม่ commit ผลลัพท์จากการ merge กลับขึ้น orangegears)
เพื่อดูว่ามันมีประสิทธิภาพแค่ไหนก่อน

Related link from Roti

Monday, June 02, 2008

Groovy in Ofbiz

น้องแซนแจ้งมาว่าขณะที่ merge source code ของ Ofbiz เข้า project Orangegears
พบว่ามี้ groovy code โผล่เข้ามาแล้วแล้ว
ว่าแล้วผมก็จัดแจง update source code ของ orangegears เสียหน่อย
แล้วก็สั่ง grep -ilR groovy ดู
ก็พบว่าเริ่มมีการ replace screen action script จากของเดิมที่เขียนด้วย bsh ไปเป็น groovy บ้างแล้ว
แล้วก็พบว่ามีการเตรียมการใช้ groovy ใน service layer อีกด้วย (แต่ยังไม่ได้มีการ implement)

ลองไล่เปรียบเทียบ syntax ของ bsh กับ groovy ดูว่า ช่วยลดรูปอะไรได้บ้าง

ใน ofbiz เวลา pass parameters มักจะใช้ Map ในการ pass arguments
พอเปลี่ยนเป็น groovy แล้วการสร้าง Map ก็เลยกระทัดรัดขึ้น
// bsh
payment = delegator.findByPrimaryKey("Payment",
UtilMisc.toMap("paymentId", paymentId)));

# groovy
payment = delegator.findByPrimaryKey("Payment", [paymentId : paymentId]);


แน่นอนพวกการ iterate collections นี่ได้ประโยชน์ไปเต็มๆ
// bsh
oibIter = orderItemBillings.iterator();
while (oibIter.hasNext()) {
orderIb = oibIter.next();
orders.add(orderIb.getString("orderId"));
}

# groovy
orderItemBillings.each { orderIb ->
orders.add(orderIb.orderId);
}


การอ้างถึง value ใน Map
ด้วย syntax sugar ของ Groovy ก็เลย สะอาดสะอ้านขึ้นแบบนี้
// bsh
context.put("decimals", decimals);
context.put("rounding", rounding);

# groovy
context.decimals = decimals;
context.rounding = rounding;


การ check empty หรือ null collections ก็สบายตาขึ้น
// bsh
if (glAccounts != null && glAccounts.size() > 0) {
glAccount = glAccounts.get(0);

# groovy
if (glAccounts) {
glAccount = glAccounts[0];

Related link from Roti

Friday, September 14, 2007

Ofbiz Screen

ถ้าใครลองเปิด source code ของ ofbiz ดู
จะเห็นว่า หน้าจอของ ofbiz ส่วนใหญ่ render โดยใช้ screen component

ลองดูตัวอย่าง definition ของ screen สักอัน
<screen name="NewInvoice">
<section>
<actions>
<set field="title" value="New Invoice"/>
<set field="titleProperty" value="PageTitleEditInvoice"/>
<entity-one entity-name="Invoice" value-name="invoice"/>
</actions>
<widgets>
<decorator-screen name="CommonInvoiceDecorator" location="${parameters.mainDecoratorLocation}">
<decorator-section name="body">
<section>
<widgets>
<label style="head1" text="${uiLabelMap.AccountingCreateNewSalesInvoice}"></label>
<include-form name="NewSalesInvoice" location="component://accounting/webapp/accounting/invoice/InvoiceForms.xml"/>
<label style="head1" text="${uiLabelMap.AccountingCreateNewPurchaseInvoice}"/>
<include-form name="NewPurchaseInvoice" location="component://accounting/webapp/accounting/invoice/InvoiceForms.xml"/>
</widgets>
</section>
</decorator-section>
</decorator-screen>
</widgets>
</section>
</screen>


ลองมาดูว่า feature ของ screen นั้นมีอะไรบ้าง
เริ่มที่ child element ของ screen ก่อน กำหนดไว้ว่าต้องเป็น <section> เท่านั้น

<screen>
<section>
...
</section>
</screen>


ภายใน section สามารถมี element ได้ 4 แบบ
<screen>
<section>
<condition>...</condition>
<actions>...</actions>
<widgets>...</widgets>
<fail-widgets>...</fail-widgets>
</section>
</screen>

ตัว condition ก็คือ expression ที่จะถูก evaluate เมื่อ screen เริ่มทำงาน
ถ้าได้ผลลัพท์เป็น true ก็จะ
ทำการเรียกใช้ actions และ render output โดยใช้ block widgets
แต่ถ้าได้ผลลัพท์เป็น false ก็จะ
render ด้วย block fail-widgets แทน

Note: ตัว condition, action, และ fail-widgets ถือว่าเป็น optional element
จะมีหรือไม่มีก็ได้

condition block ส่วนใหญ่จะไว้ใช้ check พวก authorize เช่น
<condition>
<or>
<if-has-permission permission="ORDERMGR" action="_VIEW"/>
</or>
</condition>

ส่วนภายใน action block, มีคำสั่งให้ใช้อีก 9 คำสั่ง
ซึ่งขอยกรายละเอียดไปพูดใน post หน้า

ภายใน widgets หรือ fail-widgets เราสามารถมี element ได้ดังนี้
  • section
    Note: จะเห็นว่าใน widgets ก็สามารถมี section ซ้อนอยู่ข้างในได้อีก
  • container
    container ก็คือ wrapper ที่ไว้จัดกลุ่ม widget
    การทำงานภายในของมัน ก็คือเวลามัน render html มันจะ render
    <div> block คร่อม widget ที่อยู่ข้างในมัน
  • include-screen
    อันนี้ตรงไปตรงมา ก็คือ include screen อื่นๆเข้ามา
  • decorator-screen, decorator-section-include
    อันนี้ถือเป็นหัวใจของการใช้ screen
    ถ้าเราสังเกตดูหน้าจอของ ofbiz เวลาใช้งาน
    จะเห็นว่าเวลาเราเลือก action ต่างๆ หน้าจอส่วนใหญ่จะไม่เปลี่ยนแปลง
    ส่วนที่เปลี่ยน จะเป็นแค่ region เล็กๆเท่านั้น
    ofbiz ก็เลยนำ decorator pattern มาใช้สำหรับ render code ที่ซ้ำๆกัน
    วิธีใช้ก็คือ
    <screen>
    <section>
    <widgets>
    <decorator-screen name="CommonFixedAssetDecorator" location="${parameters.mainDecoratorLocation}">
    <decorator-section name="body">
    ... widget go here.
    </decorator-section>
    </decorator-screen>
    </widgets>
    </section>
    </screen>

    <screen name="CommonFixedAssetDecorator">
    <section>
    <widgets>
    <decorator-screen name="main-decorator" location="${parameters.mainDecoratorLocation}">
    <decorator-section name="body">
    ....
    <decorator-section-include name="body"/>
    </decorator-section>
    </decorator-screen>
    </widgets>
    </section>
    </screen>

    Note: ใน ofbiz เรามักจะเห็น decorator ซ้อนไปซ้อนมาจนน่าปวดหัว
  • label
    render string ธรรมดา
    ซึ่งกรณีที่ render ออก html ก็จะมี ครอบให้ด้วย
  • include-form, include-menu, include-tree, content, sub-content, link, image, iterate-section
    อันนี้เป็นเรื่องใหญ่อีกเรื่อง ที่จะยังไม่พูดถึง
  • platform-specific
    อันนี้พบบ่อยมาก
    วิธีใช้ก็ fix ตายตัว นั่นคือใช้เรียก html-template มาทำงาน
    โดย support เฉพาะ Freemarker เท่านั้น
    <platform-specific>
    <html>
    <html-template location="component://accounting/webapp/accounting/invoice/sendPerEmail.ftl"/>
    </html>
    </platform-specific>

Related link from Roti

Thursday, August 30, 2007

ทดสอบต่อ RMI จาก Eclipse RCP ไป OFBiz

ประเด็นที่น่าสนใจ ก็คือ
  1. ปัญหาเรื่องการ pack Ofbiz library เฉพาะที่ client ต้องใช้ ให้เป็น Eclipse Plugin
  2. ปัญหาเรื่อง class loading


เริ่มที่อันแรกสุดก่อน
อันนี้ทำได้ง่ายๆ โดยการใช้ new wizard ของ Eclipse
ที่ชื่อ "Plugin from existing JAR Archives"
แต่ยังมีประเด็นที่ตาม ก็คือ library พวกนี้ ณ ขณะ runtime
มันมีการอ่าน properties file หรือ configuration file ด้วย
ดังนั้น เราเลยต้องสร้าง folder เพิ่มใน plugin
และกำหนดใน manifest file ให้มันเป็น classpath
จากนั้นก็ copy properties file ที่จำเป็นจาก ofbiz/framework/base/config
ซึ่งประกอบด้วย log4j.xml, jsse.properties, cache.properties

log4j.xml ถูกเรียกใช้จาก org.ofbiz.base.util.Debug lineno:86
ภายใน log4j.xml มี file appender อยู่จำนวนหนึ่งที่ระบุ file เป็น relative path อยู่
เนื่องจากตอนนี้ยังไม่อยากแตะประเด็นนี้ ก็เลยใช้วิธีแก้เปลี่ยนเป็น static path ไปไว้สักที่หนีงก่อน

jsse.properties ถูกเรียกใช้โดย org.ofbiz.base.util.SSLUtil lineno:249
เพื่อใช้หา configuration ของพวก proxyHost, proxyPort
ตัวต้นฉบับ ข้างในมีเนื้อหาที่ใช้จากฝั่ง server ด้วย
ซึ่ง client ไม่ต้องใช้, สามารถลบทิ้งทั้งหมดได้เลย

cache.properties ถูกเรียกใช้จาก org.ofbiz.base.util.cache.UtilCache lineno:217
ภายในมีการกำหนด relative path ของ disk cache file ด้วย
ก็ให้เปลี่ยนเป็น fix path ไปก่อน
(ตรงนี้ยังไม่ได้ดู แต่เข้าใจว่าสามารถลบทิ้งได้เยอะเหมือนกัน)

หน้าตา MANIFEST.MF file ของ plugin จะได้เป็นแบบนี้
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Rmiclient Plug-in
Bundle-SymbolicName: org.ofbiz.rmiclient
Bundle-Version: 1.0.0
Bundle-ClassPath: ofbiz-base.jar,
javolution-4.2.8.jar,
wsdl4j.jar,
ofbiz-service.jar,
jdbm-1.0.jar,
log4j-1.2.14.jar,
config/
Bundle-Vendor: com.orangegears
Bundle-Localization: plugin
Export-Package: .,
com.ibm.wsdl,
com.ibm.wsdl.extensions,
com.ibm.wsdl.extensions.http,
com.ibm.wsdl.extensions.mime,
...


จากตรงนี้ไป เราก็มี Ofbiz library ให้พร้อมเขียน RMI call แล้ว
ปัญหาถัดไปที่ตามมาก็คือ
เวลา run code นี้
RemoteDispatcher dispatcher = (RemoteDispatcher) Naming.lookup(connectionDetail.getServerUrl());
Map result = dispatcher.runSync("userLogin", UtilMisc.toMap("login.username",
connectionDetail.getUserId(),
"login.password",
connectionDetail.getPassword()));

มันจะเกิด error ดังนี้
java.rmi.UnmarshalException: error unmarshalling return; nested exception is: 
java.lang.ClassNotFoundException: org.ofbiz.entity.GenericValue
(no security manager: RMI class loader disabled)

เป็นประเด็นเรื่อง SecurityManager
แก้แบบ quick fix ด้วยการ implment Custom SecurityManager ลงไปก่อน
public class MySecurityManager extends SecurityManager {


public void checkPermission(Permission perm, Object context) {
}

public void checkPermission(Permission perm) {
}

}

จากนั้นก็แก้ code ข้างบนให้เป็นแบบนี้
System.setSecurityManager(new MySecurityManager());
RemoteDispatcher dispatcher = (RemoteDispatcher) Naming.lookup(connectionDetail.getServerUrl());
Map result = dispatcher.runSync(...);

Exception ตัวเก่า ก็จะหายไป ได้ exception ตัวใหม่มาแทน
java.lang.ExceptionInInitializerError
at org.ofbiz.base.util.UtilURL.fromOfbizHomePath(UtilURL.java:110)
at org.ofbiz.base.util.UtilURL.fromResource(UtilURL.java:76)
at org.ofbiz.base.util.UtilURL.fromResource(UtilURL.java:44)
at org.ofbiz.base.util.UtilProperties.getPropertyValue(UtilProperties.java:130)
at org.ofbiz.base.util.UtilProperties.getPropertyValue(UtilProperties.java:100)
at org.ofbiz.base.util.SSLUtil.loadJsseProperties(SSLUtil.java:249)
at org.ofbiz.base.util.SSLUtil.loadJsseProperties(SSLUtil.java:244)
at org.ofbiz.base.util.SSLUtil.(SSLUtil.java:52)
at org.ofbiz.service.rmi.socket.ssl.SSLClientSocketFactory.createSocket(SSLClientSocketFactory.java:42)

ถ้าลองเปิด code org.ofbiz.base.util.UtilURL ดูบริเวณ lineno 60-67 ซึ่งมีหน้าตาเป็นแบบนี้
        if (loader == null && url == null) {
try {
loader = Thread.currentThread().getContextClassLoader();
} catch (SecurityException e) {
UtilURL utilURL = new UtilURL();
loader = utilURL.getClass().getClassLoader();
}
}

ปัญหาจะอยู่ตรง Thread.currentThread().getContextClassLoader()
(ถือเป็นปํญหา classic ของคนเขียน Eclipse RCP ที่พยายามใช้ library ที่เคย work บน web app มาก่อน)
ตัว currentThread ก็คือ thread ที่เป็นตัว start eclipse
ซึ่งแน่นอนว่า classpath มัน ไม่รวมถึง classpath ของ plugin ด้วย

ทางแก้แบบ quick fix ที่ได้รับความนิยมสูง ก็คือ
ห่อ code ที่จะ call ofbiz ด้วยอันนี้เสีย
Thread thread = Thread.currentThread();
ClassLoader loader = thread.getContextClassLoader();
thread.setContextClassLoader(this.getClass().getClassLoader());
try {

System.setSecurityManager(new MySecurityManager());
RemoteDispatcher dispatcher = (RemoteDispatcher) Naming.lookup(connectionDetail.getServerUrl());
Map result = dispatcher.runSync("userLogin", UtilMisc.toMap("login.username",
connectionDetail.getUserId(),
"login.password",
connectionDetail.getPassword()));

} finally {
thread.setContextClassLoader(loader);
}


ปิดคดี

Related link from Roti

Tuesday, August 21, 2007

ขั้นตอนการ start ของ Ofbiz

ลองแกะขั้นตอนการ start ของ Ofbiz ดู ก็พบขั้นตอนที่น่าสนใจดังนี้

1. เหมือนกับ framework ใหญ่ๆทั่วๆไป นั่นคือ
ถ้าต้อง start อะไรที่ซับซ้อนนัก ก็ควรมี bootstrapping code
โดย class ที่เป็นจุดเริ่มต้นทำงานของ Ofbiz ก็คือ org.ofbiz.base.start.Start.java
เมื่อ initialize ค่าเบื้องต้นต่างๆหมดแล้ว (เช่น log directory, base class path)
ก็จะทำการ load configuration ตาม command parameter ที่ user สั่งผ่าน command-line มา
เช่น ผมสั่ง
java -jar ofbiz.jar -pos
มันก็จะไปใช้ configuration file ที่ชื่อ /framework/base/src/start/org/ofbiz/base/start/pos.properties
ถ้าสั่ง
java -jar ofbiz.jar
มันก็จะใช้ default configuration file ซึ่งก็คือ start.properties

element ที่สำคัยภายใน properties file ข้างบน ก็คือ ofbiz.start.loader* (* คือเลขลำดับในการ load)
ตัว loader ที่สำคัญของ Ofbiz ก็คือ org.ofbiz.base.container.ContainerLoader
ซึ่งเมื่อมัน load ขึ้นมา มันจะใช้ค่า ofbiz.container.config ที่ระบุใน configuration file
ในการ load container ขึ้นมาทำงาน

2. ofbiz มองว่า การ start ครั้งหนึ่งๆ เราสามารถกำหนด container ที่เราต้องการได้หลายตัว
โดยต้องสร้าง xml file ที่ระบุ container ที่ต้องการ
และระบุชื่อ file ไว้ใน configuration ผ่าน property ที่ชื่อ ofbiz.container.config

อย่างในกรณีที่เราจะ start โปรแกรม point of sell
ค่า ofbiz.container.config ก็จะกำหนดแบบนี้
ofbiz.container.config=framework/base/config/pos-containers.xml

ofbiz ก็จะใช้ file pos-containers.xml ในการเลือก start container ที่ต้องการ

3. Container ที่สำคัญๆ และขาดไม่ได้ ของ Ofbiz ก็คือ
3.1 component-container ทำหน้าที่ load component ต่างๆ
ทั้งที่เป็น framework component และ application component
โดย default มันจะใช้ file framework/base/config/component-load.xml เป็นจุดตั้งต้นในการ load
ซึ่งภายในจะ recursive load module ของ framework, applications, specialpurpose, hot-deploy
ถัดจากตรงนี้ไป ยังมีเรื่องให้ตามอีกยาว ดังนั้นจะพักไว้แค่ตรงนี้ก่อน

3.2 classloader-container
ตรงนี้ยังแกะไม่เข้าใจ
โดย ofbiz จะใช้ classloader ที่ชื่อ CacheClassLoader.java

จากที่ตามดูขั้นตอนการ load Point of Sell
พบว่า ofbiz มัน load ทุก component, ทุก applications
ดูเหมือนจะ load เยอะกว่าที่เราต้องการ

ประเด็นที่น่าสนใจ กรณีจะทำเป็น Eclipse RCP
1. จะ pack มันเป็น bundle(plugin) อย่างไร
การ pack เป็น bundle จะส่งผลต่อ code แค่ไหน
ใช้ wrapping อย่างเดียว เพียงพอหรือไม่
ปัญหาที่ตามมา ก็คือเรื่อง class loader
เนื่องจาก Eclipse มี model class loader ที่ค่อนข้าง strict

2. ถ้าไม่ใช้ Eclipse RCP แต่เปลี่ยนไปใช้แค่ SWT+JFace
น่าจะช่วยให้หลีกเลี่ยงปัญหาก้อนใหญ่ไปได้

Related link from Roti

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