A contracts management system

I spent most of this week banging out a contracts request, approval, and tracking system. Most of the tools were readily available in Drupal and just needed some modifications to meet specific business rules. I tried very hard on this project to do things "the Drupal way": override instead of edit, no logic in the theme layer, etc.

The Actions and Workflow modules were key players, as were CCK and views of course. I'm using slightly outdated versions of Actions and Workflow because they were what I was familiar with from a previous project and I didn't have time to learn the new APIs. Maybe some of my efforts would be redundant in the new versions; I will have to check that out.

A quick overview

We needed a way to move contract requests through all the appropriate approval channels. The ideal workflow is requisitioner -> manager -> vice-president -> various budget directors -> risk auditor -> CFO. The tricky bits revolved around the potential for multiple budget directors and the ability to create amendments to a completed contract.

Authorship as Assignation

One decision that I made early on was to hijack the concept of "author" and use it to mean "assigned". That is, when a contract node moves into a new workflow state, its author is changed to be the person that needs to work on it in that state. This allows for any user to easily see who is working on a record, and to create a "to do" view.

At node creation, user reference fields for everyone involved are presented. They are either limited by role or given default values in hook_form_alter. The workflow form looks a little weird because almost all the permissions are given to authors only.

The way I managed it was to create an action which is fired at almost every state change which gets the new ID. One snag that took a while to figure out is that the call to node_save in workflow_tab_form_submit overwrites any node access changes that are performed in actions it triggers. For now I added some code right to workflow_tab_form_submit after node_save which sets the node's author ID and calls node_save again. I think there's a hook_form_validate that I might be able to use to do that without making changes, but for now it is working.

Another difficulty was with giving default values to user reference fields and disabling them in hook_form_alter. Turns out that disabling them causes them not to submit a POST value, so they fail validation. The trick was to explicitly set their value to their default value in hook_form_alter. I documented this at http://drupal.org/node/241704#comment-983684 .

Line Items

Another tricky bit was the concepts of budget directors and line items. Each contract has an indefinite number of line items or funding sources associated with it, which essentially have their own approval process. My solution was to create a line item content type and give it its own workflow.

The problem then was that a contract can't move past a certain state until all its funding sources have been approved. Again, I created a new action which fires when the SVP moves the contract into the "processing funds sources" state. No user has rights to move a contract out of "processing funds sources". However, when each line item is moved into the "approved" state, another action calls a view for a count of all unapproved line items which share a parent. If that view is empty, the action moves the parent contract along to the risk auditor.

The line items themselves are created by the manager as part of that approval step. They are put into a "created" state and moved along to "submitted" by the SVP's action only; managers cannot submit line items directly.

Amendments

A finalized contract sometimes must be amended. Because we're producing legal documents at the end of the day, revisions were not sufficient; amendments are really separate beasts. So there is another content type for them, with a required nodereference as a parent, whose valid choices are approved contracts. Creating an amendment moves the parent into an "amending" state, where nobody can do anything to it. Only the completion of the amendment will allow the parent to come back into the "signed" state. Again, this was a custom action.

Workflow Links

Since workflows and amendments are so important, I wanted to give views a way to provide a direct link to workflows. hook_view_tables actually let me override the default behavior of workflow_states and add a couple of handlers. One just returns the current workflow state, the other is a link to the workflow tab... unless the contract is completed, at which point the link goes to the amendment creation screen, with the parent contract pre-selected based on $_GET.

Printing

Central to the project is the ability to print a legal contract to be signed based off all the forms that have been filled out. hook_menu let me define a "print" tab for contracts and amendments. Within that, I include a file where the contract body is held in a big 'ol string with placeholders. Then it's just a matter of building the variable array and calling t.

There is also a block view which shows up only on this tab which contains links to all the attachments so they can be easily printed.

Conditionally mandated workflow comments

One of the business requirements is that a contract or amendment being sent backwards in the workflow must have a reason attached, but going forward does not require one. This was done using state weights and hook_form_validate as follows:

<?php
  $wid
= workflow_get_workflow_for_type($form_values['node']->type);
 
$states = _workflow_get_weights($wid);
 
$current = $form_values['node']->_workflow;
 
$target = $form_values['workflow'];
  if (
$states[$target] < $states[$current] && strlen($form_values['workflow_comment']) === 0) {
   
form_set_error('workflow_comment', 'If you are sending this back, you must provide a comment.');
  }

function
_workflow_get_weights($wid) {
 
$states = array();
 
$result = db_query("SELECT weight, sid FROM {workflow_states} WHERE wid = %d ORDER BY weight, state", intval($wid));
  while (
$data = db_fetch_object($result)) {
   
$states[$data->sid] = $data->weight;
  }
  return
$states;
}
?>

Other modules

auto_nodetitle is being used for all content types. date and cck_address are useful, though I had to re-theme the address output to kill a bunch of extra line breaks. filefield is essential for exhibits. extlink is nice, but I'm using a modified version that also tags files.

backup users

A new requirement that came up was for users to have the ability to flag themselves as being out of office, and then have a backup user become responsible for their work while that's true. I ended up working that into another module based heavily off of Alt Login.

Basically it uses hook_user to add a checkbox for out-of-office and a pulldown of other users to the user edit screen. There is a permission to allow users to change their own backup or not. Then there is a simple function that I'll use in the main module to check if a user ID is out of the office, and who their backup is if so.

I'm thinking of cleaning this one up a bit and releasing it to the community. With some configurations I can see where it would be useful in other workflows.