Building Packaged Customizations¶
Customization Manager enables many new development and code deployment workflows. In this article we’ll work our way up to a simple workflow that focuses delivered code on customer’s business logic, makes code re-use easier, and backporting fixes to existing customers a breeze.
A simple problem¶
Let’s start with a simple use case:
My customer needs to set an option field to indicate whether an order needs a compliance check before shipping. The field should be set to true if any of the items in the order is in the account set with code “COMP”.
Extender makes this easy. One solution is to check orders after update or insert and set the optional field accordingly. At a high level:
on insert:
for each line in the order:
if the item has an account set code == "COMP":
set the optional field to True
A simple solution¶
Extender allows us to create a view script that will trigger on Insert or Update of an Order header. Handling the insert event only, we may end up with something like this attached to the OE0520 view:
from accpac.py import *
def onOpen():
return Continue
def onAfterInsert(result):
"""Check the account set of all items and set the optional field."""
# Open the order lines
oeordd = self.openDataSource("dsOEORDD")
# Seek to the first line
oeordd.browse("")
# Assume no compliance check is required
compliance_required = False
# Check all lines for any item requiring a check
while oeordd.fetch() == 0:
if oeordd.get("ACCTSET") == "COMP":
compliance_required = True
# If any on item matches we can stop looking
break
# Set the optional field, if required.
# Open the header optional fields
oeordho = self.openDataSource("dsOEORDHO")
# Try reading the field to see if it already exists.
oeordho.recordClear()
oeordho.put("OPTFIELD", OPTFIELD)
_read = oeordho.read()
# If it exists, update the field
if _read == 0:
if oeordho.read("VALUE") != compliance_required:
oeordho.put("VALUE", compliance_required)
_update = oeordho.update()
if _update != 0:
showMessageBox("Compliance Optional Field - "
"Failed to set the field.")
# If it does not, insert a new record
else:
oeordho.recordGenerate()
oroedho.put("OPTFIELD", OPTFIELD)
oeordho.put("VALUE", compliance_required)
_insert = oeordho.insert()
if _insert != 0:
showMessageBox("Compliance Optional Field - "
"Failed to set the field.")
Rinse and repeat for the update. This solution isn’t pretty and needs a good refactor but it works. With all the logic required to interact with the views readability is not good - it is hard to tease the business logic out.
Another request¶
Close on the heels of the first request, another customer requires that an optional field be set on an order if an item begins with a particular string.
Patterns begin to emerge, things that are required to deliver on the customer’s business logic. For example:
- Open a datasource, clear it, seek to the first record, and iterate. - Check all items in the order.
- Check for the existence of a record using
put
andread
. - Find whether an optional field already exists. - Insert or update an optional field. - If it exists, it needs updating, otherwise inserting.
Before starting the second request, let’s refactor the reusable parts out
from the first one into a new file called extools.py
from accpac import *
def all_records_in(datasourceid):
"""Generator that yields all records in a datasource."""
ds = self.openDataSource(datasourceid)
ds.browse("")
while ds.fetch() == 0:
yield ds
def _optional_field_exists_in(datasource, field):
"""Check if a record with field = value exists."""
datasource.recordClear()
datasource.put("FIELD", field)
if datasource.read() == 0:
return True
return False
def insert_or_update_optional_field(datasourceid, field, value):
"""Check if an optional field exists, if so update, otherwise insert"""
ofds = self.openDataSource(datasourceid)
ofds.recordClear()
if _optional_field_exists_in(ofds, field):
ofds.put("VALUE", value)
if ofds.update() != 0:
return false
else
ofds.recordGenerate()
ofds.put("FIELD")
ofds.put("VALUE", value)
if ofds.insert() != 0:
return False
return True
Now, let’s use our new tools in the solution.
from accpac.py import *
from extools import (insert_or_update_optfield, all_records_in, )
CODE = "HAL"
OPTFIELD = "COMPLIANCE"
def onOpen():
return Continue
def onAfterInsert(result):
"""Check the first characters of items and set the optional field."""
# Assume no compliance check is required
compliance_required = False
# Check all lines for any item requiring a check
for line in all_records_in("dsOEORDD"):
if line.get("ITEM").startswith(CODE):
compliance_required = True
result = insert_or_update_optional_field(
"dsOEORDHO", OPTFIELD, compliance_required)
if not result:
showMessageBox(
"Failed to update {} optional field.".format(OPTFIELD))
return Continue
That is a lot more readable - imagine trying to discuss what you’ve built with a client. Walking through the refactored version is much easier and reads almost exactly like the pseudo-code. The script is focused entirely on the customer’s business problem.
Packaging it all up¶
Now we just need a way to distribute the new package to users along with our script.
Creating a python package is easy, we can create one for our extools
by creating a new directory in the right format and adding
setup.py
and empty __init__.py
files.
extools/
|- __init__.py
|- setup.py
|- docs/
|- tests/
|- extools/
|- __init__.py
|- extools.py
The special setup.py
file contains instructions on how to install our
package and what it depends on. Our simple package has no dependencies [2]
but still requires a simple setup.py
.
Note that documentation and testing are included directly in the package, along side the code, keeping all the elements of the customization together.
[2] | It depends on accpac.py but that isn’t registered in package
indexes. |
from setuptools import setup
setup(
name='extools',
version='0.1',
author='cbinckly',
url='https://2665093.ca',
author_email='cbinckly@gmail.com',
packages=['extools'],
description='Tools for Orchid Extender.',
install_requires=[],
)
That is a minimum viable setup file. The setuptools
package provides loads
of other options that include advanced metadata, file installation, shortcuts,
and more. Check out the Hitchhiker’s Guide to Python Packaging for
excellent docs on the topic.
Now that we have our package, we just need to make it publicly available. We can publish on pypi.org or create a VCS repository at Github, bitbucket, or any other VCS server.
Now, the customer can deploy the script and install the extools
package in
two clicks using Customization Manager.
A bug report¶
All is well until a report comes in of a bug: the first script doesn’t always check all records. After some debugging, neither of them (or the three sebsequently delivered) always check all records!
It turns out data sources need to be .recordClear()
before browsing.
It is a simple one line change, before running <datasource>.browse("")
we
need to add <datasource>.recordClear()
.
For the first customer, a new script needs to be issued. For the second and
all subsequent customers, if the extools
package is updated once all
they need to do is a two click upgrade. No fiddling with updated files for
each customer, the fix is easily backported.
Conclusion¶
This is just one example of a workflow that is enabled by easy package management for Extender. It helps keep delivered code focused on the business logic, reduces the time to develop by encouraging and making reuse easy, and makes it simple to deploy fixes and backports to existing customers - all of which improve code quality, customer experience, and decrease support engagements.
See what the extools package has become.