OpenERP Sales Workflow Part I 6.0.3

This series of posts is from an internal document I wrote for 6.0.3 when trying to unravel exactly how sales workflow worked.  It is probably useful for developers wanting to understand what is going on and probably best read in conjunction with the source code.  It is a long document so I will release in a series of posts.

Sales Workflow Summary

The sales workflow manages the flow of order workflow through openerp. It is essentially made up of 2 paths which are run in parallel.

  • The first path – the invoicing path, wraps the invoicing workflow with handling of different states in and out.
  • The second path – the shipping path, wraps the management of picking and procurement with sales order workflow to handle messages coming in and out of the various workflows created in the shipping process.
  • When both paths are completed a sales order is considered done.

 

 

 

Quick Tip – calling methods with standard function signature

In OpenERP, if you have spent any time at all writing mods you will be used to something like this

prod_obj.action_update_cost(cr, uid, product_id, context)

to call a method of an object, passing the cursor, user id, product_id and context.  Also you have probably done something like this

sale_order.order_line.product_id

to obtain a browse record, or even appended .id to get the id of the object for passing in the above orm_pool function.

Well, as it turns out there is another way, and you can call these methods directly, and without arguments, so for example

sale_order.order_line.product_id.action_update_cost()

Then the ORM works out the arguments for you.  I kind of worked this out from some write calls I had seen in workflow, but to date have had no problems using it with any function with a standard signature.

Dictionary comprehensions that are OpenERP compatible

One of the great new additions in python 2.7 is dictionary comprehensions.  Unfortunately OpenERP still publicly wants python 2.5, and in reality runs on python 2.6 as a minimum in most installations.  So if you are wanting to publish a module then python 2.7 style dictionary comprehensions are out.  However it is fairly easy to approximate them using this trick.

Say you need to return a list of dictionaries, with id as the key and the field value as the value.  A very common requirement with function fields, you will see code like this

    def _get_attendee_count(self, cr, uid, ids, field, arg, context=None):
        res = {}
        for session in self.browse(cr, uid, ids, context=context):
            res[session.id] = len(session.attendee_ids)
            
        return res

