Developing Plug-Ins - Models, Documents and custom accounting

From iDempiere en
Jump to navigation Jump to search



WARNING: THIS PAGE NEEDS YOU! IF YOU SEE ANY ERRORS, PLEASE CORRECT THEM. IF YOU THINK SOMETHING SHOULD BE DESCRIBED IN ANOTHER WAY, PLEASE CORRECT IT! IF YOU HAVE ADDITIONAL INFORMATION WHICH COULD BE USEFUL, PLEASE ADD IT!


This tutorial is brought to you by Jan Thielemann from evenos GmbH (www.evenos-consulting.de). If you have questions, criticism or improvement suggestions, feel free to visit me (Jan.thielemann) or write me an email.

If you think that something is missing here, feel free to modify this page and add your name here. Other contributors and people who helped me:

  • Thomas Bayen
  • Nicols Micoud
  • Redhuan D. Oon
  • Carlos Ruiz

Goal of this tutorial

In this tutorial, I want to show you how you can implement custom document types to make your own tables act like documents. I also explain the steps to provide the necessary code so that your document can make custom accounting entries. However, I am still new to the documents topic in iDempiere so I don't claim that this tutorial is complete, nor that everything I mention is 100% correct. I dare you to correct me if I am wrong!

Prerequisites

Before you start this tutorial, you should take a look at the following tutorials:


The code used in this tutorial is available here: https://bitbucket.org/evenos-consulting/un.kong.jim.battleplan

tl;dr

https://www.youtube.com/watch?v=1H7NaSY5G5k

The Workflow

Statement of the problem

Before we start writing code or other stuff, let's find an example for this tutorial. I would like to give you a concrete case where we need a new custom document type and show you how to implement it step by step. So what could be a use case where we need a document?

Let's imagine that the dictator of East-West Rokea, Jim Kong Un, wants to use iDempiere for his battle plans and to keep track of the expenses in the upcoming war against his mortal enemy, the United States of Majaica. He needs a way to let his generals create battle plans and he wants to be the only person who can approve the plans, because he is the mighty leader.

Battle plans can have war machinery attached, like order lines on a sales order, so once a battle plan is approved, accounting entries should be created for Jim to know how much money he burns in the war. Note that his generals are good in creating battle plans but only the army commanders know how much war machinery is actually available on the ground.

So we have three type of people involved in creating battle plans:

  • Generals create battle plans
  • Commanders attach war machinery to the battle plans
  • Jim Kong Un approves battle plans

Generals and Commanders could be implemented by iDempiere Roles, but to ensure that only generals create battle plans and commanders attach war machinery to them we will require an iDempiere document and a custom workflow to manage the battle plans.

Okay, so on a fresh system, we need to create users, roles, tables, windows, a workflow, a document and also we need to write some code. Let's start.

The Battle Plan

So what is a battle plan and how could we represent it in a database? Let's think about it. What do we need? We need:

  • The location where to attack
  • The battalion which should attack
  • The time when the attack should start
  • The description of the strategy
  • The name of the battle plan
  • The person who is in charge for this plan (so he can be executed if the plan fails)

Also we need a way to represent the war machinery. For this, we create a simple table which only has a name, value and description field so we can represent a battalion. Another table is the war machinery with the following fields:

  • Name/Value/Description of the machinery
  • Price for this machine
  • Flag indicating that this machine is in repair (we cannot send broken tanks into the battle)

And last but not least, we need to attach the machinery to the battle plans in a table like order lines with a simple reference to the machinery table. That should be all for the beginning.

Create the database tables

Create the following tables and please note that in this tutorial, we don't care about database constraints, table keys, grammar or orthography. It's just an example ;)

Battalion

CREATE TABLE ROK_battalion
(
--Default Columns
  rok_battalion_ID numeric(10,0) NOT NULL,
  rok_battalion_uu character varying(36) DEFAULT NULL::character varying,
  ad_client_id numeric(10,0) NOT NULL,
  ad_org_id numeric(10,0) NOT NULL,
  isactive character(1) NOT NULL DEFAULT 'Y'::bpchar,
  created timestamp without time zone NOT NULL DEFAULT now(),
  createdby numeric(10,0) NOT NULL,
  updated timestamp without time zone NOT NULL DEFAULT now(),
  updatedby numeric(10,0) NOT NULL,

--User Columns
  value character varying(40) NOT NULL,
  name character varying(255) NOT NULL,
  description character varying(255)
)
WITH (
  OIDS=FALSE
);
ALTER TABLE rok_battalion
  OWNER TO adempiere;

