The Accelerate HR Blog

Conditional validation   (Fri Oct 19 2007)

If you're into Ruby on Rails I want to show you a neat little trick I learnt recently to help with conditional validation - when you want to check the validity of the data entered .. but only if certain conditions are met.

Let me put you in the picture. When someone enters a new employee record in Accelerate, I need to set up a whole raft of employee-related tables that are going to be needed later. One of these is a table including the employee's basic payroll details. It needs to include such details as the payment method (cash, cheque, bank transfer, etc) the bank account details, the payment currency, and so on.

This is the model we migrate to the database:

class CreatePaymentdetails < ActiveRecord::Migration

def self.up
create_table :paymentdetails do |t|
t.column :employee_id, :integer
t.column :bank_id, :integer
t.column :account_number, :string
t.column :account_name, :string
t.column :currency_id, :integer
t.column :payment_method, :integer
t.column :fixed_payment, :integer
t.column :percentage_of_salary, :integer
t.column :undertaking, :boolean, { :default => false }
end
end

def self.down
drop_table :paymentdetails
end
end

A couple of comments. employee_id, bank_id and currency_id are linked to the employees, banks and currencies tables, using the Rails magic to make all the connections.

But what are the three fields at the end? Well, there may need to be more than one entry for each employee in paymentdetails - because it's quite common for people to have their salary paid in more than one currency and/or to more than one bank. If the employee wants a fixed sum each month transferred to one of his banks, then a value is entered in fixed_payment. Alternatively he may request a percentage of the total salary to be transferred to one of the banks.

Validation here is tricky. We need to make sure that it's not possible to create an entry in both the fixed_payment and the percentage_of_salary fields. We have to check that one - and only one - of the employee's banks has no value set in either field. This will be the bank that receives the balance of the salary remaining after the other transfers have been made. And we need to anticipate and forestall user error. Almost inevitably someone's going to enter 0 in the percentage_of_salary field (i.e. pay nothing) when they actually want to leave the field blank (i.e. pay the balance to this account).

Finally, undertaking is a Yes/No field checked if the employer - on the employee's behalf - has promised to pay the bank a certain amount from the salary each month, probably as security against a bank loan to the employee. If the employee gives new instructions without the bank's permission and the payment details are changed, it's the employer who's going to be in trouble. So with undertaking checked, we'll be able to use another type of validation: don't allow this record to be changed until we've seen authorization from the bank.

When someone is hired we simply need to have a single record added to paymentdetails. Why do we need it to add it now? So there's no possibility we can forget to pay the salary when the next payroll comes round.

.

It all just happens in the background, and there's nothing particularly difficult about how we do it. We simply place an after_create build_associated tables callback at the head of the employee model and include this in the build_associated_tables method in the protected area below:

@location = Location.find_by_id(self.location_id)
@country = Country.find_by_id(@location.country_id)
@currency_id = @country.currency_id

Paymentdetail.create(:employee_id => self.id,
:currency_id => @currency_id,
:payment_method => 1,
:percentage_of_salary => 100
)

(Let me make everything really clear - add a comment and tell me if you think I'm stating the obvious. The employee belongs to a location, and the location is in a country, and the country has a standard currency. So in the first 3 lines we're finding the default currency in the country where the employee's working.

Payment method 1 is Cash, which is the way many construction businesses pay their people in my region. So until and unless we change the record, our employee is being paid cash.

And of course, because there's only one entry for this person in paymentdetail so far, then the whole salary - 100% of it - is paid to this account.)

So, leaving aside the tricky issues relating to multiple payment accounts, what do we need to validate here?

First we need to make sure that an employee_id exists in the record and that it links back to a valid record in the employees table. No problems here. In the paymentdetail model, that's just: -

belongs_to :employee
validates_associated :employee

validates_presence_of :employee_id

with a matching line in the employee model:

has_many :paymentdetails, :dependent => destroy

It's exactly the same for currency_id.

But bank_id is a little different. If the payment method is by bank transfer - we've defined that as payment method 3 - then we need to make sure that the database user has remembered to enter a valid bank_id as well as the employee's account number (and possibly the account name). But if the payment method is cash or cheque then we want to skip the validation on these fields. If we don't, we'll get an error every time.

There's a sweet way to do this shown in Ryan Bates' ScreenCast 41. And this is how it looks in Accelerate:- paymentdetail model. First set up the validations:

belongs_to :bank
validates_associated :bank

...( other validations )...

validates_presence_of :bank_id, :if => :pay_by_transfer?
validates_presence_of :account_number, :if => :pay_by_transfer?

Then set up a tiny method in the model:

def pay_by_transfer?
payment_method == 3
end

There's a little bit of magic here. How does Rails know that payment_method in the method is the same as the :payment_method column in the model? It's something to do with the fact that :if here is a symbol - note the colon. So unlike the normal if operator in a conditional statement, this one has a value assigned to it - the value defined in the pay_by_transfer? method. It'll take a better man than me to tell you exactly what's going on. But, the point is, it just works.

I've been a big fan of Ryan Bates since I first came across his work as a moderator on the Rails Forum. Somehow Ryan just seems to know everything about Rails - and I love his understated, self-deprecating style. Whenever I'm stumped with a problem, I always try and see if he's got anything to say first because if he has, I know I'm going to understand his explanation. And amongst experts, that's a rare talent. If you haven't checked out Ryan's Railscasts, do go and take a look. They're full of little gems like this one.

Filed under: Ruby on Rails






List recent Entries
List all blog entries filed under:

Employment Politics
HR
Implementation
Just thoughts
Ruby on Rails
Web 2.0

Can't find what you're looking for? Try this: -

Search blog for a word or phrase



 Subscribe to an RSS feed

Or get an email copy every time we post something new. Nothing new? Nothing mailed

Enter your email address:

Delivered by FeedBurner


If you're enjoying our blog, why not find out more, and maybe get involved?

ACCELERATE HR is a website built on Rails and designed for the enterprise. And we're building it live on the Web, right here.

Check out our home page HERE, or sign up for free HERE.


DESERT ISLAND BLOGS

Sharing a few of my favorites

HR Stimuli
McArthur's Rant

Jon Ingham's Strategic Human Capital Management Blog

The Rails Track
Railscasts

Web Power
The Technology Edge

Window on the Gulf
Mahmood's Den