From eab2e225a223d0919ee4dde65f4dfe7e269fa01c Mon Sep 17 00:00:00 2001
From: "Nimesh (Open ERP)"
- You can view the invoice document, download it and pay online using the following link: -
- View Invoice - % if object.company_id.paypal_account and object.type in ('out_invoice', 'in_refund'): <% comp_name = quote(object.company_id.name) diff --git a/addons/account/test/test_edi_invoice.yml b/addons/account/test/test_edi_invoice.yml index 80dc098e770..3d57f8dc608 100644 --- a/addons/account/test/test_edi_invoice.yml +++ b/addons/account/test/test_edi_invoice.yml @@ -38,11 +38,12 @@ - Then I export the customer invoice - - !python {model: edi.document}: | + !python {model: edi.edi}: | + import json invoice_pool = self.pool.get('account.invoice') invoice = invoice_pool.browse(cr, uid, ref("invoice_edi_1")) - token = self.export_edi(cr, uid, [invoice]) - assert token, 'Invalid EDI Token' + edi_doc = self.generate_edi(cr, uid, [invoice]) + assert isinstance(json.loads(edi_doc)[0], dict), 'EDI doc should be a JSON dict' - Then I import a sample EDI document of another customer invoice - diff --git a/addons/edi/__init__.py b/addons/edi/__init__.py index 46fabbc2fd0..71ee8aaa53d 100644 --- a/addons/edi/__init__.py +++ b/addons/edi/__init__.py @@ -20,14 +20,14 @@ ############################################################################## import logging -import models -import edi_service -from models.edi import EDIMixin, edi_document +from . import models +from . import edi_service +from models.edi import EDIMixin, edi _logger = logging.getLogger(__name__) # web try: - import controllers + import openerp.addons.web.controllers except ImportError: _logger.warn( """Could not load openerp-web section of EDI, EDI will not behave correctly diff --git a/addons/edi/__openerp__.py b/addons/edi/__openerp__.py index b06c01eb03b..72ccb32d701 100644 --- a/addons/edi/__openerp__.py +++ b/addons/edi/__openerp__.py @@ -36,12 +36,10 @@ documentation at http://doc.openerp.com. 'website': 'http://www.openerp.com', 'depends': ['base', 'email_template'], 'icon': '/edi/static/src/img/knowledge.png', - 'data': ['security/ir.model.access.csv'], 'test': ['test/edi_partner_test.yml'], 'js': ['static/src/js/edi.js'], 'css': ['static/src/css/edi.css'], 'qweb': ['static/src/xml/*.xml'], - 'installable': True, 'auto_install': False, } diff --git a/addons/edi/controllers/main.py b/addons/edi/controllers/main.py index c62fef19cba..0322099b6f3 100644 --- a/addons/edi/controllers/main.py +++ b/addons/edi/controllers/main.py @@ -1,31 +1,10 @@ -import json -import textwrap - -import simplejson -import werkzeug.wrappers - -import openerp.addons.web.http as openerpweb +import openerp.addons.web.common.http as openerpweb import openerp.addons.web.controllers.main as webmain class EDI(openerpweb.Controller): - # http://hostname:8069/edi/view?db=XXXX&token=XXXXXXXXXXX # http://hostname:8069/edi/import_url?url=URIEncodedURL _cp_path = "/edi" - def template(self, req, mods='web,edi'): - d = {} - d["js"] = "\n".join(''%i for i in webmain.manifest_list(req, mods, 'js')) - d["css"] = "\n".join(''%i for i in webmain.manifest_list(req, mods, 'css')) - d["modules"] = simplejson.dumps(mods.split(',')) - return d - - @openerpweb.httprequest - def view(self, req, db, token): - d = self.template(req) - d["init"] = 's.edi.edi_view("%s","%s");'%(db,token) - r = webmain.html_template % d - return r - @openerpweb.httprequest def import_url(self, req, url): d = self.template(req) @@ -33,46 +12,6 @@ class EDI(openerpweb.Controller): r = webmain.html_template % d return r - @openerpweb.httprequest - def download(self, req, db, token): - result = req.session.proxy('edi').get_edi_document(db, token) - response = werkzeug.wrappers.Response( result, headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]) - return response - - @openerpweb.httprequest - def download_attachment(self, req, db, token): - result = req.session.proxy('edi').get_edi_document(db, token) - doc = json.loads(result)[0] - attachment = doc['__attachments'] and doc['__attachments'][0] - if attachment: - result = attachment["content"].decode('base64') - import email.Utils as utils - - # Encode as per RFC 2231 - filename_utf8 = attachment['file_name'] - filename_encoded = "%s=%s" % ('filename*', - utils.encode_rfc2231(filename_utf8, 'utf-8')) - response = werkzeug.wrappers.Response(result, headers=[('Content-Type', 'application/pdf'), - ('Content-Disposition', 'inline; ' + filename_encoded), - ('Content-Length', len(result))]) - return response - - @openerpweb.httprequest - def binary(self, req, db, token, field_path="company_address.logo", content_type='image/png'): - result = req.session.proxy('edi').get_edi_document(db, token) - doc = json.loads(result)[0] - for name in field_path.split("."): - doc = doc[name] - result = doc.decode('base64') - response = werkzeug.wrappers.Response(result, headers=[('Content-Type', content_type), - ('Content-Length', len(result))]) - return response - - @openerpweb.jsonrequest - def get_edi_document(self, req, db, token): - result = req.session.proxy('edi').get_edi_document(db, token) - return json.loads(result) - @openerpweb.jsonrequest def import_edi_url(self, req, url): result = req.session.proxy('edi').import_edi_url(req.session._db, req.session._uid, req.session._password, url) @@ -80,6 +19,4 @@ class EDI(openerpweb.Controller): return {"action": webmain.clean_action(req, result[0][2])} return True -# - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/edi/edi_service.py b/addons/edi/edi_service.py index 63fe38786e0..fd911139170 100644 --- a/addons/edi/edi_service.py +++ b/addons/edi/edi_service.py @@ -20,8 +20,8 @@ ############################################################################## import logging -import netsvc import openerp +import openerp.netsvc as netsvc _logger = logging.getLogger(__name__) @@ -34,10 +34,10 @@ class edi(netsvc.ExportService): try: registry = openerp.modules.registry.RegistryManager.get(db_name) assert registry, 'Unknown database %s' % db_name - edi_document = registry['edi.document'] + edi = registry['edi.edi'] cr = registry.db.cursor() res = None - res = getattr(edi_document, method_name)(cr, *method_args) + res = getattr(edi, method_name)(cr, *method_args) cr.commit() except Exception: _logger.exception('Failed to execute EDI method %s with args %r.', method_name, method_args) @@ -46,9 +46,6 @@ class edi(netsvc.ExportService): cr.close() return res - def exp_get_edi_document(self, db_name, edi_token): - return self._edi_dispatch(db_name, 'get_document', 1, edi_token) - def exp_import_edi_document(self, db_name, uid, passwd, edi_document, context=None): return self._edi_dispatch(db_name, 'import_edi', uid, edi_document, None) @@ -59,9 +56,6 @@ class edi(netsvc.ExportService): if method in ['import_edi_document', 'import_edi_url']: (db, uid, passwd ) = params[0:3] openerp.service.security.check(db, uid, passwd) - elif method in ['get_edi_document']: - # No security check for these methods - pass else: raise KeyError("Method not found: %s." % method) fn = getattr(self, 'exp_'+method) diff --git a/addons/edi/models/edi.py b/addons/edi/models/edi.py index 6d11db1c52c..29e19f2da3b 100644 --- a/addons/edi/models/edi.py +++ b/addons/edi/models/edi.py @@ -30,9 +30,9 @@ import urllib2 import openerp import openerp.release as release -import netsvc -import pooler -from osv import osv,fields,orm +import openerp.netsvc as netsvc +from openerp.modules.registry import RegistryManager +from openerp.osv import osv, fields from tools.translate import _ from tools.safe_eval import safe_eval as eval _logger = logging.getLogger(__name__) @@ -74,16 +74,9 @@ def last_update_for(record): return False -class edi_document(osv.osv): - _name = 'edi.document' - _description = 'EDI Document' - _columns = { - 'name': fields.char("EDI token", size = 128, help="Unique identifier for retrieving an EDI document."), - 'document': fields.text("Document", help="EDI document content") - } - _sql_constraints = [ - ('name_uniq', 'unique (name)', 'EDI Tokens must be unique!') - ] +class edi(osv.AbstractModel): + _name = 'edi.edi' + _description = 'EDI Subsystem' def new_edi_token(self, cr, uid, record): """Return a new, random unique token to identify this model record, @@ -109,7 +102,7 @@ class edi_document(osv.osv): """Generates a final EDI document containing the EDI serialization of the given records, which should all be instances of a Model that has the :meth:`~.edi` mixin. The document is not saved in the - database, this is done by :meth:`~.export_edi`. + database. :param list(browse_record) records: records to export as EDI :return: UTF-8 encoded string containing the serialized records @@ -120,19 +113,6 @@ class edi_document(osv.osv): edi_list += record_model_obj.edi_export(cr, uid, [record], context=context) return self.serialize(edi_list) - def get_document(self, cr, uid, edi_token, context=None): - """Retrieve the EDI document corresponding to the given edi_token. - - :return: EDI document string - :raise: ValueError if requested EDI token does not match any know document - """ - _logger.debug("get_document(%s)", edi_token) - edi_ids = self.search(cr, uid, [('name','=', edi_token)], context=context) - if not edi_ids: - raise ValueError('Invalid EDI token: %s.' % edi_token) - edi = self.browse(cr, uid, edi_ids[0], context=context) - return edi.document - def load_edi(self, cr, uid, edi_documents, context=None): """Import the given EDI document structures into the system, using :meth:`~.import_edi`. @@ -171,38 +151,18 @@ class edi_document(osv.osv): """ return json.loads(edi_documents_string) - def export_edi(self, cr, uid, records, context=None): - """Export the given database records as EDI documents, stores them - permanently with a new unique EDI token, for later retrieval via :meth:`~.get_document`, - and returns the list of the new corresponding ``ir.edi.document`` records. - - :param records: list of browse_record of any model - :return: list of IDs of the new ``ir.edi.document`` entries, in the same - order as the provided ``records``. - """ - exported_ids = [] - for record in records: - document = self.generate_edi(cr, uid, [record], context) - token = self.new_edi_token(cr, uid, record) - self.create(cr, uid, { - 'name': token, - 'document': document - }, context=context) - exported_ids.append(token) - return exported_ids - def import_edi(self, cr, uid, edi_document=None, edi_url=None, context=None): """Import a JSON serialized EDI Document string into the system, first retrieving it from the given ``edi_url`` if provided. - :param str|unicode edi_document: UTF-8 string or unicode containing JSON-serialized + :param str|unicode edi: UTF-8 string or unicode containing JSON-serialized EDI Document to import. Must not be provided if ``edi_url`` is given. - :param str|unicode edi_url: URL where the EDI document (same format as ``edi_document``) + :param str|unicode edi_url: URL where the EDI document (same format as ``edi``) may be retrieved, without authentication. """ if edi_url: - assert not edi_document, 'edi_document must not be provided if edi_url is given.' + assert not edi_document, 'edi must not be provided if edi_url is given.' edi_document = urllib2.urlopen(edi_url).read() assert edi_document, 'EDI Document is empty!' edi_documents = self.deserialize(edi_document) @@ -215,10 +175,10 @@ class EDIMixin(object): ``edi_import()`` and ``edi_export()`` methods to implement their specific behavior, based on the primitives provided by this mixin.""" - def _edi_requires_attributes(self, attributes, edi_document): - model_name = edi_document.get('__imported_model') or edi_document.get('__model') or self._name + def _edi_requires_attributes(self, attributes, edi): + model_name = edi.get('__imported_model') or edi.get('__model') or self._name for attribute in attributes: - assert edi_document.get(attribute),\ + assert edi.get(attribute),\ 'Attribute `%s` is required in %s EDI documents.' % (attribute, model_name) # private method, not RPC-exposed as it creates ir.model.data entries as @@ -318,7 +278,6 @@ class EDIMixin(object): :return: list of dicts containing boilerplate EDI metadata for each record, at the corresponding index from ``records``. """ - data_ids = [] ir_attachment = self.pool.get('ir.attachment') results = [] for record in records: @@ -398,7 +357,7 @@ class EDIMixin(object): return [self.edi_m2o(cr, uid, r, context=context) for r in records] def edi_export(self, cr, uid, records, edi_struct=None, context=None): - """Returns a list of dicts representing an edi.document containing the + """Returns a list of dicts representing EDI documents containing the records, and matching the given ``edi_struct``, if provided. :param edi_struct: if provided, edi_struct should be a dictionary @@ -443,50 +402,6 @@ class EDIMixin(object): results.append(edi_dict) return results - def edi_export_and_email(self, cr, uid, ids, template_ext_id, context=None): - """Export the given records just like :meth:`~.export_edi`, the render the - given email template, in order to trigger appropriate notifications. - This method is intended to be called as part of business documents' - lifecycle, so it silently ignores any error occurring during the process, - as this is usually non-critical. To avoid any delay, it is also asynchronous - and will spawn a short-lived thread to perform the action. - - :param str template_ext_id: external id of the email.template to use for - the mail notifications - :return: True - """ - def email_task(): - db = pooler.get_db(cr.dbname) - local_cr = None - try: - time.sleep(3) # lame workaround to wait for commit of parent transaction - # grab a fresh browse_record on local cursor - local_cr = db.cursor() - web_root_url = self.pool.get('ir.config_parameter').get_param(local_cr, uid, 'web.base.url') - if not web_root_url: - _logger.warning('Ignoring EDI mail notification, web.base.url is not defined in parameters.') - return - mail_tmpl = self._edi_get_object_by_external_id(local_cr, uid, template_ext_id, 'email.template', context=context) - if not mail_tmpl: - # skip EDI export if the template was not found - _logger.warning('Ignoring EDI mail notification, template %s cannot be located.', template_ext_id) - return - for edi_record in self.browse(local_cr, uid, ids, context=context): - edi_token = self.pool.get('edi.document').export_edi(local_cr, uid, [edi_record], context = context)[0] - edi_context = dict(context, edi_web_url_view=EDI_VIEW_WEB_URL % (web_root_url, local_cr.dbname, edi_token)) - self.pool.get('email.template').send_mail(local_cr, uid, mail_tmpl.id, edi_record.id, - force_send=False, context=edi_context) - _logger.info('EDI export successful for %s #%s, email notification sent.', self._name, edi_record.id) - except Exception: - _logger.warning('Ignoring EDI mail notification, failed to generate it.', exc_info=True) - finally: - if local_cr: - local_cr.commit() - local_cr.close() - - threading.Thread(target=email_task, name='EDI ExportAndEmail for %s %r' % (self._name, ids)).start() - return True - def _edi_get_object_by_name(self, cr, uid, name, model_name, context=None): model = self.pool.get(model_name) search_results = model.name_search(cr, uid, name, operator='=', context=context) @@ -515,18 +430,20 @@ class EDIMixin(object): file_name = record.name_get()[0][1] file_name = re.sub(r'[^a-zA-Z0-9_-]', '_', file_name) file_name += ".pdf" - ir_attachment = self.pool.get('ir.attachment').create(cr, uid, - {'name': file_name, - 'datas': result, - 'datas_fname': file_name, - 'res_model': self._name, - 'res_id': record.id, - 'type': 'binary'}, - context=context) + self.pool.get('ir.attachment').create(cr, uid, + { + 'name': file_name, + 'datas': result, + 'datas_fname': file_name, + 'res_model': self._name, + 'res_id': record.id, + 'type': 'binary' + }, + context=context) - def _edi_import_attachments(self, cr, uid, record_id, edi_document, context=None): + def _edi_import_attachments(self, cr, uid, record_id, edi, context=None): ir_attachment = self.pool.get('ir.attachment') - for attachment in edi_document.get('__attachments', []): + for attachment in edi.get('__attachments', []): # check attachment data is non-empty and valid file_data = None try: @@ -614,19 +531,19 @@ class EDIMixin(object): self._edi_external_id(cr, uid, target, existing_id=ext_id_members['id'], existing_module=module, context=context) return target.id - def edi_import(self, cr, uid, edi_document, context=None): - """Imports a dict representing an edi.document into the system. + def edi_import(self, cr, uid, edi, context=None): + """Imports a dict representing an EDI document into the system. - :param dict edi_document: EDI document to import + :param dict edi: EDI document to import :return: the database ID of the imported record """ - assert self._name == edi_document.get('__import_model') or \ - ('__import_model' not in edi_document and self._name == edi_document.get('__model')), \ + assert self._name == edi.get('__import_model') or \ + ('__import_model' not in edi and self._name == edi.get('__model')), \ "EDI Document Model and current model do not match: '%s' (EDI) vs '%s' (current)." % \ - (edi_document['__model'], self._name) + (edi['__model'], self._name) # First check the record is now already known in the database, in which case it is ignored - ext_id_members = split_external_id(edi_document['__id']) + ext_id_members = split_external_id(edi['__id']) existing = self._edi_get_object_by_external_id(cr, uid, ext_id_members['full'], self._name, context=context) if existing: _logger.info("'%s' EDI Document with ID '%s' is already known, skipping import!", self._name, ext_id_members['full']) @@ -634,7 +551,7 @@ class EDIMixin(object): record_values = {} o2m_todo = {} # o2m values are processed after their parent already exists - for field_name, field_value in edi_document.iteritems(): + for field_name, field_value in edi.iteritems(): # skip metadata and empty fields if field_name.startswith('__') or field_value is None or field_value is False: continue @@ -679,7 +596,7 @@ class EDIMixin(object): dest_model.edi_import(cr, uid, o2m_line, context=context) # process the attachments, if any - self._edi_import_attachments(cr, uid, record_id, edi_document, context=context) + self._edi_import_attachments(cr, uid, record_id, edi, context=context) return record_id diff --git a/addons/edi/models/res_company.py b/addons/edi/models/res_company.py index 0ab6607cdad..d87827df99d 100644 --- a/addons/edi/models/res_company.py +++ b/addons/edi/models/res_company.py @@ -2,7 +2,7 @@ ############################################################################## # # OpenERP, Open Source Business Applications -# Copyright (c) 2011 OpenERP S.A.- You can view the ${object.state in ('draft', 'sent') and 'request for quotation' or 'order confirmation'} document and download it using the following link: -
- View Order -If you have any question, do not hesitate to contact us.
Thank you!
diff --git a/addons/purchase/test/process/edi_purchase_order.yml b/addons/purchase/test/process/edi_purchase_order.yml index d0e90f3788d..79f244a283d 100644 --- a/addons/purchase/test/process/edi_purchase_order.yml +++ b/addons/purchase/test/process/edi_purchase_order.yml @@ -27,16 +27,16 @@ - Then I export the purchase order via EDI - - !python {model: edi.document}: | - order_pool = self.pool.get('purchase.order') - order = order_pool.browse(cr, uid, ref("purchase_order_edi_1")) - token = self.export_edi(cr, uid, [order]) - assert token, 'Invalid EDI Token' - + !python {model: edi.edi}: | + import json + order_pool = self.pool.get('purchase.order') + order = order_pool.browse(cr, uid, ref("purchase_order_edi_1")) + edi_doc = self.generate_edi(cr, uid, [order]) + assert isinstance(json.loads(edi_doc)[0], dict), 'EDI doc should be a JSON dict' - Then I import a sample EDI document of a sale order - - !python {model: edi.document}: | + !python {model: edi.edi}: | purchase_order_pool = self.pool.get('purchase.order') edi_document = { "__id": "sale:724f93ec-ddd0-11e0-88ec-701a04e25543.sale_order_test", diff --git a/addons/sale/edi/sale_order.py b/addons/sale/edi/sale_order.py index edec15a86ae..0ca1b55898c 100644 --- a/addons/sale/edi/sale_order.py +++ b/addons/sale/edi/sale_order.py @@ -2,7 +2,7 @@ ############################################################################## # # OpenERP, Open Source Business Applications -# Copyright (c) 2011 OpenERP S.A.- You can view the ${object.state in ('draft', 'sent') and 'quotation' or 'order confirmation'} document, download it and pay online using the following link: -
- View Order - % if object.order_policy in ('prepaid','manual') and object.company_id.paypal_account and object.state not in ('draft', 'sent'): <% comp_name = quote(object.company_id.name) diff --git a/addons/sale/test/edi_sale_order.yml b/addons/sale/test/edi_sale_order.yml index 967aaa1337c..65a5887fe40 100644 --- a/addons/sale/test/edi_sale_order.yml +++ b/addons/sale/test/edi_sale_order.yml @@ -25,15 +25,16 @@ - Then I export the sale order via EDI - - !python {model: edi.document}: | + !python {model: edi.edi}: | + import json sale_order = self.pool.get('sale.order') so = sale_order.browse(cr, uid, ref("sale_order_edi_1")) - token = self.export_edi(cr, uid, [so]) - assert token, 'Invalid EDI Token' + edi_doc = self.generate_edi(cr, uid, [so]) + assert isinstance(json.loads(edi_doc)[0], dict), 'EDI doc should be a JSON dict' - Then I import a sample EDI document of a purchase order - - !python {model: edi.document}: | + !python {model: edi.edi}: | sale_order_pool = self.pool.get('sale.order') edi_document = { "__id": "purchase:5af1272e-dd26-11e0-b65e-701a04e25543.purchase_order_test", From c7757baf7768dee5699824f0ed9110c73c917288 Mon Sep 17 00:00:00 2001 From: Olivier Dony styling */
- white-space: pre-line;
- width: 90%;
-}
-.oe_edi_data_row .oe_edi_inner_note {
- /* prevent wide notes from disrupting layout due to styling */
- width: 25em;
-}
-.oe_edi_shade {
- background: #e8e8e8;
-}
-.oe_edi_company_name {
- text-transform: uppercase;
- font-weight: bold;
-}
-.oe_edi_address_from {
- float: left;
-}
-.oe_edi_address_to {
- float: right;
- margin-top: 25px;
- margin-bottom: 30px;
-}
-.oe_edi_company_block_title {
- width: 375px;
- margin: 0px;
- padding: 2px 14px;
- background-color: #252525;
- border-top-left-radius: 5px 5px;
- border-top-right-radius: 5px 5px;
- background-repeat: repeat no-repeat;
-}
-.oe_edi_company_block_title .oe_edi_company_name {
- margin: 0px;
- font-size: 1em;
- color: #FFF;
-}
-.oe_edi_company_block_body {
- width: 375px;
- margin: 0px;
- padding: 5px 14px;
- line-height: 16px;
- background-color: rgb(242, 242, 242);
-}
-.oe_edi_company_block_body p {
- color: #222;
- margin: 5px 0px;
-}
-.oe_edi_summary_label {
- float: left;
-}
-.oe_edi_summary_value {
- float: right;
-}
-
-
-/** Python code highlighting **/
-/* GeSHi (C) 2004 - 2007 Nigel McNie, 2007 - 2008 Benny Baumann
- (http://qbnz.com/highlighter/ and http://geshi.org/) */
-.python .de1, .python .de2 {font: normal normal 1em/1.2em monospace; margin:0; padding:0; background:none; vertical-align:top;}
-.python {font-family:monospace;}
-.python .imp {font-weight: bold; color: red;}
-.python li, .python .li1 {background: #ffffff; list-style: none;}
-.python .ln {width:1px;text-align:right;margin:0;padding:0 2px;vertical-align:top;}
-.python .li2 {background: #f8f8f8;}
-.python .kw1 {color: #ff7700;font-weight:bold;}
-.python .kw2 {color: #008000;}
-.python .kw3 {color: #dc143c;}
-.python .kw4 {color: #0000cd;}
-.python .co1 {color: #808080; font-style: italic;}
-.python .coMULTI {color: #808080; font-style: italic;}
-.python .es0 {color: #000099; font-weight: bold;}
-.python .br0 {color: black;}
-.python .sy0 {color: #66cc66;}
-.python .st0 {color: #483d8b;}
-.python .nu0 {color: #ff4500;}
-.python .me1 {color: black;}
-.python span.xtra { display:block; }
-.python ol { padding: 0px; }
diff --git a/addons/edi/static/src/img/pdf.png b/addons/edi/static/src/img/pdf.png
deleted file mode 100644
index d35dfd20c2db2622560c7f04dc174b52f223a753..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 6960
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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- OpenERP's Electronic Data Interchange documents are based on a generic and language
- independent JSON serialization of the document's attribute.
- It is usually very quick and straightforward to create a small plug-in for your preferred
- application that will be capable of importing any OpenERP EDI document.
- You can find out more details about how to do this and what the content of OpenERP EDI documents
- is like in the OpenERP documentation.
-
- To get started immediately, see is all it takes to use this EDI document in Python.
-
-
- - import urllib2, simplejson
- - edi_document = urllib2.urlopen('
').read()
- - document_data = simplejson.loads(edi_document)[0]
- - print "Amount: ", document_data['amount_total']
-
-
- You can download the raw EDI document here:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/addons/edi/static/src/xml/edi_account.xml b/addons/edi/static/src/xml/edi_account.xml
deleted file mode 100644
index 3ce1daebf93..00000000000
--- a/addons/edi/static/src/xml/edi_account.xml
+++ /dev/null
@@ -1,163 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Invoice :
-
-
- Description
- Date
- Your Reference
-
-
-
-
-
-
-
-
-
-
- Product Description
- Quantity
- Unit Price
- Discount
- Price
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Net Total:
-
-
-
-
-
-
-
-
-
-
- Taxes:
-
-
-
-
-
-
-
-
-
-
- Total:
-
-
-
-
-
-
-
-
-
-
- Tax
- Base Amount
- Amount
-
-
-
-
-
-
-
-
-
-
-
-
- Notes:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- You may directly pay this invoice online via Paypal's secure payment gateway:
-
-
-
-
-
-
-
-
-
-
-
- Please transfer to
- (postal address on the invoice header)
- using one of the following bank accounts. Be sure to mention the invoice
- reference on the transfer:
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/addons/edi/static/src/xml/edi_sale_purchase.xml b/addons/edi/static/src/xml/edi_sale_purchase.xml
deleted file mode 100644
index 8d6bab67761..00000000000
--- a/addons/edi/static/src/xml/edi_sale_purchase.xml
+++ /dev/null
@@ -1,169 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Quotation :
-
-
- Request for Quotation :
-
-
- Order :
-
-
-
-
- Your Reference
- Date
- Salesperson
- Payment terms
-
-
-
-
-
-
-
-
-
-
-
-
-
- Product Description
- Quantity
- Unit Price
- Discount(%)
- Price
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Net Total:
-
-
-
-
-
-
-
-
-
-
- Taxes:
-
-
-
-
-
-
-
-
-
-
- Total:
-
-
-
-
-
-
-
-
- Notes:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- You may directly pay this order online via Paypal's secure payment gateway:
-
-
-
-
-
-
-
-
-
-
-
- Please transfer to
- (postal address on the order header)
- using one of the following bank accounts. Be sure to mention the document
- reference on the transfer:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
From b42801595e48ab266a60ae6b9e77d73a1adb98fe Mon Sep 17 00:00:00 2001
From: Olivier Dony
Date: Wed, 17 Oct 2012 18:28:55 +0200
Subject: [PATCH 007/360] [IMP] product.uom: support name_create, allows EDI
import to work silently
bzr revid: odo@openerp.com-20121017162855-dkrr07bldsnc6f5s
---
addons/product/product.py | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/addons/product/product.py b/addons/product/product.py
index 6c6c5f2650c..9623a5f1248 100644
--- a/addons/product/product.py
+++ b/addons/product/product.py
@@ -100,6 +100,23 @@ class product_uom(osv.osv):
def _factor_inv_write(self, cursor, user, id, name, value, arg, context=None):
return self.write(cursor, user, id, {'factor': self._compute_factor_inv(value)}, context=context)
+ def name_create(self, cr, uid, name, context=None):
+ """ The UoM category and factor are required, so we'll have to add temporary values
+ for imported UoMs """
+ uom_categ = self.pool.get('product.uom.categ')
+ # look for the category based on the english name, i.e. no context on purpose!
+ # TODO: should find a way to have it translated but not created until actually used
+ categ_misc = 'Unsorted/Imported Units'
+ categ_id = uom_categ.search(cr, uid, [('name', '=', categ_misc)])
+ if categ_id:
+ categ_id = categ_id[0]
+ else:
+ categ_id, _ = uom_categ.name_create(cr, uid, categ_misc)
+ uom_id = self.create(cr, uid, {self._rec_name: name,
+ 'category_id': categ_id,
+ 'factor': 1})
+ return self.name_get(cr, uid, [uom_id], context=context)[0]
+
def create(self, cr, uid, data, context=None):
if 'factor_inv' in data:
if data['factor_inv'] <> 1:
From 48cfc1bb029407fce29d2ac31f7be474444da775 Mon Sep 17 00:00:00 2001
From: Olivier Dony
Date: Wed, 17 Oct 2012 18:29:37 +0200
Subject: [PATCH 008/360] [IMP] edi,account: fix EDI import of invoices, now
compatible again with 6.1, and correct wrt. partners import (vs old
addresses)
bzr revid: odo@openerp.com-20121017162937-fprd53n4ljo5bdim
---
addons/account/edi/invoice.py | 49 +++++++++++-------------
addons/account/test/test_edi_invoice.yml | 4 +-
addons/edi/models/edi.py | 13 +++++--
3 files changed, 33 insertions(+), 33 deletions(-)
diff --git a/addons/account/edi/invoice.py b/addons/account/edi/invoice.py
index 91747de4ea1..80a4b145054 100644
--- a/addons/account/edi/invoice.py
+++ b/addons/account/edi/invoice.py
@@ -2,7 +2,7 @@
##############################################################################
#
# OpenERP, Open Source Business Applications
-# Copyright (c) 2011 OpenERP S.A.
+# Copyright (c) 2011-2012 OpenERP S.A.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -100,8 +100,8 @@ class account_invoice(osv.osv, EDIMixin):
return tax_account
def _edi_invoice_account(self, cr, uid, partner_id, invoice_type, context=None):
- partner_pool = self.pool.get('res.partner')
- partner = partner_pool.browse(cr, uid, partner_id, context=context)
+ res_partner = self.pool.get('res.partner')
+ partner = res_partner.browse(cr, uid, partner_id, context=context)
if invoice_type in ('out_invoice', 'out_refund'):
invoice_account = partner.property_account_receivable
else:
@@ -125,36 +125,31 @@ class account_invoice(osv.osv, EDIMixin):
self._edi_requires_attributes(('company_id','company_address','type'), edi_document)
res_partner = self.pool.get('res.partner')
- _, src_company_name = edi_document.pop('company_id')
+ xid, company_name = edi_document.pop('company_id')
+ # Retrofit address info into a unified partner info (changed in v7 - used to keep them separate)
+ company_address_edi = edi_document.pop('company_address')
+ company_address_edi['name'] = company_name
+ company_address_edi['is_company'] = True
+ company_address_edi['__import_model'] = 'res.partner'
+ company_address_edi['__id'] = xid # override address ID, as of v7 they should be the same anyway
+ if company_address_edi.get('logo'):
+ company_address_edi['image'] = company_address_edi.pop('logo')
invoice_type = edi_document['type']
- partner_value = {}
- if invoice_type in ('out_invoice', 'out_refund'):
- partner_value.update({'customer': True})
- if invoice_type in ('in_invoice', 'in_refund'):
- partner_value.update({'supplier': True})
-
- # imported company_address = new partner address
- address_info = edi_document.pop('company_address')
- if '__import_model' not in address_info and '__model' not in address_info:
- # for pre-7.0 EDI format - address used to be a record of res.partner.address
- address_info['__import_model'] = 'res.partner'
- if 'name' not in address_info:
- # for pre-7.0 EDI format - address name was not required
- address_info['name'] = src_company_name
- address_info['type'] = 'invoice'
- address_info['is_company'] = True
- address_info.update(partner_value)
- address_id = res_partner.edi_import(cr, uid, address_info, context=context)
+ if invoice_type.startswith('out_'):
+ company_address_edi['customer'] = True
+ else:
+ company_address_edi['supplier'] = True
+ partner_id = res_partner.edi_import(cr, uid, company_address_edi, context=context)
# modify edi_document to refer to new partner
- partner_address = res_partner.browse(cr, uid, address_id, context=context)
- address_edi_m2o = self.edi_m2o(cr, uid, partner_address, context=context)
- edi_document['partner_id'] = address_edi_m2o
- edi_document.pop('partner_address', False) # ignored
+ partner = res_partner.browse(cr, uid, partner_id, context=context)
+ partner_edi_m2o = self.edi_m2o(cr, uid, partner, context=context)
+ edi_document['partner_id'] = partner_edi_m2o
+ edi_document.pop('partner_address', None) # ignored, that's supposed to be our own address!
- return address_id
+ return partner_id
def edi_import(self, cr, uid, edi_document, context=None):
""" During import, invoices will import the company that is provided in the invoice as
diff --git a/addons/account/test/test_edi_invoice.yml b/addons/account/test/test_edi_invoice.yml
index fea829e4ba3..5ca3b979f28 100644
--- a/addons/account/test/test_edi_invoice.yml
+++ b/addons/account/test/test_edi_invoice.yml
@@ -246,12 +246,12 @@
assert len(invoice_new.invoice_line) == 2, "invoice lines are not same"
for inv_line in invoice_new.invoice_line:
if inv_line.name == 'Basic PC':
- assert inv_line.uos_id.name == "Unit" , "uom is not same"
+ assert inv_line.uos_id.name == "PCE" , "uom is not same"
assert inv_line.price_unit == 10 , "price unit is not same"
assert inv_line.quantity == 1 , "product qty is not same"
assert inv_line.price_subtotal == 10, "price sub total is not same"
elif inv_line.name == 'Medium PC':
- assert inv_line.uos_id.name == "Unit" , "uom is not same"
+ assert inv_line.uos_id.name == "PCE" , "uom is not same"
assert inv_line.price_unit == 100 , "price unit is not same"
assert inv_line.quantity == 5 , "product qty is not same"
assert inv_line.price_subtotal == 500, "price sub total is not same"
diff --git a/addons/edi/models/edi.py b/addons/edi/models/edi.py
index 7c6fcc3ddfe..5edbca11d13 100644
--- a/addons/edi/models/edi.py
+++ b/addons/edi/models/edi.py
@@ -488,8 +488,10 @@ class EDIMixin(object):
if data_ids:
model = self.pool.get(model)
data = ir_model_data.browse(cr, uid, data_ids[0], context=context)
- result = model.browse(cr, uid, data.res_id, context=context)
- return result
+ if model.exists(cr, uid, [data.res_id]):
+ return model.browse(cr, uid, data.res_id, context=context)
+ # stale external-id, cleanup to allow re-import, as the corresponding record is gone
+ ir_model_data.unlink(cr, 1, [data_ids[0]])
def edi_import_relation(self, cr, uid, model, value, external_id, context=None):
"""Imports a M2O/M2M relation EDI specification ``[external_id,value]`` for the
@@ -503,6 +505,10 @@ class EDIMixin(object):
* If previous steps gave no result, create a new record with the given
value in the target model, assign it the given external_id, and return
the new database ID
+
+ :param str value: display name of the record to import
+ :param str external_id: fully-qualified external ID of the record
+ :return: database id of newly-imported or pre-existing record
"""
_logger.debug("%s: Importing EDI relationship [%r,%r]", model, external_id, value)
target = self._edi_get_object_by_external_id(cr, uid, external_id, model, context=context)
@@ -517,8 +523,7 @@ class EDIMixin(object):
self._name, external_id, value)
# also need_new_ext_id here, but already been set above
model = self.pool.get(model)
- # should use name_create() but e.g. res.partner won't allow it at the moment
- res_id = model.create(cr, uid, {model._rec_name: value}, context=context)
+ res_id, _ = model.name_create(cr, uid, value, context=context)
target = model.browse(cr, uid, res_id, context=context)
if need_new_ext_id:
ext_id_members = split_external_id(external_id)
From e0fcab1f620affb1a69308cdf215c9a90ab864a6 Mon Sep 17 00:00:00 2001
From: Olivier Dony
Date: Thu, 18 Oct 2012 17:21:29 +0200
Subject: [PATCH 009/360] [FIX] edi,sale,purchase: fix EDI import of SO/PO, now
compatible again with 6.1, and correct wrt. partners import (vs old
addresses)
bzr revid: odo@openerp.com-20121018152129-s22uozt77s37urp1
---
addons/account/test/test_edi_invoice.yml | 2 +
addons/purchase/edi/purchase_order.py | 32 +++--
.../test/process/edi_purchase_order.yml | 134 +++++++++++++++---
addons/sale/edi/sale_order.py | 38 ++---
addons/sale/test/edi_sale_order.yml | 131 +++++++++++++++--
5 files changed, 274 insertions(+), 63 deletions(-)
diff --git a/addons/account/test/test_edi_invoice.yml b/addons/account/test/test_edi_invoice.yml
index 5ca3b979f28..2225ae5ba9b 100644
--- a/addons/account/test/test_edi_invoice.yml
+++ b/addons/account/test/test_edi_invoice.yml
@@ -126,6 +126,7 @@
invoice_new = self.browse(cr, uid, invoice_id)
# check bank info on partner
+ assert invoice_new.partner_id.supplier, "Imported partner should be a supplier, as we just imported the document as a supplier invoice"
assert len(invoice_new.partner_id.bank_ids) == 1, "Expected 1 bank entry related to partner"
bank_info = invoice_new.partner_id.bank_ids[0]
assert bank_info.acc_number == "Sample bank: 70-123465789-156113", 'Expected "Sample bank: 70-123465789-156113", got %s' % bank_info.acc_number
@@ -233,6 +234,7 @@
invoice_new = self.browse(cr, uid, invoice_id)
# check bank info on partner
+ assert invoice_new.partner_id.supplier, "Imported partner should be a supplier, as we just imported the document as a supplier invoice"
assert len(invoice_new.partner_id.bank_ids) == 1, "Expected 1 bank entry related to partner"
bank_info = invoice_new.partner_id.bank_ids[0]
assert bank_info.acc_number == "Sample bank: 123465789-156113", 'Expected "Sample bank: 123465789-156113", got %s' % bank_info.acc_number
diff --git a/addons/purchase/edi/purchase_order.py b/addons/purchase/edi/purchase_order.py
index f44c16eb0d9..cdb4c0ca73d 100644
--- a/addons/purchase/edi/purchase_order.py
+++ b/addons/purchase/edi/purchase_order.py
@@ -91,22 +91,26 @@ class purchase_order(osv.osv, EDIMixin):
# the desired company among the user's allowed companies
self._edi_requires_attributes(('company_id','company_address'), edi_document)
- res_partner_obj = self.pool.get('res.partner')
+ res_partner = self.pool.get('res.partner')
- # imported company_address = new partner address
- _, src_company_name = edi_document.pop('company_id')
- address_info = edi_document.pop('company_address')
- address_info['customer'] = True
- if 'name' not in address_info:
- address_info['name'] = src_company_name
- address_id = res_partner_obj.edi_import(cr, uid, address_info, context=context)
+ xid, company_name = edi_document.pop('company_id')
+ # Retrofit address info into a unified partner info (changed in v7 - used to keep them separate)
+ company_address_edi = edi_document.pop('company_address')
+ company_address_edi['name'] = company_name
+ company_address_edi['is_company'] = True
+ company_address_edi['__import_model'] = 'res.partner'
+ company_address_edi['__id'] = xid # override address ID, as of v7 they should be the same anyway
+ if company_address_edi.get('logo'):
+ company_address_edi['image'] = company_address_edi.pop('logo')
+ company_address_edi['supplier'] = True
+ partner_id = res_partner.edi_import(cr, uid, company_address_edi, context=context)
- # modify edi_document to refer to new partner/address
- partner_address = res_partner_obj.browse(cr, uid, address_id, context=context)
- edi_document.pop('partner_address', False) # ignored
- edi_document['partner_id'] = self.edi_m2o(cr, uid, partner_address, context=context)
-
- return address_id
+ # modify edi_document to refer to new partner
+ partner = res_partner.browse(cr, uid, partner_id, context=context)
+ partner_edi_m2o = self.edi_m2o(cr, uid, partner, context=context)
+ edi_document['partner_id'] = partner_edi_m2o
+ edi_document.pop('partner_address', None) # ignored, that's supposed to be our own address!
+ return partner_id
def _edi_get_pricelist(self, cr, uid, partner_id, currency, context=None):
# TODO: refactor into common place for purchase/sale, e.g. into product module
diff --git a/addons/purchase/test/process/edi_purchase_order.yml b/addons/purchase/test/process/edi_purchase_order.yml
index 79f244a283d..8e34606afb4 100644
--- a/addons/purchase/test/process/edi_purchase_order.yml
+++ b/addons/purchase/test/process/edi_purchase_order.yml
@@ -34,29 +34,29 @@
edi_doc = self.generate_edi(cr, uid, [order])
assert isinstance(json.loads(edi_doc)[0], dict), 'EDI doc should be a JSON dict'
-
- Then I import a sample EDI document of a sale order
+ Then I import a sample EDI document of a sale order (v7.0)
-
!python {model: edi.edi}: |
purchase_order_pool = self.pool.get('purchase.order')
edi_document = {
- "__id": "sale:724f93ec-ddd0-11e0-88ec-701a04e25543.sale_order_test",
+ "__id": "sale:724f9v70-dv70-1v70-8v70-701a04e25v70.sale_order_test",
"__module": "sale",
"__model": "sale.order",
"__import_module": "purchase",
"__import_model": "purchase.order",
- "__version": [6,1,0],
+ "__version": [7,0,0],
"name": "SO008",
"currency": {
- "__id": "base:724f93ec-ddd0-11e0-88ec-701a04e25543.EUR",
+ "__id": "base:724f9v70-dv70-1v70-8v70-701a04e25v70.EUR",
"__module": "base",
"__model": "res.currency",
"code": "EUR",
"symbol": "€",
},
"date_order": "2011-09-13",
- "partner_id": ["sale:724f93ec-ddd0-11e0-88ec-701a04e25543.res_partner_test22", "Junjun wala"],
+ "partner_id": ["sale:724f9v70-dv70-1v70-8v70-701a04e25v70.res_partner_test22", "Junjun wala"],
"partner_address": {
- "__id": "base:724f93ec-ddd0-11e0-88ec-701a04e25543.res_partner_address_7wdsjasdjh",
+ "__id": "base:724f9v70-dv70-1v70-8v70-701a04e25v70.res_partner_address_7wdsjasdjh",
"__module": "base",
"__model": "res.partner",
"phone": "(+32).81.81.37.00",
@@ -65,48 +65,48 @@
"zip": "1367",
"country_id": ["base:5af1272e-dd26-11e0-b65e-701a04e25543.be", "Belgium"],
},
- "company_id": ["base:724f93ec-ddd0-11e0-88ec-701a04e25543.main_company", "Supplier S.A."],
+ "company_id": ["base:724f9v70-dv70-1v70-8v70-701a04e25v70.main_company", "Supplier S.A."],
"company_address": {
- "__id": "base:724f93ec-ddd0-11e0-88ec-701a04e25543.main_address",
+ "__id": "base:724f9v70-dv70-1v70-8v70-701a04e25v70.main_address",
"__module": "base",
"__model": "res.partner",
"city": "Gerompont",
"zip": "1367",
- "country_id": ["base:724f93ec-ddd0-11e0-88ec-701a04e25543.be", "Belgium"],
+ "country_id": ["base:724f9v70-dv70-1v70-8v70-701a04e25v70.be", "Belgium"],
"phone": "(+32).81.81.37.00",
"street": "Chaussee de Namur 40",
"street2": "mailbox 34",
"bank_ids": [
- ["base:724f93ec-ddd0-11e0-88ec-701a04e25543.res_partner_bank-XiwqnxKWzGbp","Guys bank: 123477777-156113"]
+ ["base:724f9v70-dv70-1v70-8v70-701a04e25v70.res_partner_bank-XiwqnxKWzGbp","Guys bank: 123477777-156113"]
],
},
"order_line": [{
- "__id": "sale:724f93ec-ddd0-11e0-88ec-701a04e25543.sale_order_line-LXEqeuI-SSP0",
+ "__id": "sale:724f9v70-dv70-1v70-8v70-701a04e25v70.sale_order_line-LXEqeuI-SSP0",
"__module": "sale",
"__model": "sale.order.line",
"__import_module": "purchase",
"__import_model": "purchase.order.line",
"name": "PC Assemble SC234",
- "product_uom": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_uom_unit", "Unit"],
+ "product_uom": ["product:724f9v70-dv70-1v70-8v70-701a04e25v70.product_uom_unit", "Unit"],
"product_qty": 1.0,
"date_planned": "2011-09-30",
"sequence": 10,
"price_unit": 150.0,
- "product_id": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_product_3", "[PCSC234] PC Assemble SC234"],
+ "product_id": ["product:724f9v70-dv70-1v70-8v70-701a04e25v70.product_product_3", "[PCSC234] PC Assemble SC234"],
},
{
- "__id": "sale:724f93ec-ddd0-11e0-88ec-701a04e25543.sale_order_line-LXEqeadasdad",
+ "__id": "sale:724f9v70-dv70-1v70-8v70-701a04e25v70.sale_order_line-LXEqeadasdad",
"__module": "sale",
"__model": "sale.order.line",
"__import_module": "purchase",
"__import_model": "purchase.order.line",
"name": "PC on Demand",
- "product_uom": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_uom_unit", "Unit"],
+ "product_uom": ["product:724f9v70-dv70-1v70-8v70-701a04e25v70.product_uom_unit", "Unit"],
"product_qty": 10.0,
"sequence": 11,
"date_planned": "2011-09-15",
"price_unit": 20.0,
- "product_id": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_product_5", "[PC-DEM] PC on Demand"],
+ "product_id": ["product:724f9v70-dv70-1v70-8v70-701a04e25v70.product_product_5", "[PC-DEM] PC on Demand"],
}],
}
new_purchase_order_id = purchase_order_pool.edi_import(cr, uid, edi_document, context=context)
@@ -114,6 +114,7 @@
order_new = purchase_order_pool.browse(cr, uid, new_purchase_order_id)
# check bank info on partner
+ assert order_new.partner_id.supplier, "Imported partner should be a supplier, as we just imported the document as a purchase order"
assert len(order_new.partner_id.bank_ids) == 1, "Expected 1 bank entry related to partner"
bank_info = order_new.partner_id.bank_ids[0]
assert bank_info.acc_number == "Guys bank: 123477777-156113", 'Expected "Guys bank: 123477777-156113", got %s' % bank_info.acc_number
@@ -133,3 +134,104 @@
assert purchase_line.product_qty == 10 , "product qty is not same"
else:
raise AssertionError('unknown order line: %s' % purchase_line)
+-
+ "Then I import a sample EDI document of a sale order (v6.1 - to test backwards compatibility)"
+-
+ !python {model: edi.edi}: |
+ purchase_order_pool = self.pool.get('purchase.order')
+ edi_document = {
+ "__id": "sale:724f93ec-ddd0-11e0-88ec-701a04e25543.sale_order_test",
+ "__module": "sale",
+ "__model": "sale.order",
+ "__import_module": "purchase",
+ "__import_model": "purchase.order",
+ "__version": [6,1,0],
+ "name": "SO08v61",
+ "currency": {
+ "__id": "base:724f93ec-ddd0-11e0-88ec-701a04e25543.EUR",
+ "__module": "base",
+ "__model": "res.currency",
+ "code": "EUR",
+ "symbol": "€",
+ },
+ "date_order": "2011-09-13",
+ "partner_id": ["sale:724f93ec-ddd0-11e0-88ec-701a04e25543.res_partner_test22", "Junjun wala"],
+ "partner_address": {
+ "__id": "base:724f93ec-ddd0-11e0-88ec-701a04e25543.res_partner_address_7wdsjasdjh",
+ "__module": "base",
+ "__model": "res.partner.address",
+ "phone": "(+32).81.81.37.00",
+ "street": "Chaussee de Namur 40",
+ "city": "Gerompont",
+ "zip": "1367",
+ "country_id": ["base:5af1272e-dd26-11e0-b65e-701a04e25543.be", "Belgium"],
+ },
+ "company_id": ["base:724f93ec-ddd0-11e0-88ec-701a04e25543.main_company", "Supplier S.A."],
+ "company_address": {
+ "__id": "base:724f93ec-ddd0-11e0-88ec-701a04e25543.main_address",
+ "__module": "base",
+ "__model": "res.partner.address",
+ "city": "Gerompont",
+ "zip": "1367",
+ "country_id": ["base:724f93ec-ddd0-11e0-88ec-701a04e25543.be", "Belgium"],
+ "phone": "(+32).81.81.37.00",
+ "street": "Chaussee de Namur 40",
+ "street2": "mailbox 34",
+ "bank_ids": [
+ ["base:724f93ec-ddd0-11e0-88ec-701a04e25543.res_partner_bank-XiwqnxKWzGbp","Another bank: 123477700-156113"]
+ ],
+ },
+ "order_line": [{
+ "__id": "sale:724f93ec-ddd0-11e0-88ec-701a04e25543.sale_order_line-LXEqeuI-SSP0",
+ "__module": "sale",
+ "__model": "sale.order.line",
+ "__import_module": "purchase",
+ "__import_model": "purchase.order.line",
+ "name": "Basic PC",
+ "product_uom": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_uom_unit", "PCE"],
+ "product_qty": 1.0,
+ "date_planned": "2011-09-30",
+ "sequence": 10,
+ "price_unit": 150.0,
+ "product_id": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_product_pc1", "[PC1] Basic PC"],
+ },
+ {
+ "__id": "sale:724f93ec-ddd0-11e0-88ec-701a04e25543.sale_order_line-LXEqeadasdad",
+ "__module": "sale",
+ "__model": "sale.order.line",
+ "__import_module": "purchase",
+ "__import_model": "purchase.order.line",
+ "name": "Medium PC",
+ "product_uom": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_uom_unit", "PCE"],
+ "product_qty": 10.0,
+ "sequence": 11,
+ "date_planned": "2011-09-15",
+ "price_unit": 20.0,
+ "product_id": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_product_pc3", "[PC3] Medium PC"],
+ }],
+ }
+ new_purchase_order_id = purchase_order_pool.edi_import(cr, uid, edi_document, context=context)
+ assert new_purchase_order_id, 'Purchase order import failed'
+ order_new = purchase_order_pool.browse(cr, uid, new_purchase_order_id)
+
+ # check bank info on partner
+ assert order_new.partner_id.supplier, "Imported partner should be a supplier, as we just imported the document as a purchase order"
+ assert len(order_new.partner_id.bank_ids) == 1, "Expected 1 bank entry related to partner"
+ bank_info = order_new.partner_id.bank_ids[0]
+ assert bank_info.acc_number == "Another bank: 123477700-156113", 'Expected "Another bank: 123477700-156113", got %s' % bank_info.acc_number
+
+ assert order_new.pricelist_id.name == 'Default Purchase Pricelist' , "Default Purchase Pricelist was not automatically assigned"
+ assert order_new.amount_total == 350, "Amount total is not same"
+ assert order_new.amount_untaxed == 350, "untaxed amount is not same"
+ assert len(order_new.order_line) == 2, "Purchase order lines number mismatch"
+ for purchase_line in order_new.order_line:
+ if purchase_line.name == 'Basic PC':
+ assert purchase_line.product_uom.name == "PCE" , "uom is not same"
+ assert purchase_line.price_unit == 150 , "unit price is not same, got %s, expected 150"%(purchase_line.price_unit,)
+ assert purchase_line.product_qty == 1 , "product qty is not same"
+ elif purchase_line.name == 'Medium PC':
+ assert purchase_line.product_uom.name == "PCE" , "uom is not same"
+ assert purchase_line.price_unit == 20 , "unit price is not same, got %s, expected 20"%(purchase_line.price_unit,)
+ assert purchase_line.product_qty == 10 , "product qty is not same"
+ else:
+ raise AssertionError('unknown order line: %s' % purchase_line)
\ No newline at end of file
diff --git a/addons/sale/edi/sale_order.py b/addons/sale/edi/sale_order.py
index 0ca1b55898c..15389f1481c 100644
--- a/addons/sale/edi/sale_order.py
+++ b/addons/sale/edi/sale_order.py
@@ -88,34 +88,36 @@ class sale_order(osv.osv, EDIMixin):
edi_doc_list.append(edi_doc)
return edi_doc_list
-
def _edi_import_company(self, cr, uid, edi_document, context=None):
# TODO: for multi-company setups, we currently import the document in the
# user's current company, but we should perhaps foresee a way to select
# the desired company among the user's allowed companies
self._edi_requires_attributes(('company_id','company_address'), edi_document)
- res_partner_obj = self.pool.get('res.partner')
+ res_partner = self.pool.get('res.partner')
- # imported company_address = new partner address
- _, src_company_name = edi_document.pop('company_id')
+ xid, company_name = edi_document.pop('company_id')
+ # Retrofit address info into a unified partner info (changed in v7 - used to keep them separate)
+ company_address_edi = edi_document.pop('company_address')
+ company_address_edi['name'] = company_name
+ company_address_edi['is_company'] = True
+ company_address_edi['__import_model'] = 'res.partner'
+ company_address_edi['__id'] = xid # override address ID, as of v7 they should be the same anyway
+ if company_address_edi.get('logo'):
+ company_address_edi['image'] = company_address_edi.pop('logo')
+ company_address_edi['customer'] = True
+ partner_id = res_partner.edi_import(cr, uid, company_address_edi, context=context)
- address_info = edi_document.pop('company_address')
- address_info['supplier'] = True
- if 'name' not in address_info:
- address_info['name'] = src_company_name
+ # modify edi_document to refer to new partner
+ partner = res_partner.browse(cr, uid, partner_id, context=context)
+ partner_edi_m2o = self.edi_m2o(cr, uid, partner, context=context)
+ edi_document['partner_id'] = partner_edi_m2o
+ edi_document['partner_invoice_id'] = partner_edi_m2o
+ edi_document['partner_shipping_id'] = partner_edi_m2o
- address_id = res_partner_obj.edi_import(cr, uid, address_info, context=context)
+ edi_document.pop('partner_address', None) # ignored, that's supposed to be our own address!
+ return partner_id
- # modify edi_document to refer to new partner/address
- partner_address = res_partner_obj.browse(cr, uid, address_id, context=context)
- edi_document.pop('partner_address', False) # ignored
- address_edi_m2o = self.edi_m2o(cr, uid, partner_address, context=context)
- edi_document['partner_id'] = address_edi_m2o
- edi_document['partner_invoice_id'] = address_edi_m2o
- edi_document['partner_shipping_id'] = address_edi_m2o
-
- return address_id
def _edi_get_pricelist(self, cr, uid, partner_id, currency, context=None):
# TODO: refactor into common place for purchase/sale, e.g. into product module
diff --git a/addons/sale/test/edi_sale_order.yml b/addons/sale/test/edi_sale_order.yml
index 65a5887fe40..2db03cadf59 100644
--- a/addons/sale/test/edi_sale_order.yml
+++ b/addons/sale/test/edi_sale_order.yml
@@ -32,64 +32,64 @@
edi_doc = self.generate_edi(cr, uid, [so])
assert isinstance(json.loads(edi_doc)[0], dict), 'EDI doc should be a JSON dict'
-
- Then I import a sample EDI document of a purchase order
+ "Then I import a sample EDI document of a purchase order (v7.0)"
-
!python {model: edi.edi}: |
sale_order_pool = self.pool.get('sale.order')
edi_document = {
- "__id": "purchase:5af1272e-dd26-11e0-b65e-701a04e25543.purchase_order_test",
+ "__id": "purchase:5af12v70-dv70-1v70-bv70-701a04e25v70.purchase_order_test",
"__module": "purchase",
"__model": "purchase.order",
"__import_module": "sale",
"__import_model": "sale.order",
- "__version": [6,1,0],
+ "__version": [7,0,0],
"name": "PO00011",
"date_order": "2011-09-12",
"currency": {
- "__id": "base:5af1272e-dd26-11e0-b65e-701a04e25543.EUR",
+ "__id": "base:5af12v70-dv70-1v70-bv70-701a04e25v70.EUR",
"__module": "base",
"__model": "res.currency",
"code": "EUR",
"symbol": "€",
},
- "company_id": ["base:5af1272e-dd26-11e0-b65e-701a04e25543.main_company", "Client S.A."],
+ "company_id": ["base:5af12v70-dv70-1v70-bv70-701a04e25v70.main_company", "Client S.A."],
"company_address": {
- "__id": "base:5af1272e-dd26-11e0-b65e-701a04e25543.some_address",
+ "__id": "base:5af12v70-dv70-1v70-bv70-701a04e25v70.some_address",
"__module": "base",
"__model": "res.partner",
"phone": "(+32).81.81.37.00",
"street": "Chaussee de Namur 40",
"city": "Gerompont",
"zip": "1367",
- "country_id": ["base:5af1272e-dd26-11e0-b65e-701a04e25543.be", "Belgium"],
+ "country_id": ["base:5af12v70-dv70-1v70-bv70-701a04e25v70.be", "Belgium"],
"bank_ids": [
- ["base:5af1272e-dd26-11e0-b65e-701a04e25543.res_partner_bank-adaWadsadasdDJzGbp","Ladies bank: 032465789-156113"]
+ ["base:5af12v70-dv70-1v70-bv70-701a04e25v70.res_partner_bank-adaWadsadasdDJzGbp","Ladies bank: 032465789-156113"]
],
},
- "partner_id": ["purchase:5af1272e-dd26-11e0-b65e-701a04e25543.res_partner_test20", "jones white"],
+ "partner_id": ["purchase:5af12v70-dv70-1v70-bv70-701a04e25v70.res_partner_test20", "jones white"],
"order_line": [{
- "__id": "purchase:5af1272e-dd26-11e0-b65e-701a04e25543.purchase_order_line-AlhsVDZGoKvJ",
+ "__id": "purchase:5af12v70-dv70-1v70-bv70-701a04e25v70.purchase_order_line-AlhsVDZGoKvJ",
"__module": "purchase",
"__model": "purchase.order.line",
"__import_module": "sale",
"__import_model": "sale.order.line",
"name": "PC Assemble SC234",
"price_unit": 150.0,
- "product_id": ["product:5af1272e-dd26-11e0-b65e-701a04e25543.product_product_3", "[PCSC234] PC Assemble SC234"],
+ "product_id": ["product:5af12v70-dv70-1v70-bv70-701a04e25v70.product_product_3", "[PCSC234] PC Assemble SC234"],
"product_qty": 1.0,
- "product_uom": ["product:5af1272e-dd26-11e0-b65e-701a04e25543.product_uom_unit", "Unit"],
+ "product_uom": ["product:5af12v70-dv70-1v70-bv70-701a04e25v70.product_uom_unit", "Unit"],
},
{
- "__id": "purchase:5af1272e-dd26-11e0-b65e-701a04e25543.purchase_order_line-Alsads33e",
+ "__id": "purchase:5af12v70-dv70-1v70-bv70-701a04e25v70.purchase_order_line-Alsads33e",
"__module": "purchase",
"__model": "purchase.order.line",
"__import_module": "sale",
"__import_model": "sale.order.line",
"name": "PC on Demand",
"price_unit": 100.0,
- "product_id": ["product:5af1272e-dd26-11e0-b65e-701a04e25543.product_product_5", "[PC-DEM] PC on Demand"],
+ "product_id": ["product:5af12v70-dv70-1v70-bv70-701a04e25v70.product_product_5", "[PC-DEM] PC on Demand"],
"product_qty": 2.0,
- "product_uom": ["product:5af1272e-dd26-11e0-b65e-701a04e25543.product_uom_unit", "Unit"],
+ "product_uom": ["product:5af12v70-dv70-1v70-bv70-701a04e25v70.product_uom_unit", "Unit"],
}],
}
new_sale_order_id = sale_order_pool.edi_import(cr, uid, edi_document, context=context)
@@ -97,6 +97,7 @@
order_new = sale_order_pool.browse(cr, uid, new_sale_order_id)
# check bank info on partner
+ assert order_new.partner_id.customer, "Imported partner should be a customer, as we just imported the document as a sale order"
assert len(order_new.partner_id.bank_ids) == 1, "Expected 1 bank entry related to partner"
bank_info = order_new.partner_id.bank_ids[0]
assert bank_info.acc_number == "Ladies bank: 032465789-156113", 'Expected "Ladies bank: 032465789-156113", got %s' % bank_info.acc_number
@@ -116,3 +117,103 @@
assert sale_line.product_uom_qty == 2 , "product qty is not same"
else:
raise AssertionError('unknown order line: %s' % sale_line)
+-
+ "Then I import a sample EDI document of a purchase order (v6.1 - to test backwards compatibility)"
+-
+ !python {model: edi.document}: |
+ sale_order_pool = self.pool.get('sale.order')
+ edi_document = {
+ "__id": "purchase:5af1272e-dd26-11e0-b65e-701a04e25543.purchase_order_test",
+ "__module": "purchase",
+ "__model": "purchase.order",
+ "__import_module": "sale",
+ "__import_model": "sale.order",
+ "__version": [6,1,0],
+ "name": "PO00011-v61",
+ "date_order": "2011-09-12",
+ "currency": {
+ "__id": "base:5af1272e-dd26-11e0-b65e-701a04e25543.EUR",
+ "__module": "base",
+ "__model": "res.currency",
+ "code": "EUR",
+ "symbol": "€",
+ },
+ "company_id": ["base:5af1272e-dd26-11e0-b65e-701a04e25543.main_company", "Client S.A."],
+ "company_address": {
+ "__id": "base:5af1272e-dd26-11e0-b65e-701a04e25543.some_address",
+ "__module": "base",
+ "__model": "res.partner.address",
+ "phone": "(+32).81.81.37.00",
+ "street": "Chaussee de Namur 40",
+ "city": "Gerompont",
+ "zip": "1367",
+ "country_id": ["base:5af1272e-dd26-11e0-b65e-701a04e25543.be", "Belgium"],
+ "bank_ids": [
+ ["base:5af1272e-dd26-11e0-b65e-701a04e25543.res_partner_bank-adaWadsadasdDJzGbp","Ladies bank: 032465789-156113"]
+ ],
+ },
+ "partner_id": ["purchase:5af1272e-dd26-11e0-b65e-701a04e25543.res_partner_test20", "jones white"],
+ "partner_address": {
+ "__id": "base:5af1272e-dd26-11e0-b65e-701a04e25543.res_partner_address_7wdsjasdjh",
+ "__module": "base",
+ "__model": "res.partner.address",
+ "phone": "(+32).81.81.37.00",
+ "street": "Chaussee de Namur 40",
+ "city": "Gerompont",
+ "zip": "1367",
+ "country_id": ["base:5af1272e-dd26-11e0-b65e-701a04e25543.be", "Belgium"],
+ },
+ "order_line": [{
+ "__id": "purchase:5af1272e-dd26-11e0-b65e-701a04e25543.purchase_order_line-AlhsVDZGoKvJ",
+ "__module": "purchase",
+ "__model": "purchase.order.line",
+ "__import_module": "sale",
+ "__import_model": "sale.order.line",
+ "name": "Basic PC",
+ "date_planned": "2011-09-30",
+ "price_unit": 150.0,
+ "product_id": ["product:5af1272e-dd26-11e0-b65e-701a04e25543.product_product_pc1", "[PC1] Basic PC"],
+ "product_qty": 1.0,
+ "product_uom": ["product:5af1272e-dd26-11e0-b65e-701a04e25543.product_uom_unit", "PCE"],
+ },
+ {
+ "__id": "purchase:5af1272e-dd26-11e0-b65e-701a04e25543.purchase_order_line-Alsads33e",
+ "__module": "purchase",
+ "__model": "purchase.order.line",
+ "__import_module": "sale",
+ "__import_model": "sale.order.line",
+ "name": "Medium PC",
+ "date_planned": "2011-09-15",
+ "price_unit": 100.0,
+ "product_id": ["product:5af1272e-dd26-11e0-b65e-701a04e25543.product_product_pc3", "[PC3] Medium PC"],
+ "product_qty": 2.0,
+ "product_uom": ["product:5af1272e-dd26-11e0-b65e-701a04e25543.product_uom_unit", "PCE"],
+ }],
+ }
+ new_sale_order_id = sale_order_pool.edi_import(cr, uid, edi_document, context=context)
+ assert new_sale_order_id, 'Sale order import failed'
+ order_new = sale_order_pool.browse(cr, uid, new_sale_order_id)
+
+ # check bank info on partner
+ assert order_new.partner_id.customer, "Imported partner should be a customer, as we just imported the document as a sale order"
+ assert len(order_new.partner_id.bank_ids) == 1, "Expected 1 bank entry related to partner"
+ bank_info = order_new.partner_id.bank_ids[0]
+ assert bank_info.acc_number == "Ladies bank: 032465789-156113", 'Expected "Ladies bank: 032465789-156113", got %s' % bank_info.acc_number
+
+ assert order_new.pricelist_id.name == 'Public Pricelist' , "Public Price list was not automatically assigned"
+ assert order_new.amount_total == 350, "Amount total is wrong"
+ assert order_new.amount_untaxed == 350, "Untaxed amount is wrong"
+ assert len(order_new.order_line) == 2, "Sale order lines mismatch"
+ for sale_line in order_new.order_line:
+ if sale_line.name == 'Basic PC':
+ assert sale_line.delay == 18 , "incorrect delay: got %s, expected 18"%(sale_line.delay,)
+ assert sale_line.product_uom.name == "PCE" , "uom is not same"
+ assert sale_line.price_unit == 150 , "unit price is not same, got %s, expected 150"%(sale_line.price_unit,)
+ assert sale_line.product_uom_qty == 1 , "product qty is not same"
+ elif sale_line.name == 'Medium PC':
+ assert sale_line.delay == 3 , "incorrect delay: got %s, expected 3"%(sale_line.delay,)
+ assert sale_line.product_uom.name == "PCE" , "uom is not same"
+ assert sale_line.price_unit == 100 , "unit price is not same, got %s, expected 100"%(sale_line.price_unit,)
+ assert sale_line.product_uom_qty == 2 , "product qty is not same"
+ else:
+ raise AssertionError('unknown order line: %s' % sale_line)
\ No newline at end of file
From 46b2a78ea0fb825c3874080614f267c305fffc94 Mon Sep 17 00:00:00 2001
From: Olivier Dony
Date: Fri, 19 Oct 2012 12:39:45 +0200
Subject: [PATCH 010/360] [FIX] sale/edi: typo when adding 6.1 EDI testcase
bzr revid: odo@openerp.com-20121019103945-mor902q1qzyszhn0
---
addons/sale/test/edi_sale_order.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/sale/test/edi_sale_order.yml b/addons/sale/test/edi_sale_order.yml
index 2db03cadf59..78ff1c24da3 100644
--- a/addons/sale/test/edi_sale_order.yml
+++ b/addons/sale/test/edi_sale_order.yml
@@ -120,7 +120,7 @@
-
"Then I import a sample EDI document of a purchase order (v6.1 - to test backwards compatibility)"
-
- !python {model: edi.document}: |
+ !python {model: edi.edi}: |
sale_order_pool = self.pool.get('sale.order')
edi_document = {
"__id": "purchase:5af1272e-dd26-11e0-b65e-701a04e25543.purchase_order_test",
From ce1226a4a8ebb7e197214be54a8c3c8815e72d6b Mon Sep 17 00:00:00 2001
From: Olivier Dony
Date: Fri, 19 Oct 2012 13:11:00 +0200
Subject: [PATCH 011/360] [FIX] sale,edi: fix sale EDI import test + better
debug
- `delay` field on SO line moved to sale_stock, ignoring it for now
- avoid reusing the same bank account in 6.1 test, would cause a test
failure because the import would be skipped and the new partner
would have no bank account. Bank accounts should be unique in the
real world.
- log in DEBUG when a relationship record is skipped during EDI import
bzr revid: odo@openerp.com-20121019111100-dwf0crocqv43m3ij
---
addons/edi/models/edi.py | 3 +++
addons/sale/test/edi_sale_order.yml | 6 ++----
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/addons/edi/models/edi.py b/addons/edi/models/edi.py
index 5edbca11d13..48a04c2a94d 100644
--- a/addons/edi/models/edi.py
+++ b/addons/edi/models/edi.py
@@ -525,6 +525,9 @@ class EDIMixin(object):
model = self.pool.get(model)
res_id, _ = model.name_create(cr, uid, value, context=context)
target = model.browse(cr, uid, res_id, context=context)
+ else:
+ _logger.debug("%s: Importing EDI relationship [%r,%r] - record already exists with ID %s, using it",
+ self._name, external_id, value, target.id)
if need_new_ext_id:
ext_id_members = split_external_id(external_id)
# module name is never used bare when creating ir.model.data entries, in order
diff --git a/addons/sale/test/edi_sale_order.yml b/addons/sale/test/edi_sale_order.yml
index 78ff1c24da3..24fdfdf2786 100644
--- a/addons/sale/test/edi_sale_order.yml
+++ b/addons/sale/test/edi_sale_order.yml
@@ -63,7 +63,7 @@
"zip": "1367",
"country_id": ["base:5af12v70-dv70-1v70-bv70-701a04e25v70.be", "Belgium"],
"bank_ids": [
- ["base:5af12v70-dv70-1v70-bv70-701a04e25v70.res_partner_bank-adaWadsadasdDJzGbp","Ladies bank: 032465789-156113"]
+ ["base:5af12v70-dv70-1v70-bv70-701a04e25v70.res_partner_bank-adaWadsadasdDJzGbp","Another bank: 032465700-156700"]
],
},
"partner_id": ["purchase:5af12v70-dv70-1v70-bv70-701a04e25v70.res_partner_test20", "jones white"],
@@ -100,7 +100,7 @@
assert order_new.partner_id.customer, "Imported partner should be a customer, as we just imported the document as a sale order"
assert len(order_new.partner_id.bank_ids) == 1, "Expected 1 bank entry related to partner"
bank_info = order_new.partner_id.bank_ids[0]
- assert bank_info.acc_number == "Ladies bank: 032465789-156113", 'Expected "Ladies bank: 032465789-156113", got %s' % bank_info.acc_number
+ assert bank_info.acc_number == "Another bank: 032465700-156700", 'Expected "Another bank: 032465700-156700", got %s' % bank_info.acc_number
assert order_new.pricelist_id.name == 'Public Pricelist' , "Public Price list was not automatically assigned"
assert order_new.amount_total == 350, "Amount total is wrong"
@@ -206,12 +206,10 @@
assert len(order_new.order_line) == 2, "Sale order lines mismatch"
for sale_line in order_new.order_line:
if sale_line.name == 'Basic PC':
- assert sale_line.delay == 18 , "incorrect delay: got %s, expected 18"%(sale_line.delay,)
assert sale_line.product_uom.name == "PCE" , "uom is not same"
assert sale_line.price_unit == 150 , "unit price is not same, got %s, expected 150"%(sale_line.price_unit,)
assert sale_line.product_uom_qty == 1 , "product qty is not same"
elif sale_line.name == 'Medium PC':
- assert sale_line.delay == 3 , "incorrect delay: got %s, expected 3"%(sale_line.delay,)
assert sale_line.product_uom.name == "PCE" , "uom is not same"
assert sale_line.price_unit == 100 , "unit price is not same, got %s, expected 100"%(sale_line.price_unit,)
assert sale_line.product_uom_qty == 2 , "product qty is not same"
From 67d4646d092c93f9f78d38f8364242ffacef5415 Mon Sep 17 00:00:00 2001
From: Olivier Dony
Date: Tue, 23 Oct 2012 13:42:14 +0200
Subject: [PATCH 012/360] [IMP] sale,purchase,invoice: attach PDF report to
`Send by mail` template
This is quite useful in general, and partly
compensates for partial EDI integration, e.g.
when the sender company does not have a portal
or any way for customers to see the document.
bzr revid: odo@openerp.com-20121023114214-8zpl96jxqvqxv4xw
---
addons/account/edi/invoice_action_data.xml | 4 ++++
addons/purchase/edi/purchase_order_action_data.xml | 4 ++++
addons/sale/edi/sale_order_action_data.xml | 4 ++++
3 files changed, 12 insertions(+)
diff --git a/addons/account/edi/invoice_action_data.xml b/addons/account/edi/invoice_action_data.xml
index 1ad023c3a60..b4fb6d07fcd 100644
--- a/addons/account/edi/invoice_action_data.xml
+++ b/addons/account/edi/invoice_action_data.xml
@@ -27,6 +27,8 @@
${object.partner_id.email or ''}
+
+ Invoice_${(object.number or '').replace('/','_')}_${object.state == 'draft' and 'draft' or ''}
@@ -42,7 +44,9 @@
% if object.origin:
Order reference: ${object.origin}
% endif
+ % if object.user_id:
Your contact: ${object.user_id.name}
+ % endif
% if object.company_id.paypal_account and object.type in ('out_invoice', 'in_refund'):
diff --git a/addons/purchase/edi/purchase_order_action_data.xml b/addons/purchase/edi/purchase_order_action_data.xml
index 02e7a1c1ae1..fa043b72ce9 100644
--- a/addons/purchase/edi/purchase_order_action_data.xml
+++ b/addons/purchase/edi/purchase_order_action_data.xml
@@ -24,6 +24,8 @@
${object.partner_id.email}
+
+ RFQ_${(object.name or '').replace('/','_')}
@@ -42,7 +44,9 @@
% if object.partner_ref:
Your reference: ${object.partner_ref}
% endif
+ % if object.user_id:
Your contact: ${object.validator.name}
+ % endif
diff --git a/addons/sale/edi/sale_order_action_data.xml b/addons/sale/edi/sale_order_action_data.xml
index 5ce35bf497d..009f575f00d 100644
--- a/addons/sale/edi/sale_order_action_data.xml
+++ b/addons/sale/edi/sale_order_action_data.xml
@@ -26,6 +26,8 @@
${object.partner_invoice_id.email}
+
+ ${(object.name or '').replace('/','_')}_${object.state == 'draft' and 'draft' or ''}
@@ -44,7 +46,9 @@
% if object.client_order_ref:
Your reference: ${object.client_order_ref}
% endif
+ % if object.user_id:
Your contact: ${object.user_id.name}
+ % endif
% if object.order_policy in ('prepaid','manual') and object.company_id.paypal_account and object.state not in ('draft', 'sent'):
From 6fadb453259697c10cf2aed0be33c41760dc246b Mon Sep 17 00:00:00 2001
From: Olivier Dony
Date: Tue, 23 Oct 2012 13:56:28 +0200
Subject: [PATCH 013/360] [IMP] sale,purchase,invoice: make `Send by mail`
action more robust to template/view deletion
bzr revid: odo@openerp.com-20121023115628-3wvxcrkgazhi9p75
---
addons/account/account_invoice.py | 24 ++++++++++++++----------
addons/account/edi/invoice.py | 1 -
addons/purchase/purchase.py | 25 ++++++++++++++-----------
addons/sale/sale.py | 23 +++++++++++++----------
4 files changed, 41 insertions(+), 32 deletions(-)
diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py
index 57f16473b82..15f000cb90e 100644
--- a/addons/account/account_invoice.py
+++ b/addons/account/account_invoice.py
@@ -390,28 +390,32 @@ class account_invoice(osv.osv):
'''
This function opens a window to compose an email, with the edi invoice template message loaded by default
'''
- mod_obj = self.pool.get('ir.model.data')
- template = mod_obj.get_object_reference(cr, uid, 'account', 'email_template_edi_invoice')
- template_id = template and template[1] or False
- res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')
- res_id = res and res[1] or False
+ assert len(ids) == 1, 'This option should only be used for a single id at a time.'
+ ir_model_data = self.pool.get('ir.model.data')
+ try:
+ template_id = ir_model_data.get_object_reference(cr, uid, 'account', 'email_template_edi_invoice')[1]
+ except ValueError:
+ template_id = False
+ try:
+ compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
+ except ValueError:
+ compose_form_id = False
ctx = dict(context)
ctx.update({
'default_model': 'account.invoice',
'default_res_id': ids[0],
- 'default_use_template': True,
+ 'default_use_template': bool(template_id),
'default_template_id': template_id,
})
return {
+ 'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'mail.compose.message',
- 'views': [(res_id, 'form')],
- 'view_id': res_id,
- 'type': 'ir.actions.act_window',
+ 'views': [(compose_form_id, 'form')],
+ 'view_id': compose_form_id,
'target': 'new',
'context': ctx,
- 'nodestroy': True,
}
def confirm_paid(self, cr, uid, ids, context=None):
diff --git a/addons/account/edi/invoice.py b/addons/account/edi/invoice.py
index 80a4b145054..3e27bc5ea99 100644
--- a/addons/account/edi/invoice.py
+++ b/addons/account/edi/invoice.py
@@ -148,7 +148,6 @@ class account_invoice(osv.osv, EDIMixin):
edi_document['partner_id'] = partner_edi_m2o
edi_document.pop('partner_address', None) # ignored, that's supposed to be our own address!
-
return partner_id
def edi_import(self, cr, uid, edi_document, context=None):
diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py
index db6ed8b80be..ed8cdb63cef 100644
--- a/addons/purchase/purchase.py
+++ b/addons/purchase/purchase.py
@@ -349,28 +349,31 @@ class purchase_order(osv.osv):
'''
This function opens a window to compose an email, with the edi purchase template message loaded by default
'''
- mod_obj = self.pool.get('ir.model.data')
- template = mod_obj.get_object_reference(cr, uid, 'purchase', 'email_template_edi_purchase')
- template_id = template and template[1] or False
- res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')
- res_id = res and res[1] or False
+ ir_model_data = self.pool.get('ir.model.data')
+ try:
+ template_id = ir_model_data.get_object_reference(cr, uid, 'purchase', 'email_template_edi_purchase')[1]
+ except ValueError:
+ template_id = False
+ try:
+ compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
+ except ValueError:
+ compose_form_id = False
ctx = dict(context)
ctx.update({
'default_model': 'purchase.order',
'default_res_id': ids[0],
- 'default_use_template': True,
+ 'default_use_template': bool(template_id),
'default_template_id': template_id,
- })
+ })
return {
+ 'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'mail.compose.message',
- 'views': [(res_id, 'form')],
- 'view_id': res_id,
- 'type': 'ir.actions.act_window',
+ 'views': [(compose_form_id, 'form')],
+ 'view_id': compose_form_id,
'target': 'new',
'context': ctx,
- 'nodestroy': True,
}
#TODO: implement messages system
diff --git a/addons/sale/sale.py b/addons/sale/sale.py
index a2ee88ab860..3c984b64349 100644
--- a/addons/sale/sale.py
+++ b/addons/sale/sale.py
@@ -618,29 +618,32 @@ class sale_order(osv.osv):
This function opens a window to compose an email, with the edi sale template message loaded by default
'''
assert len(ids) == 1, 'This option should only be used for a single id at a time.'
- mod_obj = self.pool.get('ir.model.data')
- template = mod_obj.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')
- template_id = template and template[1] or False
- res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')
- res_id = res and res[1] or False
+ ir_model_data = self.pool.get('ir.model.data')
+ try:
+ template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
+ except ValueError:
+ template_id = False
+ try:
+ compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
+ except ValueError:
+ compose_form_id = False
ctx = dict(context)
ctx.update({
'default_model': 'sale.order',
'default_res_id': ids[0],
- 'default_use_template': True,
+ 'default_use_template': bool(template_id),
'default_template_id': template_id,
'mark_so_as_sent': True
})
return {
+ 'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'mail.compose.message',
- 'views': [(res_id, 'form')],
- 'view_id': res_id,
- 'type': 'ir.actions.act_window',
+ 'views': [(compose_form_id, 'form')],
+ 'view_id': compose_form_id,
'target': 'new',
'context': ctx,
- 'nodestroy': True,
}
def action_done(self, cr, uid, ids, context=None):
From 6ea4181244579279fde119a79ac214531922870d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Tue, 23 Oct 2012 17:56:31 +0200
Subject: [PATCH 014/360] [FIX] mail: incoming emails are not correctly set
with email type.
bzr revid: tde@openerp.com-20121023155631-h7gbhrmgpg3mwj0f
---
addons/mail/mail_thread.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
index 04da686b49d..273e0edaa36 100644
--- a/addons/mail/mail_thread.py
+++ b/addons/mail/mail_thread.py
@@ -554,7 +554,7 @@ class mail_thread(osv.AbstractModel):
('file2', 'bytes')}
}
"""
- msg_dict = {}
+ msg_dict = {'type': 'email'}
if not isinstance(message, Message):
if isinstance(message, unicode):
# Warning: message_from_string doesn't always work correctly on unicode,
From a8da0fbef74cf71d934140c3b9a27b2846b1ebc9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Tue, 23 Oct 2012 17:57:10 +0200
Subject: [PATCH 015/360] [IMP] mail: res_partner override: incoming emails are
set as private, by setting thread_id to False. In message_post, this will set
model and res_id of the message to False, leading to a private message.
bzr revid: tde@openerp.com-20121023155710-j6ugemc60khdqk74
---
addons/mail/res_partner.py | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/addons/mail/res_partner.py b/addons/mail/res_partner.py
index 6b13fe6ad20..46b250328f8 100644
--- a/addons/mail/res_partner.py
+++ b/addons/mail/res_partner.py
@@ -42,4 +42,21 @@ class res_partner_mail(osv.Model):
'notification_email_send': lambda *args: 'comment'
}
+ def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification',
+ subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
+ """ Override related to res.partner. In case of email message, set it as
+ private:
+ - add the target partner in the message partner_ids
+ - set thread_id as None, because this will trigger the 'private'
+ aspect of the message (model=False, res_is=False)
+ """
+ if isinstance(thread_id, (list, tuple)):
+ thread_id = thread_id[0]
+ if type == 'email':
+ partner_ids = kwargs.get('partner_ids', [])
+ partner_ids.append(thread_id)
+ kwargs['partner_ids'] = partner_ids
+ return super(res_partner_mail, self).message_post(cr, uid, False, body=body, subject=subject,
+ type=type, subtype=subtype, parent_id=parent_id, attachments=attachments, context=context, **kwargs)
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
From 1cf9b2a60da502de27aa67e8318f41ad6692e5a9 Mon Sep 17 00:00:00 2001
From: Xavier Morel
Date: Wed, 24 Oct 2012 13:01:46 +0200
Subject: [PATCH 016/360] [REM] test_support controller
bzr revid: xmo@openerp.com-20121024110146-bzuf19sfptew0dn0
---
addons/web/test_support/__init__.py | 37 -----------------------
addons/web/test_support/controllers.py | 42 --------------------------
2 files changed, 79 deletions(-)
delete mode 100644 addons/web/test_support/__init__.py
delete mode 100644 addons/web/test_support/controllers.py
diff --git a/addons/web/test_support/__init__.py b/addons/web/test_support/__init__.py
deleted file mode 100644
index 59f6cc67ca2..00000000000
--- a/addons/web/test_support/__init__.py
+++ /dev/null
@@ -1,37 +0,0 @@
-# -*- coding: utf-8 -*-
-import xmlrpclib
-from ..common.openerplib.main import Connector
-
-execute_map = {}
-
-class TestConnector(Connector):
- def db_list_lang(self):
- return [('en_US', u'Test Language')]
-
- def common_authenticate(self, db, login, password, environment):
- return 87539319
-
- def common_login(self, db, login, password):
- return self.common_authenticate(db, login, password, {})
-
- def object_execute_kw(self, db, uid, password, model, method, args, kwargs):
- if model in execute_map and hasattr(execute_map[model], method):
- return getattr(execute_map[model], method)(*args, **kwargs)
-
- raise xmlrpclib.Fault({
- 'model': model,
- 'method': method,
- 'args': args,
- 'kwargs': kwargs
- }, '')
-
- def send(self, service_name, method, *args):
- method_name = '%s_%s' % (service_name, method)
- if hasattr(self, method_name):
- return getattr(self, method_name)(*args)
-
- raise xmlrpclib.Fault({
- 'service': service_name,
- 'method': method,
- 'args': args
- }, '')
diff --git a/addons/web/test_support/controllers.py b/addons/web/test_support/controllers.py
deleted file mode 100644
index f1013fbaaf8..00000000000
--- a/addons/web/test_support/controllers.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from ..common import http, nonliterals
-from ..controllers.main import Session
-
-UID = 87539319
-DB = 'test_db'
-LOGIN = 'test_login'
-PASSWORD = 'test_password'
-CONTEXT = {'lang': 'en_US', 'tz': 'UTC', 'uid': UID}
-
-def bind(session):
- session.bind(DB, UID, LOGIN, PASSWORD)
- session.context = CONTEXT
- session.build_connection().set_login_info(DB, LOGIN, PASSWORD, UID)
-
-class TestController(http.Controller):
- _cp_path = '/tests'
-
- @http.jsonrequest
- def add_nonliterals(self, req, domains, contexts):
- return {
- 'domains': [nonliterals.Domain(req.session, domain)
- for domain in domains],
- 'contexts': [nonliterals.Context(req.session, context)
- for context in contexts]
- }
-
-class TestSession(Session):
- _cp_path = '/web/session'
-
- def session_info(self, req):
- if not req.session._uid:
- bind(req.session)
-
- return {
- "session_id": req.session_id,
- "uid": req.session._uid,
- "context": CONTEXT,
- "db": req.session._db,
- "login": req.session._login,
- }
From 21b56902b7eb7ad4eabb2b3be97ca42384d12fdc Mon Sep 17 00:00:00 2001
From: Olivier Dony
Date: Wed, 24 Oct 2012 14:59:57 +0200
Subject: [PATCH 017/360] [FIX] sale/edi: paypal payment should be possible as
soon as SO is not draft
Most companies won't mind if customers pay too early.
Even if the amount is not 100% correct, it can be
adjusted later, and it's better than missing a
revenue opportunity.
bzr revid: odo@openerp.com-20121024125957-oo8r9zmazzdonhd7
---
addons/sale/edi/sale_order_action_data.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/sale/edi/sale_order_action_data.xml b/addons/sale/edi/sale_order_action_data.xml
index 009f575f00d..cfe5277d3ce 100644
--- a/addons/sale/edi/sale_order_action_data.xml
+++ b/addons/sale/edi/sale_order_action_data.xml
@@ -51,7 +51,7 @@
% endif
- % if object.order_policy in ('prepaid','manual') and object.company_id.paypal_account and object.state not in ('draft', 'sent'):
+ % if object.order_policy in ('prepaid','manual') and object.company_id.paypal_account and object.state != 'draft':
<%
comp_name = quote(object.company_id.name)
order_name = quote(object.name)
From 20c81b5e2fea00643f234ac83814ce73bae14d12 Mon Sep 17 00:00:00 2001
From: Olivier Dony
Date: Wed, 24 Oct 2012 15:15:54 +0200
Subject: [PATCH 018/360] [ADD] portal: added portal.payment.acquirer model
portal.payment.acquirer stores payment processor
options (formally called payment acquirers). Each
acquirer is just a name and an HTML form template,
used to render an HTML snippet that can be included
in views where a payment option should be displayed.
The aquirer model has a generic method for rendering
a complete block of HTML to be included in form views
directly (with matching CSS): render_payment_block().
This method takes a few parameters to figure out
the name/reference, amount, currency, etc. to pay.
bzr revid: odo@openerp.com-20121024131554-j01ucniecjmoz3jq
---
addons/portal/__init__.py | 2 +-
addons/portal/__openerp__.py | 2 +
addons/portal/acquirer.py | 96 ++++++++++++++++++++++
addons/portal/acquirer_view.xml | 67 +++++++++++++++
addons/portal/portal_data.xml | 24 ++++++
addons/portal/security/ir.model.access.csv | 2 +
addons/portal/static/src/css/portal.css | 60 ++++++++++++++
7 files changed, 252 insertions(+), 1 deletion(-)
create mode 100644 addons/portal/acquirer.py
create mode 100644 addons/portal/acquirer_view.xml
create mode 100644 addons/portal/static/src/css/portal.css
diff --git a/addons/portal/__init__.py b/addons/portal/__init__.py
index 2bfc6df0d96..0045a557a49 100644
--- a/addons/portal/__init__.py
+++ b/addons/portal/__init__.py
@@ -22,6 +22,6 @@
import portal
import mail_mail
import wizard
-
+import acquirer
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/portal/__openerp__.py b/addons/portal/__openerp__.py
index 66a736c6048..4f37a2205d7 100644
--- a/addons/portal/__openerp__.py
+++ b/addons/portal/__openerp__.py
@@ -49,8 +49,10 @@ very handy when used in combination with the module 'share'.
'portal_view.xml',
'wizard/portal_wizard_view.xml',
'wizard/share_wizard_view.xml',
+ 'acquirer_view.xml',
],
'demo': ['portal_demo.xml'],
+ 'css': ['static/src/css/portal.css'],
'installable': True,
}
diff --git a/addons/portal/acquirer.py b/addons/portal/acquirer.py
new file mode 100644
index 00000000000..922a341b679
--- /dev/null
+++ b/addons/portal/acquirer.py
@@ -0,0 +1,96 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Business Applications
+# Copyright (c) 2012-TODAY OpenERP S.A.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+import logging
+from urllib import quote as quote
+
+from openerp.osv import osv, fields
+from openerp.tools import ustr
+from openerp.tools.translate import _
+
+_logger = logging.getLogger(__name__)
+try:
+ from mako.template import Template as MakoTemplate
+except ImportError:
+ _logger.warning("payment_acquirer: mako templates not available, payment acquirer will not work!")
+
+
+class acquirer(osv.Model):
+ _name = 'portal.payment.acquirer'
+ _description = 'Online Payment Acquirer'
+
+ _columns = {
+ 'name': fields.char('Name', required=True),
+ 'form_template': fields.text('Payment form template (HTML)', translate=True),
+ 'visible': fields.boolean('Visible', help="Whether this payment acquirer is currently displayed in portal forms"),
+ }
+
+ _default = {
+ 'visible': True,
+ }
+
+ def render(self, cr, uid, id, object, reference, currency, amount, context=None, **kwargs):
+ """ Renders the form template of the given acquirer as a mako template """
+ if not isinstance(id, (int,long)):
+ id = id[0]
+ this = self.browse(cr, uid, id)
+ if context is None:
+ context = {}
+ try:
+ i18n_kind = _(object._description) # may fail to translate, but at least we try
+ template = ustr(this.form_template)
+ result = MakoTemplate(template).render_unicode(object=object,
+ reference=reference,
+ currency=currency,
+ amount=amount,
+ kind=i18n_kind,
+ quote=quote,
+ # context kw would clash with mako internals
+ ctx=context,
+ format_exceptions=True)
+ result = result.strip()
+ if result == u'False':
+ result = u''
+ return result
+ except Exception:
+ _logger.exception("failed to render mako template value for payment.acquirer %s: %r", this.name, template)
+ return
+
+ def _wrap_payment_block(self, cr, uid, html_block, context=None):
+ payment_header = _('Pay safely online:')
+ result = """
+ %s
+ %%s
+ """ % payment_header
+ return result % html_block
+
+ def render_payment_block(self, cr, uid, object, reference, currency, amount, context=None, **kwargs):
+ """ Renders all visible payment acquirer forms for the given rendering context, and
+ return them wrapped in an appropriate HTML block, ready for direct inclusion
+ in an OpenERP v7 form view """
+ acquirer_ids = self.search(cr, uid, [('visible', '=', True)])
+ if not acquirer_ids:
+ return
+ html_forms = []
+ for this in self.browse(cr, uid, acquirer_ids):
+ html_forms.append(this.render(object, reference, currency, amount, context=context, **kwargs))
+ html_block = '\n'.join(html_forms)
+ return self._wrap_payment_block(cr, uid, html_block, context=context)
diff --git a/addons/portal/acquirer_view.xml b/addons/portal/acquirer_view.xml
new file mode 100644
index 00000000000..0e88d47f758
--- /dev/null
+++ b/addons/portal/acquirer_view.xml
@@ -0,0 +1,67 @@
+
+
+
+
+ portal.payment.acquirer
+
+
+
+
+
+ portal.payment.acquirer
+
+
+
+
+
+
+
+
+ portal.payment.acquirer
+
+
+
+
+
+
+
+
+ Payment Acquirers
+ portal.payment.acquirer
+
+
+
+
+
+
+
+
diff --git a/addons/portal/portal_data.xml b/addons/portal/portal_data.xml
index 90a570bc1d5..89e62c8d48e 100644
--- a/addons/portal/portal_data.xml
+++ b/addons/portal/portal_data.xml
@@ -27,6 +27,30 @@
form
+
+% endif
+ ]]>qe6Ye3^M*QAX2x)8lQ)#lPv{9%Gy)&=t&z`LOYVd9OYBzBO zUJuitvCxTCnV%U@ER4rn!`lnoKD)z-RNgX%5a*MT-d5=!rLGE)jPni1X^H;6N=5yP z)x3iBb>jPi&q*%H*@zQ)(TIqqE7~z*wm1Y5qTW79!|!9=R3o}VeX%!I7L)-xIeGNu zL-)=>p~A40b^#}66$rL*+=M3Ez3)p9hxdGrlvs&M9a;tR`Aflh5|tMHDjIs47%qgX zZ{a4^Vd&1mLTb&7l*`~Y;rKd*L$qD>k_4d{Mm3hkHzebNlgPp@vim5IUnHTTy)W_4 zC@yDuAfnH*X7S|4Bvo35v!m8Jt^yA}tb~9gSD1*NYL3993TSdaj{_niK}zlWEKKIV zLr%TySO$Y+v?pX%|N2l@NwOW7p)9w$;gBkat~brUGMG~q=KE1euE|3SdzAs+-?nol zBc-Rh_C~e=`9lvMStO&a?J&JS?YbxLLS;6wo;mLTQS9*OuB?Djnl^>`I-s G-}YY~}x`+@E*+gRNb+w3M8-{G6^^XTov$*ub?u^uYi-6**@#Jk_)vgoU_ z8I2P-J3s($mx<%8uS1$j=$<+|oi&^3&66%qS|8iHF}oLvu8}yj=?p6;l05I~pUFjX zM4DB@oCpQ?ev|4v$fNh;|C*+AR%;ZWJzlF?1#A3ZoFeV0z5j%5N%;l?TTqDgq4|~+ z^|Ye8`cHaC!7X}0`aWV3j&zT);p{DcVIyWIW@#!+qz-LD7st^tzM9M9br^Y}NNl|S zZ=q|?u^{q4?Np(L%;!J6B%6y!axbnaDfLz=)Z0ARE9E^Tf!v*Fp7L2K^3;hYuX6+Y ztUS6## wnX-4yk* ze`0s2T*U-+m8_Uo0Qd93p7h$hT0m7hPFJ3dM~fGxN&+lWU!X!sjOj}x-|1cd&HP;A z@BubOV`xDrbXu2DqF6C6QhIA!+c0hAN9=e1DfccfVPs+eWh?*7744a}&)@X5g5gBk z4nsb0aV>xTc5aNV$%`%{Bg9<(10PCttiHvi0Xg?da-$^O6M>aCEPoF$@OR^%?@5{8 zv9?Zub7#|0H@_he{Q&bR#psblP)MZox7otI(gY!&2({Tk?}*Qh8JRH)XJ%R{y)=DZ z_Kwr^$n#3hXvuIl)>`^~p$*wR^#=utPnTCt;I!jcNk&2)Ab*t^9p((2C1!TlHOX2` zSN{Y$?))N~rN*~h1L%U&ToU$Hv1-CEH3bC{N+L RwKe^Xe|Y*FfD8k zfgabq_gFC} Gi8 zP1us#^PKqx%db~q;L0E2hB`s &8z8e%HG{6U>!tPmh2Jnwd?e(UO^7sKx$}I;^q3w%yrm zR4O18wf%}=XIynn4!d(TPdZ;MT-6?!Thf44ewTtAVMc#LF%WUC?rHP`^i0n`p%IDW z|AhJj+RshZONiYEC{yq1t$1_^fWMhw7)oM(kueZx^V(n~s1yb0X}bq4NeT`qY6r_s zhcI ~G!u znH!zsiV4V%88@*pV2^RTBexy?=~|o!{%R #kWg> zf`2eUCt9L^(?AB+1dNl84-X4!SJAt34qR^Q|3@r9%|Pth&@-koM`)9YAuI01U+G+{ zMfTztjnj7?Pzvg$Z82@S^kc3aO)ERk5;8=zLYlv|`xcvzMb#>vj$BEj? zoPXX3M#0Z8Uo;E}EnmeOIAlU{{8~J+AEkeq|JfcrY&)jF>zgGr)3FC|N(i^bJ4k%L z90r5<>!}Agq3T(q^urnw3!jb42wrh_YX*Ln35lvgV{!a!XcNnu|Mg0|f34zLKz#$X zyQKwHs_@y_d@X-+ijpvkKX2mnY2 3gG;SI00}kUShmUx|Ih6+srK(ojM*z=4V(LXwF;D=?)ud0 zh{i3vzAQJi@01!*P4)ih$tO16LG>HvLPA+4=!?QpO$cIHLsVLJ7nJGr!R@(k+gaM~ zJ_; J?6rHJ2TP`1G^FIbDYSX%Rs%nXd4W3tt$oHv*0! z@WA0r!{ejaI@a0C+wGZvVELo`^P3f9oFPG$H9!aH69vUCtOzqM{13JAO0E;%#^eR9 zs>jBPs_rK3^+pEW_Qn+5lSMTz?e>+3w#Eg~PC#`y(jR|+{4inv*w~<;wGn}Yk+sS3 z^O!UpNV=@B?DdN`zt%{mli)to!%uKO6ZDV?%JjG>>e01BZhp^phl0=+82Z}pet2{X z;^S4~d1w;Sb2}C3>TG~V|1Fgf{K@{B^Qub!qyR6!15tEuwR?R~n@GOrXKy|k1izPU zY2w`X1z8)L>Bi4xYGZIgKBwEp9FCk;%1IO3UFUM6`tH(0=k 6(SO4rc23it_={f}IT^K|#8CD{=|vTPBHJRoD_lZ;Wm*aZSNQ{rhB7LWdey zq~H{s6%xmzTXnLw{7EZ#w6jJw|D}!8MvxNkwfh+T1hQaL#qNq8?Q8NyxZvQhf^XQ5 z2=#>827Z$Pe~c@0w@S?BH8pj7*MxDqUNyHS3@HD=XNr+UPI7-1*r7^bjuS8$SWpuc z)x9)ibx%!ah5Cucv!?s>(c3>}x>*!{vg++ppGk6El1)&J>616*;>JXm>6;AT`p2QU z#H5ovR>r!VbYIJD;Q`AbrZX?m?~g|He$}#DnHV>OMBH!xgm1bzo`|qW6o{~-zmR*v zlyi;_>)BKDY(OV8-nTrptlvuzgUVmsAG@_q#laE0a_e>@==zb3_n8+So$l71*XpMt zRzJUcTn@H)uHag7M@kgj_zeyv{ +BSuZJts?N*t+{q>34NWY!$_gW=(xYcli73}n3^4U z`VS?(xPs#$Z3jLH9nsB_eDg*0dZbq^PLzb$&L2wL+5ISa ^2 ~-KX(#yoo%225HP|XWCC{# z`7Q;EE8!`OasH?mbssTw$n!1Kj|mr;Y#nq>J7YM8D}7Vu2D#BXzR+up&|70ZA=ILx zU39S`N=jm6=LvqRN13KBVR3(6zj!%xx?crvLh^U11ijY%>Y2VMP+oK}mogA-pyH#J znWTVS+Htwtn4+I?@;C||>zclXc)HJKM=qZBtuKZdN|PDru>=A)-)(}+g+u!4%D?x> zLKRzoJ(KI`wJv`mH=)D3iXcHE=E877q;}CpxCPBqnE3wV&6}T-0A)@dIll2T`ts7o z_7K}r#NwVXi}s(Hn=;z!4OaxK4Y7jh-#-{7S*Y1m{gBMmQM3xu5-D>o{9ai5q(H}x zQ~Ed9<^2+)@$(1Ys@zr0>{qD+bJp5mW&Yqru~_I(i$pzx?rEZGM>G>#@sKWm^wXf zp(Xq7TlncF>O#&3A|dNxrvo2!1Wr>Sy9T4Q?^Eoe%r-=xcsrlYcCVFBy6)Gx@wSlN z&Zzj|(mL7(9`n`-|KJhGFQWT(b8sp8+;p4km9R;tU^by+Sm>^(+Y()?Gj0GLr*3o# ze*^N3o`Oj$0CZeb &QZ~g6q9sm7O8R{9 eU66lw#~x)0;1vkbbg~C<+ KlA+mrXYyd=*a zwb4rZNs0o(BxWW3PX)B(9TzUkh}T|p%`xHU %Qp_%Kd%(A!jAR zvN=x)vx(T6V7*(Aa$>WV9beHowKA<^aV-W4An=x-GG{h@kJ$fltMFykeER6+m`;nx zZaJaW&Fiwv#~grMJg)P(q5e eQ$@cLfQz602aPf?kT zn;o~+_kDCct|7RxH-V3iMG6l4_o4X;QouwOk{Te9gCCCHnUhZ9k;#5>v&R@%0>fg- zOGjI@0M7@*-21^Tzv0NEf(h@bOy%A?&>vwiQ}*3sy;6NCg`2o ?! zh466j!hPa#TK0Lw><3!GZPH#e 7F;C@?f-QTNma8$-=b^NDxksFBsV{8P@6NJ48+JT$yEABDITw>e8fi5WDR zm;b $0?@*22x#^+1n8FpR&j7jK%np7^9_=eo66vx)o%n; z`0XqqB}+ _}7%op=zOe4{$gCvOp zB00ooBF8ENW4*to*DPrvTMr-WCa0CaW+eZdJKo1|*x0w< J~BY3q#a_!H)KpyONgipb2lKd%es&rIv~?L#3- zjpV4p;O;L$f&}xC9fBGWix^8O`7zISS-$kAJ;CPAEo=Kwk`8e8+oQMOo1kP!i^Ofw zYz#!DU^*9%N5t#bo7)-Op_TLF-x_S)W@BO$B2?>Fa0NRiqom!$MUF5vK}lS<*jOH3 z>)*#`qRd)c*5v)T7Mix{>90AfVRp`H^+RtVO+D({5{nUPLIH67`wrVG^L6ePNj+I1 zpziajmqS$g>)uZ(fxYR--sPdYgE)xmVKH_xMl65hH~x9J^8^}Vq`sWQv@(?n0v9#c zPTi$%j3T+6&!G=PquV#If^SkrI;&R?-bOZ{eblf{QX1WyMZWaKxVC9?cG4WHf-dBh zeP~of1F8Lo8*$qwe1`12uD=+=fZ~A*4RhF=!e6j<{gF*sy>CsUap0N6zD1_-kZf1q zM+j+1h86QZ<86#=^b_*&J!|x?Pob=P=eV@|_5xIyTUVu@Nq;%)2OEwN#G4)etswR~ zl4q@LK-$cOqubj=a_K7WJx4y*!RKugLUpEBr^qoO0RNc|Yu9NsAS%v#$g(>C_TjB$ z{*ju(Dya_4NzplFWc=(gy#EWBY|;2?vcf$XN4|H_0%TC>TUq$o8A+tyRqJF0@VQ^D z#=l%b6|}txIF<|oIt73opB=uP5(=c F*pTb!R4c$ zVj_Ce?<7D~Y{hU6R1)FtoWAPcl~?i(B_8Rs_Z*7$Zt_ DM`P|81QHXvK&3C zY=966`!y_pST#NNV8s`MZ&2xJP38VGVo;O&)t$&G`Nm_~$>V;$b;5G Xx}{0t+rDt)wKiOpo%I}t z3U$@p)8Ms=MNsUZyJj7_>S2MVVDQC7SSaPoh@wukiHNV!%R<-E -=qzLj3o8UmP+vx`Vfgjb}6DuJhpm6nKCZRdhzIi=LgUV4ZmEH}t(yTdZqj zw!{5o{!Su(KV{JVsOg~2dE}_+xxws{{41!Oe&}tfvZOgMMgFMO_m7X{FsjSVDWTvg z6(waD6lLb;d<+Ah=H}$nsr>j4Y(||#`6E16Gs&xmLw+t$&Kxw&;Vnz+6_iI)ha>v; z(9$zTn8x#}p^|>GzMh^s;4y@_R!cM4$mDX(7?NHb?}G!f^~~v?Y{*_kPqZq{!9jCj z`QlZ@Mw`)Dl7VAs)6#dl4lEWcp wRf&?|}(LUvPL&ZNPK(QE|Dm=nrZ; zAE}6>*e?OMw@ciKjj}yH_MG(qji?tl+6STL{6Re=(&CCk0oXhaCB1Z_d$FR!-&{p9 z^)y+1HPC3Ksbj!FiQD22oh|C$M(Rs*qs5*g(|?p|-V4QyXQ~X7*oD>9aksCy=PLkT zxEKrOkP;_MCiQ8Tot NpX;`Rzk3NvI1`Z7E3%wHmr2fKyVDsyUMyHm2ddZEuMyXG@b#yG+bV@rFh25r#d zgVD4&^tL*s=j_xN6AVo`&Dbz`@DN{XoFnYon12`#v;RtbVh-M4P3J!PFy(0DLCTe^ zr9{S{tu%lmqipZW4mBp){p@_<9E!+n|CA0-F~@abp77@DG%+LErNfD$Ajt;GoQvH* z*||hOgo)VSznD-kLDbbCOZG_4(ibGrYD&bt4Cq}jCHC$d>>f`Xn1J_Z^oChqp6@e~ zL|s!m*#)_1Ii3*lukT#$cLHLWQb5S<7iD7ov5AN0{4l?NYa(<^?H5Mb{>(T&=l__j zt7!{4o`2y^(fSi|0S@2%DTsrxKR7?%cOH%3@|4ivE+nlBjGV 8^Xf6)-~k+`)i(V5Wt z;mP1^Icpev9A5i;`q9G*N6D6D=9ex}*9HOhYX_%*yIB+G+1kuNBc1fUtrYTYwCUNJ zBcYWk`P)!C2QX *wcx_ zUJGUhjqJtI0nxEg>l!{SRI;+oK@2>yIm~@zis5NP97GfmMSp?+Zkh`Q)-;hnyg5s# z548C{imFF)%QbEYOdFXTh2uY+9U+}^Dtlpy{zYGo?5g4MD8ypjhqgZdb0I1(Me0`K zu{3WisRY= u;-n6q%2d&qk7y@KVe_7~aAT1FPU8!;a?uL; zZ~;5A) R3k~mq`3{N#2fR-Q z_k_q%xZzpz{MA?rIx7qXH6=8vQ5;v2IQZj&97AN29&?Q3SbF|kF33?Dk^n~-Q%lb< zCe7L(&}lIga^I=<&3R;;sFP YgihU z*yg94OypVs?nHY`r8S*V17=4HjMyCFo!$`|(f;e`hvbx9=x+G6uudo7w*9-Abx_~3 zkZ>ot7H>l2=Eq8 24h z*s!P5XI2Wy?d%v5)4|Oj=02J;2>D)qjM->|@sB%JxV{Ph)W&|me`4L_puW9i;|Pt@ zx}|;BxeS(n=R|!_0MJ#K>TLNKkyw3*A)t m7$>)gN@P&;p2KId zxt*>?fVurZeL$6(S8i^|L{e1mrw!k2F17PYz*xv`VO>i%oe%CA5u}kLe6g-I`chej zOGb%BCH+h_g;`X6uKR_&n8Qr*T* Af zmL!${MR8hR^7hGoSe6gHUb?*UK&E8-fkl9zukJF={pw&e5j818)I$KBseB|WhH{g} z ^n@#h5>E(0}G^
z^5mZ1H<> 6 z0S=fUC3Y|SqkjN{PS5mXE2PykaXGls lM-m_T!3p)*+KUn>6Y;PC zKfQorDw_ck+v{Iz-h+uj6x7yJ?)hbbjJ!Q|wGZ0g997P}aduC1sXN z4Zk+1<72O_+txp%%a!Pl<#|VHB1(#Yx =%+Jxm9Yf P 7Tdda12^R;f=1D(wcu@V~TF9Oypu0eA1 zfzyk1H%imiLStTnh$rtKu3|49CsV5|j0}v=uJ)oN3AlUmEvYVJNTR_O$GW}WhAkg1 zuii@rp1{>j^iOn1x#!s{m?fRul}v}f{m=(3XL^3iy724hL^4C8^&LemJ^5orJX*Jx z_q95%**7l*E~dE^`@y=20h279aUC%qYhLH{vpiot?zur>LJ$A4JzXTyfzS8YAuF>( z1w>k1on7N^P>_k!PtWpr>T-mKjhQJXKH38pt>O#hkcLbN8U``k`ys3c%+R8WD_?Pe z8m?RW9)yQrwp<0A-2ZT-bHBRTfpfaQC6#rCoJ0R1OCQwR-$GlcS98Ij_V@0+EcQXn zuc{-Mfjsh|!{D~muZ=HOJUK8nMs_|ftpk~IYD{wa$8P1$>z_Nw&a~1+?VK=#);B`l zt_*k1DzF`-ia*$^)HQovGjh{0WZ6|^4Hn~w%`%79xVZ>Ak?G7K@U7fGG*Lx+!T@yI z^Zzno-%CJm`f@f+ t(JOB(tw>oMw; zS&!p=)fZRTVqEJvO<~#zCsf(3*UJJC+5kXdU%7CtZ6Lji+RtOMQfoRe2UK_8j---Y zbaWq$)P}Y8oCoT8+O^zIN&{B0qitSA{?o~+7MlUM_}8`kApGkz%2`;f{+8dBaCy!4 zPi aTk) ChS0joZvV_;eBXuG9&)av_~<|e zKcR7fb_@@Gim2+Nql=r3+Kc_h!(P0`Mu+`ms%l-OzUFZH7Ej*$*RlDf#K8dX`Gn@C zRICQk&8u8R9o`&6VH+aTvC9Jte%1d8Q=yJ{YV9`kx6^OVgsNb!SRJ>RdBKg@>3*F5 zI{FwAtiR_h@%(& w=MCpZv4X zsFlWYB_2AcB-Z5Bo>JEJ3`IUDvpKmJt`R<&^?(_Vf4*DXG>xd!+Pu-<6+pe_j&{Wu z^u-V%QrJIb!53#YQr9Ev1#V9ASxNLw+~PVk9*fAC{W7>D{m+6{Cgi(|ILqCt<-^_> zA|6b9Lj1$f3Rz^pxqf6}oEEN4=WpVeWDfCOp_a^K6UsD$xhZeCB%CR{z)e*Pd5Q?N zwoWbH@^^3Uig@6>d(7|uK{t}sxcHyfwRa|9OB0)f?+wgOPrm=469gQT4by94W;uFZ z`*$b`KE6*tvi~3a=3BGP%wKQ|7-L~WFUspAzyB}U!grdegJUd=Io&CbrDbbywL>=z z&ytx@j}vyuzQw=X0Q|nf<~kD+umLyEe>$==odJn2Uxa;mUR9yK7@+A7k*TDRynlX2 zFL3dKyF~+{Tkr{-{L~<>7LOS9Mzmg&qGcrS8}q;2N%Cv<^{a&ZHsOIKWN)Dd=k&yf zU&+91Vh2#G1T*Ci9E_WNb=hagl+(6hc=CEKKrXL}AhEH_AA+B}rQ)=<=tHBCz<|zT zz%W_<#GhV7Nmft%B E zGnXY^NP)K?wZz($G;3awZ)N5!v2U{*jI)-o$y+tKbvY5Q6(#Z4D(gJQNs4p6=nF@u zfjowHCr(=Yg7i@;?b&kvyS`s%d @DIPb|EP t8RS9!{v!U %*D*QsS!cHcQtcdn5>RkcLybp(%9{>46fB62BhE;28JN)-x>kDKT`<` zCaH(LwEqk4WR3s+Yw$!S$?R3zVPnO?H3I$65*&58HoUk9p60~=gY#SeH_m(F2%F=- zQyEanXzn`d*ISq3lL_h;upEPeFAK>D RZwYu@p%w?tK+mBz5iZC_0U z$4%kD6$O@(l;;q}o!bk|YBL(w LWzYF=_< z!J1S2`Qc=WJ--SkTLE5X2JuFVjKp14$A8fJ?*)=%uVHl8xw*+-ttMOtC=<%dlvE8* z)bXU}{s$^r?bH_N`>-S NlK z&;KD9r2mwD(x;gB0!QtWk^HOMq>Ai*{g-+C9|%8k_r!k>2f!z0HS0apDj1W8zC^cZ z#6r^|TBd?rCRBr+osu_O7Z%KiiEL>1%d&K;Plpn|tES+Y=Wl{(g_(PE)aSBbBDVDq zM&=?1GZCudhUBZcpoKl>u}!?orRABWaf8s5dY&JIHs@)MtyHrR& w0x zuI@l)Pf1e%BN}Q;YDTtHwnIEHc4$PzieR3X+S85<=d*Fb@xnmTepkTk$&p(FGkL-w zFTq=r4N54R+Lf#(BTGzfA(@s?1AYl>bzylz&;og(O+~6E4wA# D~?5F9Z2(z@G=RvUU ld zP)>qN8$4dhG#t 0JvvhuDhJVAc{XcX1JtpYbsy-+W{rQn>#cK1a zLt;ein!}IK G!xpWP!VQPur<&nZP%4hz(D>!fmZk~(6(ULv)5iw};-aNU(IU&sL| z(rS27t7BhYvdLBB)}=*#svPYM`c-z*agenuAxMDU`67rYKxM1%`ynNx)0*d+F~-%a z=bA<;T3R9GLXAu`5F+oN2eKRch&^#A8 >p1rN}7>9JFv)CG`d^8(uLr@Q(IspFm)X^qkhjGr)=y-0<$dF`xIb%?ek z+ix!^V6rMVUl{$gvL_;d1X5$PrebY@H63n$b{ ZwSv2DiPT1>%A#`4cla(Apl z3(-x7-|pNWVr5 qg=~-8vQb@uM~H4M8+{3DL2b% zoXzZx`wPCxHSK@KjC4M-eZ%-k^g!M#YT361h *~5b zIv 9h-|8Sdn7&U5b8KXFZ+WKdezTA0*e9K zSune zQrE?#>wrjP#CrdqWW%Z_u@;L90%usScFN0C!u-QJAr5lr*YG(^m23aCK?R3ypx zz#1>L2*XR)T%#FS?$U7A+04B{(kc+Da;W-@?s7%=YV^yYI2MlG>hVDFJHtZ4m#ieS z>Ms14g$e!5^`RjLAk&cXf|h~8<`+0xjLbu<4u8-WnhMf{Gpm#jB`i08nCyret)zc0 zzo|RA<_vV*n&MZt;rRd+>JH-Tu$X!wXJl6UW2qRE`^MRnG%o6nWP%AF_$5N#SA7>w z^LG`zF0kInfi-QB(c-_xc}!8pl2Tva{kUOdKi~2;A`B{>Q?kf}2xs%nRp9bHy64tU zZLX5=?LB#avg%s=(lVjmufK2df34|fesV=ou=NQg2U?a*PF~rW>-J64$)p8_%G?P7 z7xCYPBoF^F6lF3q5h+M@J(flY{_<@6L?GR$W@SKu%enZv`h0zliexiN;K-=ln3_ZI z(7^KmCN>CF;rRERed5h~J+K8gc38!>1LfB)XEa6G1uLdH7W=ari!5V)!C#| my!V$kmrQjSWVI_x4@N0&O|45t5Z~t( zF I5Sm~SUCp8698<33=IvRrN zFxLcRac}Q;?NBy=yNq|vL45n0dD#xv4UxCq8KIuQL3c&B*GA>mbs|OAWmpAI{oyap zgg!U*L>9kXTY!!cb>?T{ZcD5jITpuSQ^~}830r9I)6-T=og#6LpPtC!$$+pm7HfRM zhom`fP$_wZb_Y4%RR}|byn){D?&ZfR6VI7%0bSV9pFK2^zfp2DBSyu`o%(E|YA4=y zclUn$T&)A!dCp?zhAJ2sFkqvQpmb4lO+NvgL1lg~cpH 8Na|*8quday8YC!2aHA(t%UpY`Q%8MBwE=L&-xwji#Ykl?D&S|s!6y*ODpg@~McG#SkQI7obYj)}2W6T2S&+c; zsO2=OCu}i&45E P+lR;98z$3Ss(j88KKJ07E z16b|mV|f*OFJVg{r=#|RrENrP%mIsFPBgBAV`<&(b{<)6Hs1n~As`i=_e+J-> _4u2qlZwcR*=A4SF*Ab^akJ830t3Ud-Fxu^m}T)w`M>tTza&@65WN6= z2Y-TfQg-|ZvD#5Xn;($)K5Oou5F2ejV-PxGV2EM^SuVfAFp;YedL_o$0I3vYeFe== zUeJcxE@zG}$`~*OpD`r@Kaeo8S8F=^9OjtododgWvk~Xv%WxE3HsM7w@Riwe2^620 zqx^ge5cYLsazmVvrPaYLPJccy<-WjB1w}Z4$=3sfr67-FbQxCo9=XWFjQhtC&iu )PM)nNC*tfD|Yf@q?#n_6Ngpf64HyBxF=9_uH<@&Dg`{VudUd}o9eLc@} zp65R2cRl9-w);)7u2~KDja-?Gw@iG{aR+Vxwb3kSN)+Aj2G@8~frYOTr+>K%8`o)V z=3HoMIdxJl)M@jEP>e&mJx4iK00%e&*fT^V!3#SMx(w+}G01dyPk-D({&IyF8hPm* z!Ll>Pz{{ypqaoOzZaY5qav{^q;nS@mg+=Gi-*3QvSwGcIAw>8RhxFPd9C_MhL~Yc` zK2PwQgg40<`d7b5*kZC=yziv9VcBfo1-xy^8Mjc8AX^>lOp{n4s!U~lox7GRe(z&} z7p(p@I$m!kX-N9Tf$y2(cH!x#HXz`9BeqTD+M5vW=$o}Tbr0Z1sludEw_1F+Q97zH zv!uLbY{s9pyb6Pd-@;e-6FGx9fvFa@Hrt*Zi@bdt)$QIDETj*~-8}iFE>jFg0^Ue; zs50`IE^@gWBpNO?mLyzw0eK4gIW`cCl`o7^>U5fh5deJM4?|Ro=0gTY; z1;lmM>yVSx4bdo3*Ao`T9ox`ZHSTULuZimx2AnO!eQ|@45An}B!L9EDIr801BGV+6 z_pJ#oYl|)o#i^`KL1j1y!KV8{S|rwma=zZ)y35mmK=(lczuwxsCc4U5Q8h0j5k8#~ z8As;RT98W&tWVB@_dw_MCaoGSV=x$yuaC*uWdPu4ML_Rwk7o2eE9*N?p10d+3RsYj zQYr;pc77l{tIAyz`0cPP)q#)v>NaB@gsJvSHU%CJ2%e5j3lCVPwKg9?UN)<;>+4|P zwrtKBVDe0^ w$>(u(}qYnGgP zqdks~Hz}f8GhahL9vfJ8V^*>A< Azf;nRmZd*`-z$Ggjxo3$EGNII%D#AWvgf0ZX{YH6z;O7Tn6GzD7DWmrtF zoeyy=-pViJ98KfhFj{aX4*m{r&H#N0^=aC)`{|lx{wts?|5nY?`Ewz#G}zl%L!Z`S zmT*%kwXj-D#HntKs#qF%4(zZMUkgbpcec0{7@K?1b*6T#W=8@C$-}_j232bK&B>u) zo|mD*cxz7VN@Fxp{i1APyBm2E7oO| sk?!c;s@djkaOb=JZiRJ1*YNvZkN0(=`?g|AQc vKMvGC`(&q5R$JMw8c1tYIR2Fy;x$9{zTO|Wd zob=M~#bmH1r(tx4dR&bmp4w@O{J$;zT7>lsa{-$REox#b`urDIW_-4=0hGkr&7J_h zwl-7c+e&1!yjWUr+c#3{@KU8l$j-6qRLURL8f_t&+x637Kg)ua9@;f$2c)ZkXeXWU zCRIB5f%OKVjC!ZeF2@+@EHMm8pP<9qacv+~Vmktl2W<~dlPt1NBbuL?brjqvr8J6L zOnh6Iai0_g%gQLDf9IHO4*Z8u(x+ymGec9QY=6rh$xRXmAr6Ut6Ka91tnS7Z`!`dW zl6@)s1CduEXj>QU&{RwgCo@)d%=o|ny#KSeWQz$l87q*Q0Zw*r*FN?=VoGQmA8 GXWN+Vdc&xr=(jL`rBP1R@Av~(Fu<< z3E@(SvZ13*WPx%Z6 J9HTA#H=|nrIOKW;VSIV^yfuVz|1gZ37(kTX+li}Zd0G6v-sZD>=elh!OcRhwiE6u^n35iqJ!^-!+@h9 z{jyE=S7QkK{IreUh2WjW=Lx0i>3odqOd9RP1Dpzflc1X*Xn0m{)xjB4Ojru;tUt*i zx1`ejk`?UWL>@r~w5@MSKlCn-YyOzTwxZ!Xm1H?V$bv4N3kEKg?V(srY-u@~R42K~ zCH|>`T%O9scNqHadZUD~tyjcTkykW%y=tLZv4bPKFo*W7UY{vPNOofCJo-WO0>m=@ zPz|KnQ KJgsZ_B7da3@ktK^9os4V>t|dT*G8FYgyPT+ z+ba@bR`va=urKK5PL8e04ZNHTgn5I#P4ZwI`bbxqhd!R}U!xgrCCRy0pU+wH(5B-= ze3!kXuPlo;xCkL*^9e}sws`B~f#f4f2EW{Yv6AenPzBG_X=b5atp>s0Inh+%@f{7Q zT?&VASHnF(-rgXJLYRoX?3^_U{z&G#J@ayJ=%-y9->$~zx&~fL3>zX8R+HDKRk#Ad zpS;lA4y~t2@X0{hQqCys4 cu^rw4pG3jmWv!`B|&Ro}#n% zdqO`K2H}%%Y$D^v4;|= EEh^+jeyyAB-PWh) 4sqs_V8DDYJ$TS zZ~pT(2*d8coD3H@s`NzgV)$&!sN1226{J6==E2brs9ks7ZMOjOz}vm0n}{w7O>BSQ z3$NDnD%zzxPWs*Cp{qXZ5OcK}3)A{e_a74)-*LZ`B%9SydOjb>t5FxV-1j O`Lcd`t_a<$&UNE?NTAOpC^@qc)mM~x zm(C{L(yixkN7ac5k{bI6 (503bK%jW4O4WVJ z50Z-9v+R3M_q=0j@Iq1BTx#YrWfyUeg21w$%jO_83cygzz$NP|xDU`=PkwFHu&~;q z3ra($eWIB*bo?uzda-{DLeDuXyeLKF8hsb%C@bya`{C|kdI=qwqqj%VU!j_`XJiS4 zd?;bC0Q`pYe*>@nuW$Auq7t^ELR+Gku3cPt)j%|yYtO@t;a8mBXHtaN&EON599HJ* zc%iK@V+7{ts5MW@R3nwSG$UW^XZaV2wLmatB!ZxLhm0PRkWbx zurZ~xIZ3o(6-G*jU5;w}n_WjejMC7BVM^UiKCaG9L{_Qt?fs?Xq`VW`hrpp*l6!j# z=b8TCafX<9Hh|^fa2%qS*Dj2buMCaqz0XxA&QQz1U;Y2d@y7U-Qp5ftc;%>n{)^Q8 i-_8DQ^61~$CMvxfN=PbeEjY%khi<56s#~S)_~;*;tJ>56 literal 0 HcmV?d00001 diff --git a/addons/web/doc/images/tests3.png b/addons/web/doc/images/tests3.png new file mode 100644 index 0000000000000000000000000000000000000000..10b45eddedcbaf300d54245b85134efb2736c3cb GIT binary patch literal 37827 zcmb@sb8zKd^Dnw%+xEn^ZQGjIwr1jFV%yHdb~2gRwv!#(zImSaJ?Eaff1O+P+f{q- z{;ulp>h9H8y*{-floTWpU~ypq004rtl$Z(t0E+xY-$6ru{f^2Iu>k;>C|07PO46dD z#7fQ%=2o_50Dx9VlNOYc+C<6)Z#qmb(|(qVMBsi_GCsv2e1b3tOdx0=DXDZsAh@tH z3bIn1k&v>Q5CnL9Q8+QFv~f+(XLn0SQ%!TT^`o5IBj=%ec6W=SNA4{ nUMDT?`blXAcYws5B}<@yCizM<$QR8BeVKE zF|jKLGw}yFig`Rb8UmnaYjb#4=dhYZe+>PZ4)J^rdm|(qJjwCDa2!)Zctk-$M0}6V z?blk~6I{N2SLERTFrM4kcqDq7YI&pJ(*F#vC4OxEJaB!RLd8eFzceA<7P&*Ls|(a^ z-?}_St9F2CiE>ni =x#QCJzkHlXN5oM^S;M2*&&aZXE3>@kAO!x{G2JjxR=1fI1Uy zDSk(sLu^1+f|84ZC-sAz7$=@j3Z@8E1 |M3}>Fk8cANHp>SY&)RL@`e-(=$ zNI?=S&t|%1=KW~ti2Du+9 jz&NuU2N1Oc$A#z?l=at2@Ao=7E#}|2 zi}0s0(SJ)!e$_@V`z)iJ;k(00_B!pDI=yg7>OnI=X#S`ia@&(|glpBO(V+pMF{5Fn zVXWY)(5tYnK&&vX;I7c;!(j~8?AP4`yBc{C@kQ$<68vdMi45Ht90U#*D7XWjALp2T z6~`pH{B0IRJc37z@;f?nn3im(qOYhPWxP}%xqJc{bwZM81sOlFe0*~m^?bs-#Js`0 z^gP9U)jZieEt?t}D4Px&1KTwl1RJMSs8xzpxg~^^o8^WTnw6~On$?gc=P-Ib{7MwQ z+~l{VM51`C<6751S8rDXS1VU_SEM6zYAIME@vvQK>0&k27iyO~Y;O{8vKPP$-HXlL z)R8965jG;zGRt3 AB Nt7Y57&8S41^G$9r0=BtBn1O(Do?7Qroa+^wc E`EL?mwN{m z6ggkcxu z;IhuLT+4`~T5J(H;c>&Wgk?CV#wX!x 1>E!IsZzj13aI&?x-8+lsVGrikh8X;ptJ0yw+q2vYR87FWjAm~x5 z096uI7 FrZ@M|ZY&k@qEcUiW_Ycz2oz`v@^O>zrbE z8Q3|j_^i83H*6KGI_w&33QQNQBpF)i=$XDu80-j~=Ul;DroKmS`4Rn~VWNmz%wXmc z=7$sCCPKzzGq##~wbFExw8nKuwFq1A8w<4BbQW~qYZsd*oBS=vY|+f-Y-CMNO?53g ztV(TCEjaBM&2o)C|NdO)+sdCnIki01+G}+ZyBrQ7b5XcaI94c}6q&4>jMdZ957cYd zv(nqpcWBSwI9os2@Nds-(` j*;R(za%*|c8;%s%YeE(9hXHXKy~ zeKtKt6#Hv>9aap7dX@v|dztd+j991XYB?MQhZ)Mav9ZoD*9k?iP_bvRMsaZQUP$HW z==g14%5G-hBsKfx|Jab`K_7-UOL=oCV@IOK@ZhkcRT1i=c43&{xv-|- cC+X53bKB_LM*|>!Xlz4>Hjs1 zYUk3o)LGW}Yv|g1WKm~$eTsUX091LHoh!~v?PQ47TkKp{8&pnFZ&tl96fiJWn$e-w z(pCy7kF3t9X|5vdEO0HZ4sH5Y3DmTcI=@pNtMqe=baT5uyKh4X4=oEVmM9%OTym{x zVwYx9XJh5C(1%(otPW`3aW`okC@Ov|nVl%n1#OXWP_?&c7O8Tos`PlfkZa*??6Tyv zzdTnuZuQf=vA3gCw2;OU)wJjA=B@JiG nf_3`yF_tE#+;;#+3s+cOc)b5e~ z><4-O2^FLh^hj92f96~CR`y;uyT_}Dp3{gpg0$gJ^Lh8c8e&n^AK7m=g(Pt4ng3GY zBW6-`SbQ<|Q&XZQr&HkVWuR>0=qYV(`L1>8X*V;E)5f82>!O>N`De~>R#7%x+V|WF zU&fnmFCF7{8wn5dqo9Q_kA|FvTtNxnH||sp#SBKRZJjdRB>gkJSwruS Z!2U%OA;2c9wS^GC2PAI@>k5`XZ|<&E+6?)?RgL_u zCM^xMc(-V;4OgkdH8K2&T@mlw@9#{Hj*mSmAmn<;0s;WvN{Ao(006`!*7bbA%~%fv z%=^f+ZT?(%p%eu=nKj}&vg)sifdOT&y;x-1a7=PtYgt@L9nN|peQ1j8?%eos*Wk8L zvuOEb{-8aCR5lONDOv&M54E4tBxXAz@kHMjcU#DDigt@EOyv)B_E#`wF?t9aH7?W+ z)Om}$3#`iBbi=ecH7=unChUk8BB0sKg^D(qqr1@mG8+4(!M}Lh?8%Gb;|GN`bVXLK zV394WN26!6UfsUrJ@2jIrT-vsnz)a*d$=3GCB&{o7fzps+=*6?&O^xId8goMCnmf< zQZ{Tkg*~QK#dYmjVlrKKuxA3p9}z8$rAST*Rk~byQY4*YDlb`DV ;;xEEOizV-z@+tG@o5?l+IY zxzj+cM14xP`{PMi`_MfnZX}*+H3a(#n~fe;n{Zp*QoZNxK~?iwaTVo-XY+9roZB`J z<|U%L!LiJRdwZ#8?!$+QqN2l2P@F)vf67PG&%+>2B22!>fU_GhAE_tm`?+Wfw0^%S zJVQNfY2V}5(YK}>sx53QTNhv8#PHgt2R&MH;$ewGCsMY14)X`G;MhCL=k?lp=Ccpn z!Y>@zpSg&BDSF=+V*RKMVcsfQBMk_2ZQfFvFCO%5M7N # zn_a$sd>DQ#iCU5r$I+G%xq*VA^Lz zWG2%_*M487S*GOHZlh~;@eF;sdV>U|gdl?16N?ge`D-BUDE%V6ks>^rF@iymrXHsh zrF^3PtxT&trEJ17+!E5J$