War Machines

CREATE TABLE ROK_war_machine
(
--Default Columns
  rok_war_machine_ID numeric(10,0) NOT NULL,
  rok_war_machine_uu character varying(36) DEFAULT NULL::character varying,
  ad_client_id numeric(10,0) NOT NULL,
  ad_org_id numeric(10,0) NOT NULL,
  isactive character(1) NOT NULL DEFAULT 'Y'::bpchar,
  created timestamp without time zone NOT NULL DEFAULT now(),
  createdby numeric(10,0) NOT NULL,
  updated timestamp without time zone NOT NULL DEFAULT now(),
  updatedby numeric(10,0) NOT NULL,

--User Columns
  value character varying(40) NOT NULL,
  name character varying(255) NOT NULL,
  description character varying(255),
  price numeric NOT NULL DEFAULT 0,
  isrepairing character(1) NOT NULL DEFAULT 'N'::bpchar
)
WITH (
  OIDS=FALSE
);
ALTER TABLE rok_war_machine
  OWNER TO adempiere;

Battle Plan

CREATE TABLE ROK_battle_plan
(
--Default columns
  rok_battle_plan_ID numeric(10,0) NOT NULL,
  rok_battle_plan_uu character varying(36) DEFAULT NULL::character varying,
  ad_client_ID numeric(10,0) NOT NULL,
  ad_org_ID numeric(10,0) NOT NULL,
  isactive character(1) NOT NULL DEFAULT 'Y'::bpchar,
  created timestamp without time zone NOT NULL DEFAULT now(),
  createdby numeric(10,0) NOT NULL,
  updated timestamp without time zone NOT NULL DEFAULT now(),
  updatedby numeric(10,0) NOT NULL,

--Document columns
  documentno character varying(30) NOT NULL,
  docstatus character(2) NOT NULL,
  docaction character(2) NOT NULL,
  processing character(1),
  processed character(1) NOT NULL DEFAULT 'N'::bpchar,
  c_doctype_id numeric(10,0) NOT NULL,
  c_doctypetarget_id numeric(10,0) NOT NULL,
  isapproved character(1) NOT NULL DEFAULT 'Y'::bpchar,

--Document columns to enable accounting
  dateacct timestamp without time zone NOT NULL,
  processedon numeric,
  posted character(1) NOT NULL DEFAULT 'N'::bpchar,
  c_currency_id numeric(10,0) NOT NULL,

--User columns
  value character varying(40) NOT NULL,
  name character varying(255) NOT NULL,
  description character varying(255),
  ad_user_id numeric(10,0), --in charge
  rok_battalion_ID numeric(10,0),
  attack_location character varying(255) NOT NULL,
  attack_time timestamp without time zone NOT NULL
)
WITH (
  OIDS=FALSE
);
ALTER TABLE ROK_battle_plan
  OWNER TO adempiere;

Battle Plan Line

CREATE TABLE rok_battle_plan_line
(
--Default Columns
  rok_battle_plan_line_ID numeric(10,0) NOT NULL,
  rok_battle_plan_line_uu character varying(36) DEFAULT NULL::character varying,
  ad_client_id numeric(10,0) NOT NULL,
  ad_org_id numeric(10,0) NOT NULL,
  isactive character(1) NOT NULL DEFAULT 'Y'::bpchar,
  created timestamp without time zone NOT NULL DEFAULT now(),
  createdby numeric(10,0) NOT NULL,
  updated timestamp without time zone NOT NULL DEFAULT now(),
  updatedby numeric(10,0) NOT NULL,
  rok_battle_plan_id numeric(10,0) NOT NULL,

--User Columns
  rok_war_machine_ID numeric(10,0) NOT NULL
)
WITH (
  OIDS=FALSE
);
ALTER TABLE rok_battle_plan_line
  OWNER TO adempiere;

Create tables, columns, windows, tabs and fields

Now that we have our database ready, we can start creating windows for our tables. You should be able to do this by your own. You are not? Okay, okay! Sorry. Just take a look here: https://www.youtube.com/watch?v=F1lFHS2riG4&list=UUbBGUUNnLy4kIHxq19DNPbA

So the general concept is:

  1. Create a database table
  2. Login as SuperUser on System Administrator role and System client
  3. Open Table and Columns window and create your table and columns
  4. Open Window, Tab and Field window and create your window