Using a dictionary comprehension we can make this code much shorter and importantly faster. Simply write

    def _get_attendee_count(self, cr, uid, ids, field, arg, context=None):
        return dict([(session.id, len(session.attendee_ids) for session in self.browse(cr, uid, ids, context=context)])

OK not so simple lets break it down.

dict(lots of code) – casts our result as a dictionary

[lots of code] – starts and ends a standard python list comprehension

(session.id, len(session.attendee_ids) – creates a tuple which dict casts as {key: value}

for session etc is our loop condition.

So we end up with a list of tuples initial like [(1,4),(2,5),(13,8)] which dict transforms in to {1: 4, 2: 5, 13: 8}.

Same result, faster.  Not for use everytime, as they can be hard to understand, but where the function is obvious, or the code is going to be run many times with lots of ids e.g. function fields in tree views they save a lot of time.

For the record what does a 2.7.2 dictionary comprehension look like (I’m not sure this is standard library but definitely is in Python 3).

return {session.id: len(session.attendee_ids) for session in self.browse(cr, uid, ids, context=context)}

 

How to use a bogus domain to do special searching

This excellent post on openerp.com from Smart IT

This assumes you have a ‘thing’ with an ‘action_date’ and we want a list of this thing from today’s date + a few days, we dont want everything.

By adding a bogus domain field in the menu item then overriding the search function if the bogus domian field is found, this example adds 2 days onto today so you get everything ‘today’(at midnight) and 2 days from now.
First add the domain filter to the menu item:
<record model=”ir.actions.act_window” id=”open_x_upcomingtasks”>
<field name=”domain”> [('state','=','wacky')] </field>
</record>
Then in the py file:
#otherimports up here…
from datetime import datetime
from datetime import date, timedelta

class x(osv.osv):

_name=”x.x”

def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False, xtra=None):
if (args and len(args)>0 and str(args[0]) == “(‘state’, ‘=’, ‘wacky’)”):
args=[]
newStartPeriod=datetime.today()
newStartPeriod = newStartPeriod.replace(hour=0) #i wanted everything today, not just from this minute
newStartPeriod = newStartPeriod.replace(minute=0) #if you want you can remove these and you
newStartPeriod = newStartPeriod.replace(second=0) #will just get stuff after this second
newEndPeriod=newStartPeriod+timedelta(days=(2)) #add on 2 days…
args = [('action_date', '>=', newStartPeriod.strftime('%Y-%m-%d %H:%M:%S'))]
args += [('action_date', '<=', newEndPeriod.strftime('%Y-%m-%d %H:%M:%S'))]
args +=[('action_type','=','phonecall')] #i have an action type here and i only want phone calls
#you could add state filters or user filters etc
ret = super(x, self).search(cr, user, args, offset, limit, order, context, count)
else:
ret = super(x, self).search(cr, user, args, offset, limit, order, context, count)
return ret

_________________
Developing OpenERP Modules for what seems like an eternity
Smart IT Ltd
http://www.smart-ltd.co.uk

Mental notes to self – you are an idiot

  File "/home/graemeg/workspace/openerp/server/bin/tools/convert.py", line 865, in parse
    self._tags[rec.tag](self.cr, rec, n)
  File "/home/graemeg/workspace/openerp/server/bin/tools/convert.py", line 507, in _tag_act_window
    id = self.pool.get('ir.model.data')._update(cr, self.uid, 'ir.actions.act_window', self.module, res, xml_id, noupdate=self.isnoupdate(data_node), mode=self.mode)
  File "/home/graemeg/workspace/openerp/server/bin/addons/base/ir/ir_model.py", line 691, in _update
    res_id = model_obj.create(cr, uid, values, context=context)
  File "/home/graemeg/workspace/openerp/server/bin/osv/orm.py", line 3688, in create
    self._validate(cr, user, [id_new], context)
  File "/home/graemeg/workspace/openerp/server/bin/osv/orm.py", line 946, in _validate
    raise except_orm('ValidateError', '\n'.join(error_msgs))
except_orm: ('ValidateError', 'Error occurred while validating the field(s) res_model,src_model: Invalid model name in the action definition.')

Means you forgot to end your module files with a carriage return or NOT. You used underscores not dots in the model name.  Check next time.

May as well keep on going

Traceback (most recent call last):
  File "/home/graemeg/workspace/openerp/server/bin/osv/osv.py", line 122, in wrapper
    return f(self, dbname, *args, **kwargs)
  File "/home/graemeg/workspace/openerp/server/bin/osv/osv.py", line 176, in execute
    res = self.execute_cr(cr, uid, obj, method, *args, **kw)
  File "/home/graemeg/workspace/openerp/server/bin/osv/osv.py", line 167, in execute_cr
    return getattr(object, method)(cr, uid, *args, **kw)
  File "/home/graemeg/workspace/openerp/server/bin/osv/orm.py", line 1645, in fields_view_get
    xarch, xfields = self.__view_look_dom_arch(cr, user, result['arch'], view_id, context=ctx)
  File "/home/graemeg/workspace/openerp/server/bin/osv/orm.py", line 1314, in __view_look_dom_arch
    fields_def = self.__view_look_dom(cr, user, node, view_id, context=context)
  File "/home/graemeg/workspace/openerp/server/bin/osv/orm.py", line 1285, in __view_look_dom
    fields.update(self.__view_look_dom(cr, user, f, view_id, context))
  File "/home/graemeg/workspace/openerp/server/bin/osv/orm.py", line 1246, in __view_look_dom
    attrs['selection'] = relation._name_search(cr, user, '', dom, context=search_context, limit=None, name_get_uid=1)
AttributeError: 'NoneType' object has no attribute '_name_search'

Just because the field is a selection field doesn’t mean it has a selection widget – idiot.

The Wizard is dead long live osv_memory,

oops osv_memory is dead (lasting all of 1 release), long live osv (well short live osv)  I found this on a bug report as to why it won’t be fixed.  Feel less guilty for not finishing the wizard series of posts now.  The bug report revealing this is 813395 if anyone is interested.  Still don’t quite get why they won’t fix the bug – so much for LTS.  Lets just ignore the problem until someone who pays us tells us about it – nice proactive quality approach.

 

Hello,

The osv_memory  will soon be disappearing from the server, it will be
replaced by osv with just some boolean information to know that it
should be cleaned from the db from time to time so these will work as
normal osv but with short life span.

Thanks

Edit: Just realised, most of my modules are heavily wizard dependent.  This should be fun, hopefully not too bad.

Wizards in Reporting

OK, I know I am still calling them wizards.  The name has grown on me.  I mean osv_memory objects.  At least it isn’t as confusing as OpenERP calling a python class an object, and a python object an instance but I digress.  For the record when I say object I use it for both a python class and python object.  Usually which one I am referring to is clear by the context, if it isn’t then I’ll use class and record or instance respectively.

So why do we want to use wizards in reporting.

Well sometimes we don’t.  Sales Orders (maybe), Invoices etc, we press print and away we go.  Pops up a screen and we select our report, and generally it is the same one.  But sometimes, we want the report to look different depending on some attributes of our object (I’m just going to use sale.order here to make it real) such as its state.  Excellent, just create a wizard and tell it to choose between 2 reports based on state.  Well no, again, we can place conditional content in our reports based on any attribute of the sale.order (or for that matter any relation we can chain to such as partner or sale order line)  so we don’t need 2 reports.  (Hmm I better do a section on reporting).

But sometimes we want to make human choices about a range of options that are in no way related to the object, but that need feeding to the report.  Excellent, now a wizard is appropriate.  So what might those options be.  With my sale.order example I struggle to think of one.  So lets blur the lines a wee bit.  Stay with me.  A balance sheet report.  Oh but I hear you say, but a balance sheet isn’t an object, and you’d be wrong at least temporarily, but we’ll explain that later, for now we are focusing on options that require human interaction.

So lets take our balance sheet, which is essentially a report of specific parts of our General Ledger, grouped and summed that gives us a snapshot of our businesses assets and liabilities at a point in time.  In other words, we are working with account moves, and accounts.  So what options might we like.  We might want to know what our balance sheet looked like 8 seconds ago, not likely, but more likely what it looked like (or will look like) on a specific date.  We might have multiple companies and want to choose one of them, we might decide we only want to see down to a certain level, or for a certain chart of accounts.  We might want all amounts without cents, or with cents, all accounts, accounts with balances only.  Now once we start to go through the options we see that there are many possible combinations of these attributes and really only attributes that the user can determine.

So our balance sheet wizard might look like this.  Taken from account_account_extension_o4sb found at apps.openerp.com .

Balance Sheet Wizard

Great Wizard, no hat

So next time we’ll dig in to this a little deeper, explain our other uses for wizards in reporting and maybe look at some code.

osv.osv_memory wizards

Up until now I have managed to avoid wizards.  I’ve always had a feeling that they would be useful, but with so much more to learn on other aspects, have left them alone.  This is partly because I find that while I have half a clue what I’m doing, in most cases OpenERP assumes at least three-quarters when explaining things in their developer documentation.  When it comes to wizards, it is even worse.  I must have read that documentation 50 times and it is still useless.  Even after successfully creating a wizard.  80% of the documentation refers to old style wizards, with 10% explaining new style, only in relation to res.config.  Add on a bit of how to convert from old style to new style and you are left worse off than when you begin.

So now I’ll make it my mission to explain them more from a user/developer perspective.  It’s going to be more than one post.

I think we should get another thing straight, I hate the terms old style and new style wizards. It makes it sound like a whole new syntax, methods, architecture to get to grips with.  In the case of old style wizards, such as those in v5 – this is (probably) absolutely true.  It also doesn’t help that the default folder name for these – thinking of a better name – Openerp memory objects, is wizard, and half the time their views are called wizard or even their name.  That is a throwback to v5 and hopefully, like we saw the death of __terp__.py, once all the old style wizards are gone and everyone forgets their awfulness we can call the new style one just plain old wizards again.  Until then it is confusing.

The documentation will tell you it is about a series of interactions between server and client.  I kind of get that but they never really got to the crux of the matter.  For me, you want to use a wizard when you want the user to define the parameters to be sent to a particular function.  I use the term function loosely, this may be producing a report, updating the database, or anything else you can do in OpenERP.

Importantly, once it is finished, you no longer want that object instance/record.  That is unlike standard osv.osv objects that are stored in our postgres database, osv.osv_memory objects are temporal and only exist in memory.

Other than that it all seems pretty much the same.  Views, workflows (yet to really experiment but I have a couple of ideas), fields and functions are all the same.  They can be assigned to menus, objects or called in code and aside from popping up a modal box instead of tab, you are probably using them and not knowing.

I would put a note there that because they exist only in memory, defining bi-directional relations with osv.osv objects is unwise at best, and I don’t really see the point of defining a one2many with an osv.osv object either even if it is one way.  many2one and many2many uni-directional though are all good, and the many2many bridging table, at least as far as I can tell also only exists in memory.  There isn’t a great deal of point of one2many in the main application I have used wizards for to date anyway, which is reporting, because you don’t want to create new records typically (at least not manually), it is about using existing osv.osv database records.

Anyhow, the next post will focus on using Wizards in Reporting