After creating the Tables in iDempiere, you may need to change the following columns:

ROK_Battle_Plan

  • Make C_DocType_ID not updatable and set default logic to "0"
  • Change C_DocTypeTarget_ID reference to "Table" and set the reference key to "C_DocType". Also create a new dynamic validation and set it to here. The validation sql should look like "C_DocType.DocBaseType ='ROK'"
  • Make DocumentNo not updateable and part of the identifier
  • Change DocAction reference to button, change reference key to "_Document Action" and create a new process "Process Battle Plan" and attach it to the button. The Process only need a name and a search key for now. I chose "Process Battle Plan" for both. Also change the default logic to "CO"
  • For DocStatus, set the reference key to "_Document Status" and set the default logic to "DR"
  • Make Processed not updateable
  • Change length of ProcessedOn to 20
  • Change reference of Processing to Button and use Process Battle Plan as the process
  • Make isApproved not updateable and set default logic to "@IsApproved@"
  • Change reference of DateAcct to Date, length to 7 and use "@#Date@" as default logic
  • Change reference of Posted to Button, set reference key to "_Posted Status" and default logic to "N"
  • Set default logic of C_Currency_ID to "@C_Currency_ID@" and make it not updateable

ROK_Battle_Plan_Line

  • Set default logic of ROK_Battle_Plan_ID to "@ROK_Battle_Plan_ID@", make it not updateable and check the Parent link column checkbox


Now create the corresponding windows. After you created them, you may need to change the following fields:

ROK_Battle_Plan window

  • Hide Account Date
  • Hide Approved
  • Hide Process Now
  • Hide Processed
  • Hide Processed On
  • Set display logic of Posted to "@Processed@=Y & @#ShowAcct@=Y"
  • Make currency read only
  • Make Document Type read only
  • Make Document Status read only

Finally create the menu entries for our new windows.

Create a custom document type in the Application Dictionary

Now that we have our windows ready, we can start creating our new document type. To do this, first open the Reference window and search the "C_DocType DocBaseType" reference. Go to the List Validation tab and add a new entry:

  • Value=ROK
  • Name=Battle Plan

With this new base type, we can create a new document type. Open the Document Type window and create a new entry:

  • Name=Battle Plan
  • Print Text=Battle Plan
  • CL Category=None
  • Document BaseType=Battle Plan

Please note that if you want to use this document on a specific client, you have to create it on this client!

IModelFactory and IDocFactory

The Application Dictionary is now ready. Now we can implement some code. Follow the other tutorials on this wiki or take a look at evenos Consultings youtube videos. What you basically do is:

  • Create a new Plug-in
  • Create a ModelFactory class and implement IModelFactory
  • Create a DocumentFactory class and implement IDocFactory
  • Create component definitions for Model- and DocFactory (note that DocFactory has an additional parameter called "gaap" whose value is "*"
  • Generate the X and I classes via model.generator app in eclipse
  • Create M classes for your models
  • Create a new document class called "Doc_ROKBattlePlan" which extends "Doc"
  • Implement DocAction and DocOption in MROKBattlePlan

Don't forget to add the dependencies to your MANIFEST.MF


After you created the basic shell for all classes, let's get into the implementation details. Open MROKBattlePlan and make some changes to the default generated methods:

@Override
public int customizeValidActions(String docStatus, Object processing, String orderType, String isSOTrx, int AD_Table_ID,
		String[] docAction, String[] options, int index) {
	if (options == null)
		throw new IllegalArgumentException("Option array parameter is null");
	if (docAction == null)
		throw new IllegalArgumentException("Doc action array parameter is null");

	// If a document is drafted or invalid, the users are able to complete, prepare or void
	if (docStatus.equals(DocumentEngine.STATUS_Drafted) || docStatus.equals(DocumentEngine.STATUS_Invalid)) {
		options[index++] = DocumentEngine.ACTION_Complete;
		options[index++] = DocumentEngine.ACTION_Prepare;
		options[index++] = DocumentEngine.ACTION_Void;

		// If the document is already completed, we also want to be able to reactivate or void it instead of only closing it
	} else if (docStatus.equals(DocumentEngine.STATUS_Completed)) {
		options[index++] = DocumentEngine.ACTION_Void;
		options[index++] = DocumentEngine.ACTION_ReActivate;
	}

	return index;
}

@Override
public boolean processIt(String action) throws Exception {
	log.warning("Processing Action=" + action + " - DocStatus=" + getDocStatus() + " - DocAction=" + getDocAction());
	DocumentEngine engine = new DocumentEngine(this, getDocStatus());
	return engine.processIt(action, getDocAction());
}

@Override
public boolean unlockIt() {
	return true;
}

@Override
public boolean invalidateIt() {
	return true;
}

@Override
public String prepareIt() {
	setC_DocType_ID(getC_DocTypeTarget_ID());
	return DocAction.STATUS_InProgress;
}

@Override
public boolean approveIt() {
	return true;
}

@Override
public boolean rejectIt() {
	return true;
}

@Override
public String completeIt() {
	setProcessed(true);
	return DocAction.STATUS_Completed;
}

@Override
public boolean voidIt() {
	return true;
}

@Override
public boolean closeIt() {
	return true;
}

@Override
public boolean reverseCorrectIt() {
	return true;
}

@Override
public boolean reverseAccrualIt() {
	return true;
}

@Override
public boolean reActivateIt() {
	return true;
}

@Override
public String getSummary() {
	return null;
}

@Override
public String getDocumentInfo() {
	return null;
}

@Override
public File createPDF() {
	return null;
}

@Override
public String getProcessMsg() {
	return null;
}

@Override
public int getDoc_User_ID() {
	return 0;
}

@Override
public BigDecimal getApprovalAmt() {
	return BigDecimal.ONE;
}

As you see, we don't do much business logic here. Thats not necessary for our case but if you create documents for your own, you may want to add some more code here ;) Take a look at other document model classes to see what you could do. Just search for classes which implement DocAction. A example would be MOrder, MInvoice and so on.

Also heres how the Doc_ROKBattlePlan looks like:

public class Doc_ROKBattlePlan extends Doc{

	public Doc_ROKBattlePlan (MAcctSchema as, ResultSet rs, String trxName)
	{
		super (as, MROKBattlePlan.class, rs, null, trxName);
	}

	@Override
	protected String loadDocumentDetails() {
		return null;
	}

	@Override
	public BigDecimal getBalance() {
		return BigDecimal.ZERO;
	}

	@Override
	public ArrayList<Fact> createFacts(MAcctSchema as) {
		ArrayList<Fact> facts = new ArrayList<Fact>();
		return facts;
	}
}



For testing purposes i added a "log.warning("---------------")" to every method in my model as well as my Doc_ class.

Create Users and Roles

Since we want to play a little bit, we need differen users and roles. I created the following users and roles:

  • User: Jim
  • User: GeneralA
  • User: CommanderA
  • Role: Generals
  • Role: Commanders

The assignment of the users to the roles should be clear i guess :P. By the way, all roles are created on the target client. I use GardenWorld for this purpose. Also make sure that all users are added to the Business Partners GardenAdmin BP or GardenUser BP or you won't find them later in the Workflow Responsible window.

Create the workflow

The workflow is an essential part when working with documents. The workflow describes the steps needed to make a document transition into other states. Heres an example: At first, the document has the status drafted. The Genreals enter some information and complete the document. The doc now has the status in progress because it needs approval by Jim. Once Jim approved the document, it gets the status approved. From this status it can transition into completed. Which steps exactly are necessarry are described by the workflow.

Workflow

So let's start by creating a new workflow on System client. I call mine ROK_Battle_Plan, set access level to all and author to Jim Kong Un, our mighty leader. The Workflow Type is Document Process because we want to process a document. The Table is ROK_Battle_Plan and the Publication Status is Released. Then we open the report and process window and search the process we created earlier (Process Battle Plan). Here i chose my newly created workflow and save the process. This all happend on the System Client.


Responsibles

Next we have to create some Workflow Responsibles. Go to the GardenWorld client and create some responsibles there.

Name Type Others
Jim Human User=Jim
Generals Role Role=Generals
Commanders Role Role=Commanders

Nodes

Now to the more tricky part. We need to create Workflow Nodes and transitions but our responsibles are in GardenWorld and not on System client so we have to mix it a little bit:

Name/Search Key Action Others Workflow Responsible Client
(Start) Wait (Sleep) - - System
(DocPrepare) Document Action Document Action = Prepare - System
(DocComplete) Document Action Document Action = Complete - System
(DocAuto) Document Action Document Action = <None> - System
(DocApprove) User Choice Column = isApproved Jim GardenWorld
(AddWarMachine) User Window Window = ROK_Battle_Plan Commanders GardenWorld

When you are done, make sure that (Start) is set as the starting node in the Workflow window.

Transitions

The next step is to create the transitions. You need the following transitions:

Start Node Destination Node LineNo
(Start) (DocPrepare) 10
(Start) (DocAuto) 100
(DocPrepare) (AddWarMachine) 10
(AddWarMachine) (DocApprove) 10
(DocApprove) (DocComplete) 10


Testing

We are nearly done. Hopefully i didn't forget to mention a step i did. If something doesn't work for you like i describe it here, sent me a mail! If you find the error by yourself, then please correct this page!

However, for me it now should work so let's make a test!


Login as GeneralA:

Roktest1.png


Open the ROK_Battle_Plan window and enter some details:

Roktest2.png

Hit the process button in the toolbar and start the process. Chose Complete as the action:

Roktest4.png Roktest3.png


Hit the button again and you will see that the workflow has started:

Roktest4.png Roktest5.png


Log out and back in but this time chose CommanderA as the user:

Roktest6.png


You should see that there is a Workflow Activity waiting for you:

Roktest7.png


Click on the button and the Workflow Activities tab opens:

Roktest8.png


Click on the window button and enter some lines. When you are done, close the window and hit the green checkmark button in the Workflow Activties tab:

Roktest9.png


Open the battle plan again and try to complete it. You should see a message that the plan need approval by Jim:

Roktest10.png


Finally log into the system with Jim as the user. He also has a workflow activity. Open it and chose Yes. Then hit the green Checkbox. Congratulations! You completed the document! Open the battle plan and hit the process toolbar button. You should now also see the Posted button. Press it and you will see the accounting entries which were created (no in our case).

Roktest12.png Roktest11.png Roktest13.png Roktest14.png


If you take a look at the console output (if you don't see anything but it still works, you have not added "log.warning()" to your methods ;), you see that the model ran through different stages and also our DocFactory returned our Doc_ROKBattlePlan and called it's methods:

Aug 27, 2014 2:27:31 PM un.kong.jim.model.MROKBattlePlan processIt
WARNING: Processing Action=AP - DocStatus=IP - DocAction=CO
Aug 27, 2014 2:27:31 PM un.kong.jim.model.MROKBattlePlan approveIt
WARNING: ----------------------------------------------------------------------------------
Aug 27, 2014 2:27:31 PM un.kong.jim.model.MROKBattlePlan <init>
WARNING: ----------------------------------------------------------------------------------
Aug 27, 2014 2:27:31 PM un.kong.jim.model.MROKBattlePlan processIt
WARNING: Processing Action=CO - DocStatus=AP - DocAction=CO
Aug 27, 2014 2:27:31 PM un.kong.jim.model.MROKBattlePlan completeIt
WARNING: ----------------------------------------------------------------------------------
Aug 27, 2014 2:27:31 PM un.kong.jim.model.MROKBattlePlan <init>
WARNING: ----------------------------------------------------------------------------------
Aug 27, 2014 2:27:31 PM un.kong.jim.document.Doc_ROKBattlePlan <init>
WARNING: !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
Aug 27, 2014 2:27:31 PM un.kong.jim.document.Doc_ROKBattlePlan loadDocumentDetails
WARNING: !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
Aug 27, 2014 2:27:31 PM org.compiere.acct.Doc isConvertible
WARNING: NOT from C_Currency_ID=195 to 100 - X_ROK_Battle_Plan[1000002]
Aug 27, 2014 2:27:31 PM un.kong.jim.document.Doc_ROKBattlePlan getBalance
WARNING: !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
Aug 27, 2014 2:27:31 PM un.kong.jim.model.MROKBattlePlan getSummary
WARNING: ----------------------------------------------------------------------------------
Aug 27, 2014 2:27:31 PM un.kong.jim.model.MROKBattlePlan getProcessMsg
WARNING: ----------------------------------------------------------------------------------
Aug 27, 2014 2:27:31 PM un.kong.jim.model.MROKBattlePlan <init>
WARNING: ----------------------------------------------------------------------------------


All you now have to do is to implement all the methods in your model/document. I would love to explain them step by step but unfortunately i am still very new to documents and i simply cant. So please, if there is somebody who has experience with it, tell the world how to do it. However, by taking a look at other document classes you should be able to find a way to get things working.

See Also