From eab2e225a223d0919ee4dde65f4dfe7e269fa01c Mon Sep 17 00:00:00 2001 From: "Nimesh (Open ERP)" Date: Fri, 6 Jul 2012 14:46:08 +0530 Subject: [PATCH 001/191] [FIX]: add date validation. bzr revid: nco@tinyerp.com-20120706091608-f5o08ke6z84oilui --- addons/membership/membership.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/addons/membership/membership.py b/addons/membership/membership.py index dc4eadbada9..f7007ac28e7 100644 --- a/addons/membership/membership.py +++ b/addons/membership/membership.py @@ -478,7 +478,14 @@ class Product(osv.osv): 'membership_date_from': fields.date('Date from', help='Date from which membership becomes active.'), 'membership_date_to': fields.date('Date to', help='Date until which membership remains active.'), } - + def _check_end_date(self, cr, uid, ids, context=None): + for membership in self.browse(cr, uid, ids, context=context): + if membership.membership_date_to < membership.membership_date_from: + return False + return True + _constraints = [ + (_check_end_date, 'Error ! Ending Date cannot be set before Beginning Date.', ['membership_date_to']), + ] _defaults = { 'membership': False, } From 5f2459422335c966567370eadce3281adff729f7 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Thu, 11 Oct 2012 17:20:08 +0200 Subject: [PATCH 002/191] [IMP] edi: work-in-progress: remove permanent storage of edi.document The EDI documents will now be generated on demand and available from the Portal view of each document. Instead of getting a link to a statically generated EDI document, customers will receive a link to the portal access to the document. They will be able to signup on the portal the first time as well, provided they are using the secure token that was sent to them (i.e. the right link). The link to pay online will be available in the portal as well. Still much to do, this is a small first step, with edi.document renamed to edi.edi for consistency, as it will not persist any edi.document anymore. bzr revid: odo@openerp.com-20121011152008-bht7ub6woaex0a7u --- addons/account/edi/invoice.py | 12 +- addons/account/edi/invoice_action_data.xml | 26 +-- addons/account/test/test_edi_invoice.yml | 7 +- addons/edi/__init__.py | 8 +- addons/edi/__openerp__.py | 2 - addons/edi/controllers/main.py | 65 +------- addons/edi/edi_service.py | 12 +- addons/edi/models/edi.py | 153 ++++-------------- addons/edi/models/res_company.py | 4 +- addons/edi/models/res_currency.py | 4 +- addons/edi/models/res_partner.py | 6 +- addons/edi/security/ir.model.access.csv | 3 - addons/edi/test/edi_partner_test.yml | 10 +- .../wizard/mail_compose_message.py | 1 + addons/mail/static/src/xml/mail.xml | 4 +- addons/purchase/edi/purchase_order.py | 21 +-- .../edi/purchase_order_action_data.xml | 29 +--- .../test/process/edi_purchase_order.yml | 14 +- addons/sale/edi/sale_order.py | 23 +-- addons/sale/edi/sale_order_action_data.xml | 10 +- addons/sale/test/edi_sale_order.yml | 9 +- 21 files changed, 86 insertions(+), 337 deletions(-) delete mode 100644 addons/edi/security/ir.model.access.csv diff --git a/addons/account/edi/invoice.py b/addons/account/edi/invoice.py index 751eb8656f7..e84bec6614d 100644 --- a/addons/account/edi/invoice.py +++ b/addons/account/edi/invoice.py @@ -71,16 +71,6 @@ INVOICE_EDI_STRUCT = { class account_invoice(osv.osv, EDIMixin): _inherit = 'account.invoice' - def action_invoice_sent(self, cr, uid, ids, context=None): - """"Override this method to add a link to mail""" - if context is None: - context = {} - invoice_objs = self.browse(cr, uid, ids, context=context) - edi_token = self.pool.get('edi.document').export_edi(cr, uid, invoice_objs, context = context)[0] - web_root_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url') - ctx = dict(context, edi_web_url_view=edi.EDI_VIEW_WEB_URL % (web_root_url, cr.dbname, edi_token)) - return super(account_invoice, self).action_invoice_sent(cr, uid, ids, context=ctx) - def edi_export(self, cr, uid, records, edi_struct=None, context=None): """Exports a supplier or customer invoice""" edi_struct = dict(edi_struct or INVOICE_EDI_STRUCT) @@ -136,7 +126,7 @@ 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_id, src_company_name = edi_document.pop('company_id') + _, src_company_name = edi_document.pop('company_id') invoice_type = edi_document['type'] partner_value = {} diff --git a/addons/account/edi/invoice_action_data.xml b/addons/account/edi/invoice_action_data.xml index 53fa03a6902..1ad023c3a60 100644 --- a/addons/account/edi/invoice_action_data.xml +++ b/addons/account/edi/invoice_action_data.xml @@ -1,17 +1,6 @@ - - - if (object.type in ('out_invoice', 'out_refund')) and not object.partner_id.opt_out: object.edi_export_and_email(template_ext_id='account.email_template_edi_invoice', context=context) - - code - ir.actions.server - - True - Auto-email confirmed invoices - - Email Templates @@ -27,17 +16,12 @@ - - - - - - - Automated Invoice Notification Mail + Invoice - Send by mail ${object.user_id.email or object.company_id.email or 'noreply@localhost'} ${object.company_id.name} Invoice (Ref ${object.number or 'n/a' }) ${object.partner_id.email or ''} @@ -61,12 +45,6 @@   Your contact: ${object.user_id.name}

-

- 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. +# 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 @@ -19,7 +19,7 @@ # ############################################################################## -from osv import fields,osv +from openerp.osv import osv class res_company(osv.osv): """Helper subclass for res.company providing util methods for working with diff --git a/addons/edi/models/res_currency.py b/addons/edi/models/res_currency.py index 0f56bc83f45..474e386a248 100644 --- a/addons/edi/models/res_currency.py +++ b/addons/edi/models/res_currency.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 @@ -19,7 +19,7 @@ # ############################################################################## -from osv import fields,osv +from openerp.osv import osv from edi import EDIMixin from openerp import SUPERUSER_ID diff --git a/addons/edi/models/res_partner.py b/addons/edi/models/res_partner.py index 86d8c81b790..37483c1d38d 100644 --- a/addons/edi/models/res_partner.py +++ b/addons/edi/models/res_partner.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 @@ -20,10 +20,10 @@ ############################################################################## import logging -from osv import fields,osv +from openerp.osv import osv from edi import EDIMixin from openerp import SUPERUSER_ID -from tools.translate import _ +from openerp.tools.translate import _ _logger = logging.getLogger(__name__) RES_PARTNER_EDI_STRUCT = { diff --git a/addons/edi/security/ir.model.access.csv b/addons/edi/security/ir.model.access.csv deleted file mode 100644 index b1f470d80ae..00000000000 --- a/addons/edi/security/ir.model.access.csv +++ /dev/null @@ -1,3 +0,0 @@ -id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_ir_edi_all_read,access_ir_edi_all_read,model_edi_document,,1,0,0,0 -access_ir_edi_employee_create,access_ir_edi_employee_create,model_edi_document,base.group_user,1,0,1,0 diff --git a/addons/edi/test/edi_partner_test.yml b/addons/edi/test/edi_partner_test.yml index 9fc523bf84f..97ae669e52e 100644 --- a/addons/edi/test/edi_partner_test.yml +++ b/addons/edi/test/edi_partner_test.yml @@ -6,11 +6,10 @@ with an attached file, check the result, the alter the data and reimport it. - - !python {model: edi.document}: | + !python {model: edi.edi}: | import json - partner_obj = self.pool.get('res.partner') - tokens = self.export_edi(cr, uid, [partner_obj.browse(cr, uid, ref('base.res_partner_2'))]) - doc = self.get_document(cr, uid, tokens[0], context=context) + res_partner = self.pool.get('res.partner') + doc = self.generate_edi(cr, uid, [res_partner.browse(cr, uid, ref('base.res_partner_2'))]) edi_doc, = json.loads(doc) # check content of the document @@ -36,8 +35,7 @@ "Expected (%r,> %r) after import 1, got %r" % ('res.partner', ref('base.res_partner_2'), result) # export the same partner we just created, and see if the output matches the input - tokens = self.export_edi(cr, uid, [partner_obj.browse(cr, uid, result[1])]) - doc_output = self.get_document(cr, uid, tokens[0], context=context) + doc_output = self.generate_edi(cr, uid, [res_partner.browse(cr, uid, result[1])]) edi_doc_output, = json.loads(doc_output) for attribute in ('__model', '__module', '__id', 'name', '__attachments'): assert edi_doc_output.get(attribute) == edi_doc.get(attribute), \ diff --git a/addons/email_template/wizard/mail_compose_message.py b/addons/email_template/wizard/mail_compose_message.py index c0cb7fc92e5..3e434c4d65e 100644 --- a/addons/email_template/wizard/mail_compose_message.py +++ b/addons/email_template/wizard/mail_compose_message.py @@ -83,6 +83,7 @@ class mail_compose_message(osv.osv_memory): 'datas_fname': attach_fname, 'res_model': model, 'res_id': res_id, + 'type': 'binary', # override default_type from context, possibly meant for another model! } values['attachment_ids'].append(ir_attach_obj.create(cr, uid, data_attach, context=context)) else: diff --git a/addons/mail/static/src/xml/mail.xml b/addons/mail/static/src/xml/mail.xml index 2fc97154e33..66556a30c24 100644 --- a/addons/mail/static/src/xml/mail.xml +++ b/addons/mail/static/src/xml/mail.xml @@ -60,7 +60,7 @@
  • - ...wait upload... + Upload in progress...
    @@ -73,7 +73,7 @@ - x + x
  • diff --git a/addons/purchase/edi/purchase_order.py b/addons/purchase/edi/purchase_order.py index d1c00a3e60d..f44c16eb0d9 100644 --- a/addons/purchase/edi/purchase_order.py +++ b/addons/purchase/edi/purchase_order.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 @@ -19,13 +19,8 @@ # ############################################################################## -from datetime import datetime, timedelta -from dateutil.relativedelta import relativedelta - -from osv import fields, osv, orm +from openerp.osv import osv from edi import EDIMixin -from edi.models import edi -from tools import DEFAULT_SERVER_DATE_FORMAT from tools.translate import _ PURCHASE_ORDER_LINE_EDI_STRUCT = { @@ -62,16 +57,6 @@ PURCHASE_ORDER_EDI_STRUCT = { class purchase_order(osv.osv, EDIMixin): _inherit = 'purchase.order' - def wkf_send_rfq(self, cr, uid, ids, context=None): - """"Override this method to add a link to mail""" - if context is None: - context = {} - purchase_objs = self.browse(cr, uid, ids, context=context) - edi_token = self.pool.get('edi.document').export_edi(cr, uid, purchase_objs, context = context)[0] - web_root_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url') - ctx = dict(context, edi_web_url_view=edi.EDI_VIEW_WEB_URL % (web_root_url, cr.dbname, edi_token)) - return super(purchase_order, self).wkf_send_rfq(cr, uid, ids, context=ctx) - def edi_export(self, cr, uid, records, edi_struct=None, context=None): """Exports a purchase order""" edi_struct = dict(edi_struct or PURCHASE_ORDER_EDI_STRUCT) @@ -109,7 +94,7 @@ class purchase_order(osv.osv, EDIMixin): res_partner_obj = self.pool.get('res.partner') # imported company_address = new partner address - src_company_id, src_company_name = edi_document.pop('company_id') + _, 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: diff --git a/addons/purchase/edi/purchase_order_action_data.xml b/addons/purchase/edi/purchase_order_action_data.xml index daad4467929..02e7a1c1ae1 100644 --- a/addons/purchase/edi/purchase_order_action_data.xml +++ b/addons/purchase/edi/purchase_order_action_data.xml @@ -1,18 +1,6 @@ - - - Email Templates @@ -25,19 +13,12 @@ - - - - - Automated Purchase Order Notification Mail + Purchase Order - Send by mail ${object.validator.email or ''} ${object.company_id.name} Order (Ref ${object.name or 'n/a' }) ${object.partner_id.email} @@ -64,12 +45,6 @@   Your contact: ${object.validator.name}

    -

    - 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. +# 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 @@ -19,13 +19,8 @@ # ############################################################################## -from datetime import datetime, timedelta -from dateutil.relativedelta import relativedelta - -from osv import fields, osv, orm +from openerp.osv import osv from edi import EDIMixin -from edi.models import edi -from tools import DEFAULT_SERVER_DATE_FORMAT SALE_ORDER_LINE_EDI_STRUCT = { 'sequence': True, @@ -65,15 +60,6 @@ SALE_ORDER_EDI_STRUCT = { class sale_order(osv.osv, EDIMixin): _inherit = 'sale.order' - def action_quotation_send(self, cr, uid, ids, context=None): - if context is None: - context = {} - sale_objs = self.browse(cr, uid, ids, context=context) - edi_token = self.pool.get('edi.document').export_edi(cr, uid, sale_objs, context = context)[0] - web_root_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url') - ctx = dict(context, edi_web_url_view=edi.EDI_VIEW_WEB_URL % (web_root_url, cr.dbname, edi_token)) - return super(sale_order, self).action_quotation_send(cr, uid, ids, context=ctx) - def edi_export(self, cr, uid, records, edi_struct=None, context=None): """Exports a Sale order""" edi_struct = dict(edi_struct or SALE_ORDER_EDI_STRUCT) @@ -112,7 +98,7 @@ class sale_order(osv.osv, EDIMixin): res_partner_obj = self.pool.get('res.partner') # imported company_address = new partner address - src_company_id, src_company_name = edi_document.pop('company_id') + _, src_company_name = edi_document.pop('company_id') address_info = edi_document.pop('company_address') address_info['supplier'] = True @@ -171,7 +157,6 @@ class sale_order(osv.osv, EDIMixin): currency_id = res_currency.edi_import(cr, uid, currency_info, context=context) order_currency = res_currency.browse(cr, uid, currency_id) - date_order = edi_document['date_order'] partner_ref = edi_document.pop('partner_ref', False) edi_document['client_order_ref'] = edi_document['name'] edi_document['name'] = partner_ref or edi_document['name'] @@ -185,7 +170,7 @@ class sale_order(osv.osv, EDIMixin): order_lines = edi_document['order_line'] for order_line in order_lines: - self._edi_requires_attributes(( 'product_id', 'product_uom', 'product_qty', 'price_unit'), order_line) + self._edi_requires_attributes(('product_id', 'product_uom', 'product_qty', 'price_unit'), order_line) order_line['product_uom_qty'] = order_line['product_qty'] del order_line['product_qty'] diff --git a/addons/sale/edi/sale_order_action_data.xml b/addons/sale/edi/sale_order_action_data.xml index 9796d0bda16..5ce35bf497d 100644 --- a/addons/sale/edi/sale_order_action_data.xml +++ b/addons/sale/edi/sale_order_action_data.xml @@ -1,7 +1,6 @@ - Email Templates @@ -15,14 +14,13 @@ - - Automated Sale Order Notification Mail + Sale Order - Send by mail ${object.user_id.email or ''} ${object.company_id.name} Order (Ref ${object.name or 'n/a' }) ${object.partner_invoice_id.email} @@ -49,12 +47,6 @@   Your contact: ${object.user_id.name}

    -

    - 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 Date: Tue, 16 Oct 2012 14:29:13 +0200 Subject: [PATCH 003/191] [IMP] edi: ported web part to 7.0 + removed the EDI preview- and export-related parts As started in previous commits, in 7.0 there is no more static storage of exported EDI documents, and they are instead replaced by the improved portal - which will provide the preview as well as export capabilities. (The export might be added in a second phase) bzr revid: odo@openerp.com-20121016122913-ndl0l36buhku3kxy --- addons/account/edi/invoice.py | 10 +- addons/account/test/test_edi_invoice.yml | 145 ++++++++++-- addons/auth_signup/controllers/main.py | 1 - addons/edi/__init__.py | 13 +- addons/edi/controllers/main.py | 19 +- addons/edi/models/edi.py | 4 +- addons/edi/models/res_partner.py | 1 - addons/edi/static/src/css/edi.css | 210 ------------------ addons/edi/static/src/img/pdf.png | Bin 6960 -> 0 bytes addons/edi/static/src/js/edi.js | 137 ++---------- addons/edi/static/src/xml/edi.xml | 93 -------- addons/edi/static/src/xml/edi_account.xml | 163 -------------- .../edi/static/src/xml/edi_sale_purchase.xml | 169 -------------- 13 files changed, 163 insertions(+), 802 deletions(-) delete mode 100644 addons/edi/static/src/css/edi.css delete mode 100644 addons/edi/static/src/img/pdf.png delete mode 100644 addons/edi/static/src/xml/edi.xml delete mode 100644 addons/edi/static/src/xml/edi_account.xml delete mode 100644 addons/edi/static/src/xml/edi_sale_purchase.xml diff --git a/addons/account/edi/invoice.py b/addons/account/edi/invoice.py index e84bec6614d..91747de4ea1 100644 --- a/addons/account/edi/invoice.py +++ b/addons/account/edi/invoice.py @@ -19,9 +19,8 @@ # ############################################################################## -from osv import fields, osv, orm +from openerp.osv import osv from edi import EDIMixin -from edi.models import edi INVOICE_LINE_EDI_STRUCT = { 'name': True, @@ -137,9 +136,14 @@ class account_invoice(osv.osv, EDIMixin): # 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) @@ -190,7 +194,7 @@ class account_invoice(osv.osv, EDIMixin): invoice_type = invoice_type.startswith('in_') and invoice_type.replace('in_','out_') or invoice_type.replace('out_','in_') edi_document['type'] = invoice_type - #import company as a new partner + # import company as a new partner partner_id = self._edi_import_company(cr, uid, edi_document, context=context) # Set Account diff --git a/addons/account/test/test_edi_invoice.yml b/addons/account/test/test_edi_invoice.yml index 3d57f8dc608..fea829e4ba3 100644 --- a/addons/account/test/test_edi_invoice.yml +++ b/addons/account/test/test_edi_invoice.yml @@ -45,39 +45,39 @@ 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 + Then I import a sample EDI document of another customer invoice from OpenERP 7.0 - !python {model: account.invoice}: | import time edi_document = { - "__id": "account:b22acf7a-ddcd-11e0-a4db-701a04e25543.random_invoice_763jsms", + "__id": "account:b33adf8a-decd-11f0-a4de-702a04e25700.random_invoice_763jsms", "__module": "account", "__model": "account.invoice", - "__version": [6,1,0], - "internal_number": time.strftime("SAJ/%Y/002"), + "__version": [7,0,0], + "internal_number": time.strftime("SAJ/%Y/070"), "company_address": { - "__id": "base:b22acf7a-ddcd-11e0-a4db-701a04e25543.main_address", + "__id": "base:b33adf8a-decd-11f0-a4de-702a04e25700.main_address", "__module": "base", "__model": "res.partner", "city": "Gerompont", "name": "Company main address", "zip": "1367", - "country_id": ["base:b22acf7a-ddcd-11e0-a4db-701a04e25543.be", "Belgium"], + "country_id": ["base:b33adf8a-decd-11f0-a4de-702a04e25700.be", "Belgium"], "phone": "(+32).81.81.37.00", "street": "Chaussee de Namur 40", "bank_ids": [ - ["base:b22acf7a-ddcd-11e0-a4db-701a04e25543.res_partner_bank-ZrTWzesfsdDJzGbp","Sample bank: 123465789-156113"] + ["base:b33adf8a-decd-11f0-a4de-702a04e25700.res_partner_bank-ZrTWzesfsdDJzGbp","Sample bank: 70-123465789-156113"] ], }, - "company_id": ["account:b22acf7a-ddcd-11e0-a4db-701a04e25543.res_company_test11", "Thomson pvt. ltd."], + "company_id": ["account:b33adf8a-decd-11f0-a4de-702a04e25700.res_company_test11", "Thomson pvt. ltd."], "currency": { - "__id": "base:b22acf7a-ddcd-11e0-a4db-701a04e25543.EUR", + "__id": "base:b33adf8a-decd-11f0-a4de-702a04e25700.EUR", "__module": "base", "__model": "res.currency", "code": "EUR", "symbol": "€", }, - "partner_id": ["account:b22acf7a-ddcd-11e0-a4db-701a04e25543.res_partner_test20", "Junjun wala"], + "partner_id": ["account:b33adf8a-decd-11f0-a4de-702a04e25700.res_partner_test20", "Junjun wala"], "partner_address": { "__id": "base:5af1272e-dd26-11e0-b65e-701a04e25543.res_partner_address_7wdsjasdjh", "__module": "base", @@ -92,7 +92,7 @@ "date_invoice": time.strftime('%Y-%m-%d'), "name": "sample invoice", "tax_line": [{ - "__id": "account:b22acf7a-ddcd-11e0-a4db-701a04e25543.account_invoice_tax-4g4EutbiEMVl", + "__id": "account:b33adf8a-decd-11f0-a4de-702a04e25700.account_invoice_tax-4g4EutbiEMVl", "__module": "account", "__model": "account.invoice.tax", "amount": 1000.0, @@ -103,21 +103,21 @@ "invoice_line": [{ "__module": "account", "__model": "account.invoice.line", - "__id": "account:b22acf7a-ddcd-11e0-a4db-701a04e25543.account_invoice_line-1RP3so", - "uos_id": ["product:b22acf7a-ddcd-11e0-a4db-701a04e25543.product_uom_unit", "Unit"], + "__id": "account:b33adf8a-decd-11f0-a4de-702a04e25700.account_invoice_line-1RP3so", + "uos_id": ["product:b33adf8a-decd-11f0-a4de-702a04e25700.product_uom_unit", "Unit"], "name": "PC Assemble SC234", "price_unit": 10.0, - "product_id": ["product:b22acf7a-ddcd-11e0-a4db-701a04e25543.product_product_3", "[PCSC234] PC Assemble SC234"], + "product_id": ["product:b33adf8a-decd-11f0-a4de-702a04e25700.product_product_3", "[PCSC234] PC Assemble SC234"], "quantity": 1.0 }, { "__module": "account", "__model": "account.invoice.line", - "__id": "account:b22acf7a-ddcd-11e0-a4db-701a04e25543.account_invoice_line-u2XV5", - "uos_id": ["product:b22acf7a-ddcd-11e0-a4db-701a04e25543.product_uom_unit", "Unit"], + "__id": "account:b33adf8a-decd-11f0-a4de-702a04e25700.account_invoice_line-u2XV5", + "uos_id": ["product:b33adf8a-decd-11f0-a4de-702a04e25700.product_uom_unit", "Unit"], "name": "PC on Demand", "price_unit": 100.0, - "product_id": ["product:b22acf7a-ddcd-11e0-a4db-701a04e25543.product_product_5", "[PC-DEM] PC on Demand"], + "product_id": ["product:b33adf8a-decd-11f0-a4de-702a04e25700.product_product_5", "[PC-DEM] PC on Demand"], "quantity": 5.0 }] } @@ -128,10 +128,10 @@ # check bank info on partner 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 + assert bank_info.acc_number == "Sample bank: 70-123465789-156113", 'Expected "Sample bank: 70-123465789-156113", got %s' % bank_info.acc_number assert invoice_new.partner_id.supplier, 'Imported Partner is not marked as supplier' - assert invoice_new.reference == time.strftime("SAJ/%Y/002"), "internal number is not stored in reference" + assert invoice_new.reference == time.strftime("SAJ/%Y/070"), "internal number is not stored in reference" assert invoice_new.reference_type == 'none', "reference type is not set to 'none'" assert invoice_new.internal_number == False, "internal number is not reset" assert invoice_new.journal_id.id, "journal id is not selected" @@ -153,3 +153,110 @@ for inv_tax in invoice_new.tax_line: assert inv_tax.manual, "tax line not set to manual" assert inv_tax.account_id, "missing tax line account" +- + Then I import a sample EDI document of another customer invoice from OpenERP 6.1 (to test backwards compatibility) +- + !python {model: account.invoice}: | + import time + edi_document = { + "__id": "account:b22acf7a-ddcd-11e0-a4db-701a04e25543.random_invoice_763jsms", + "__module": "account", + "__model": "account.invoice", + "__version": [6,1,0], + "internal_number": time.strftime("SAJ/%Y/061"), + "company_address": { + "__id": "base:b22acf7a-ddcd-11e0-a4db-701a04e25543.main_address", + "__module": "base", + "__model": "res.partner.address", + "city": "Gerompont", + "zip": "1367", + "country_id": ["base:b22acf7a-ddcd-11e0-a4db-701a04e25543.be", "Belgium"], + "phone": "(+32).81.81.37.00", + "street": "Chaussee de Namur 40", + "bank_ids": [ + ["base:b22acf7a-ddcd-11e0-a4db-701a04e25543.res_partner_bank-ZrTWzesfsdDJzGbp","Sample bank: 123465789-156113"] + ], + }, + "company_id": ["account:b22acf7a-ddcd-11e0-a4db-701a04e25543.res_company_test11", "Thomson pvt. ltd."], + "currency": { + "__id": "base:b22acf7a-ddcd-11e0-a4db-701a04e25543.EUR", + "__module": "base", + "__model": "res.currency", + "code": "EUR", + "symbol": "€", + }, + "partner_id": ["account:b22acf7a-ddcd-11e0-a4db-701a04e25543.res_partner_test20", "Junjun wala"], + "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"], + }, + "date_invoice": time.strftime('%Y-%m-%d'), + "name": "sample invoice", + "tax_line": [{ + "__id": "account:b22acf7a-ddcd-11e0-a4db-701a04e25543.account_invoice_tax-4g4EutbiEMVl", + "__module": "account", + "__model": "account.invoice.tax", + "amount": 1000.0, + "manual": True, + "name": "sale tax", + }], + "type": "out_invoice", + "invoice_line": [{ + "__module": "account", + "__model": "account.invoice.line", + "__id": "account:b22acf7a-ddcd-11e0-a4db-701a04e25543.account_invoice_line-1RP3so", + "uos_id": ["product:b22acf7a-ddcd-11e0-a4db-701a04e25543.product_uom_unit", "PCE"], + "name": "Basic PC", + "price_unit": 10.0, + "product_id": ["product:b22acf7a-ddcd-11e0-a4db-701a04e25543.product_product_pc1", "[PC1] Basic PC"], + "quantity": 1.0 + }, + { + "__module": "account", + "__model": "account.invoice.line", + "__id": "account:b22acf7a-ddcd-11e0-a4db-701a04e25543.account_invoice_line-u2XV5", + "uos_id": ["product:b22acf7a-ddcd-11e0-a4db-701a04e25543.product_uom_unit", "PCE"], + "name": "Medium PC", + "price_unit": 100.0, + "product_id": ["product:b22acf7a-ddcd-11e0-a4db-701a04e25543.product_product_pc3", "[PC3] Medium PC"], + "quantity": 5.0 + }] + } + invoice_id = self.edi_import(cr, uid, edi_document, context=context) + assert invoice_id, 'EDI import failed' + invoice_new = self.browse(cr, uid, invoice_id) + + # check bank info on partner + 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 + + assert invoice_new.partner_id.supplier, 'Imported Partner is not marked as supplier' + assert invoice_new.reference == time.strftime("SAJ/%Y/061"), "internal number is not stored in reference" + assert invoice_new.reference_type == 'none', "reference type is not set to 'none'" + assert invoice_new.internal_number == False, "internal number is not reset" + assert invoice_new.journal_id.id, "journal id is not selected" + assert invoice_new.type == 'in_invoice', "Invoice type was not set properly" + 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.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.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" + else: + raise AssertionError('unknown invoice line: %s' % inv_line) + for inv_tax in invoice_new.tax_line: + assert inv_tax.manual, "tax line not set to manual" + assert inv_tax.account_id, "missing tax line account" diff --git a/addons/auth_signup/controllers/main.py b/addons/auth_signup/controllers/main.py index 54dcc5b4362..65a05255f97 100644 --- a/addons/auth_signup/controllers/main.py +++ b/addons/auth_signup/controllers/main.py @@ -35,7 +35,6 @@ class Controller(openerp.addons.web.http.Controller): def retrieve(self, req, dbname, token): """ retrieve the user info (name, login or email) corresponding to a signup token """ registry = RegistryManager.get(dbname) - user_info = None with registry.cursor() as cr: res_partner = registry.get('res.partner') user_info = res_partner.signup_retrieve_info(cr, openerp.SUPERUSER_ID, token) diff --git a/addons/edi/__init__.py b/addons/edi/__init__.py index 71ee8aaa53d..b1563ed72cd 100644 --- a/addons/edi/__init__.py +++ b/addons/edi/__init__.py @@ -20,18 +20,9 @@ ############################################################################## import logging +from . import controllers from . import models from . import edi_service -from models.edi import EDIMixin, edi -_logger = logging.getLogger(__name__) - -# web -try: - import openerp.addons.web.controllers -except ImportError: - _logger.warn( - """Could not load openerp-web section of EDI, EDI will not behave correctly - -To fix, launch openerp-web in embedded mode""") +from .models.edi import EDIMixin, edi # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/edi/controllers/main.py b/addons/edi/controllers/main.py index 0322099b6f3..14cd97b3e62 100644 --- a/addons/edi/controllers/main.py +++ b/addons/edi/controllers/main.py @@ -1,4 +1,6 @@ -import openerp.addons.web.common.http as openerpweb +import simplejson + +import openerp.addons.web.http as openerpweb import openerp.addons.web.controllers.main as webmain class EDI(openerpweb.Controller): @@ -7,10 +9,17 @@ class EDI(openerpweb.Controller): @openerpweb.httprequest def import_url(self, req, url): - d = self.template(req) - d["init"] = 's.edi.edi_import("%s");'%(url) - r = webmain.html_template % d - return r + modules = webmain.module_boot(req) + ['edi'] + modules_str = ','.join(modules) + modules_json = simplejson.dumps(modules) + js = "\n ".join('' % i for i in webmain.manifest_list(req, modules_str, 'js')) + css = "\n ".join('' % i for i in webmain.manifest_list(req, modules_str, 'css')) + return webmain.html_template % { + 'js': js, + 'css': css, + 'modules': modules_json, + 'init': 's.edi.edi_import("%s");' % url, + } @openerpweb.jsonrequest def import_edi_url(self, req, url): diff --git a/addons/edi/models/edi.py b/addons/edi/models/edi.py index 29e19f2da3b..7c6fcc3ddfe 100644 --- a/addons/edi/models/edi.py +++ b/addons/edi/models/edi.py @@ -24,14 +24,12 @@ import hashlib import json import logging import re -import threading import time import urllib2 import openerp import openerp.release as release 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 @@ -540,7 +538,7 @@ class EDIMixin(object): 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['__model'], self._name) + (edi.get('__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['__id']) diff --git a/addons/edi/models/res_partner.py b/addons/edi/models/res_partner.py index 37483c1d38d..057e84b5541 100644 --- a/addons/edi/models/res_partner.py +++ b/addons/edi/models/res_partner.py @@ -23,7 +23,6 @@ import logging from openerp.osv import osv from edi import EDIMixin from openerp import SUPERUSER_ID -from openerp.tools.translate import _ _logger = logging.getLogger(__name__) RES_PARTNER_EDI_STRUCT = { diff --git a/addons/edi/static/src/css/edi.css b/addons/edi/static/src/css/edi.css deleted file mode 100644 index 6385959c036..00000000000 --- a/addons/edi/static/src/css/edi.css +++ /dev/null @@ -1,210 +0,0 @@ -/** EDI content **/ -.openerp .company_logo { - background-size: 180px 46px; -} -.oe_edi_view { - width: 65%; - vertical-align: top; - padding: 0px 25px; - border-right: 1px solid #D2CFCF; -} -.oe_edi_sidebar_container { - width: 35%; - padding: 0px 10px; - vertical-align: top; -} -button.oe_edi_action_print { - font-size: 1.5em; - margin-left: 35%; - margin-bottom: 20px; -} -button.oe_edi_action_print img { - vertical-align: bottom; - width: 32px; - height: 32px; -} - -/** EDI Sidebar **/ -.oe_edi_sidebar_title { - border-bottom: 1px solid #D2CFCF; - font-weight: bold; - font-size: 1.3em; - min-width: 10em; -} -.oe_edi_nested_block, .oe_edi_nested_block_import, .oe_edi_nested_block_pay { - margin: 0px 40px; - min-width: 10em; - display: none; /* made visible by click on parent input/label */ -} -.oe_edi_right_top .oe_edi_nested_block label { - float: left; - text-align: right; - margin-right: 0.5em; - line-height: 180%; - font-weight: bold; - min-width: 5em; -} -.oe_edi_option { - padding-left: 5px; - line-height: 2em; -} -.oe_edi_option:hover { - background: #e8e8e8; -} -.oe_edi_import_button { - margin: 2px 10px; - white-space: nowrap; -} -.oe_edi_small, .oe_edi_small input { - font-size: 90%; -} - -/** Sidebar bottom **/ -.oe_edi_paypal_button { - margin: 6px; -} - - -/** Paperbox, from http://www.sitepoint.com/pure-css3-paper-curl/ **/ -.oe_edi_paperbox { - position: relative; - width: 700px; - padding: 30px; - padding-bottom: 50px; - margin: 20px auto; - background-color: #fff; - -webkit-box-shadow: 0 0 4px rgba(0, 0, 0, 0.2), inset 0 0 50px rgba(0, 0, 0, 0.1); - -moz-box-shadow: 0 0 4px rgba(0, 0, 0, 0.2), inset 0 0 50px rgba(0, 0, 0, 0.1); - box-shadow: 0 0 5px rgba(0, 0, 0, 0.2), inset 0 0 50px rgba(0, 0, 0, 0.1); -} -.oe_edi_paperbox:before, .oe_edi_paperbox:after { - position: absolute; - width: 40%; - height: 10px; - content: ' '; - left: 12px; - bottom: 15px; - background: transparent; - -webkit-transform: skew(-5deg) rotate(-5deg); - -moz-transform: skew(-5deg) rotate(-5deg); - -ms-transform: skew(-5deg) rotate(-5deg); - -o-transform: skew(-5deg) rotate(-5deg); - transform: skew(-5deg) rotate(-5deg); - -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3); - -moz-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3); - box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3); - z-index: -1; -} -.oe_edi_paperbox:after { - left: auto; right: 12px; - -webkit-transform: skew(5deg) rotate(5deg); - -moz-transform: skew(5deg) rotate(5deg); - -ms-transform: skew(5deg) rotate(5deg); - -o-transform: skew(5deg) rotate(5deg); - transform: skew(5deg) rotate(5deg); -} - -/** Sale Order / Purchase Order Preview **/ -table.oe_edi_data, .oe_edi_doc_title { - border-collapse: collapse; - clear: both; -} -.oe_edi_data th { - white-space: nowrap; -} -.oe_edi_data .oe_edi_floor { - border-bottom: 1px solid black; -} -.oe_edi_data .oe_edi_ceiling { - border-top: 1px solid black; -} -.oe_edi_data .oe_edi_data_row { - border-bottom: 1px solid #D2CFCF; -} -.oe_edi_data_row td { - vertical-align: top; -} -.oe_edi_inner_note { - font-style: italic; - font-size: 95%; - padding-left: 10px; - - /* prevent wide notes from disrupting layout due to
     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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
    -
    -    
    -
    - - - - - - - - - - - - - - -
    -
    - -
    - - - - - -
    -

    - -
    -

    - Import this document -

    -
    - - -
    -

    - -
    -
    - -

    - -
    - - -
    -

    - -

    - -
    - - -
    -

    - 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. -

    -
    -
    1. import urllib2, simplejson
    2. -
    3. edi_document = urllib2.urlopen('').read()
    4. -
    5. document_data = simplejson.loads(edi_document)[0]
    6. -
    7. print "Amount: ", document_data['amount_total']
    8. -
    -

    - 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 @@ - 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 @@ - From b42801595e48ab266a60ae6b9e77d73a1adb98fe Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 17 Oct 2012 18:28:55 +0200 Subject: [PATCH 004/191] [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 005/191] [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 006/191] [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 007/191] [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 008/191] [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 009/191] [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 010/191] [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 1cf9b2a60da502de27aa67e8318f41ad6692e5a9 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 24 Oct 2012 13:01:46 +0200 Subject: [PATCH 011/191] [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 012/191] [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 013/191] [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 + +
    + +
    +
    + +
    +

    + This is an HTML form template to submit a payment through this acquirer. + The template will be rendered through mako, so it may use normal mako expressions. + The mako evaluation context provides: +

      +
    • reference: the reference number of the document to pay
    • +
    • kind: the kind of document on which the payment form is rendered (translated to user language, e.g. "Invoice")
    • +
    • currency: the currency record in which the document is issued (e.g. currency.name could be EUR)
    • +
    • amount: the total amount to pay, as a float
    • +
    • object: the document on which the payment form is rendered (usually an invoice or sale order record)
    • +
    • quote(): a method to quote special string character to make them suitable for inclusion in a URL
    • +
    • cr: the current database cursor
    • +
    • uid: the current user id
    • +
    • ctx: the current context dictionary
    • +
    + If the template renders to an empty result in a certain context it will be ignored, as if it was inactive. +

    +
    + +
    +
    +
    +
    +
    + + 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 + + + + Paypal + + + + +% endif + ]]> + diff --git a/addons/portal/security/ir.model.access.csv b/addons/portal/security/ir.model.access.csv index 9202c7f4087..b6d932bf7ee 100644 --- a/addons/portal/security/ir.model.access.csv +++ b/addons/portal/security/ir.model.access.csv @@ -4,3 +4,5 @@ access_res_partner,res.partner,base.model_res_partner,portal.group_portal,1,0,0, access_res_partner_address,res.partner_address,base.model_res_partner_address,portal.group_portal,1,0,0,0 access_res_partner_category,res.partner_category,base.model_res_partner_category,portal.group_portal,1,0,0,0 access_res_partner_title,res.partner_title,base.model_res_partner_title,portal.group_portal,1,0,0,0 +access_acquirer,portal.payment.acquirer,portal.model_portal_payment_acquirer,,1,0,0,0 +access_acquirer_all,portal.payment.acquirer,portal.model_portal_payment_acquirer,base.group_system,1,1,1,1 diff --git a/addons/portal/static/src/css/portal.css b/addons/portal/static/src/css/portal.css new file mode 100644 index 00000000000..02d0d44e6b3 --- /dev/null +++ b/addons/portal/static/src/css/portal.css @@ -0,0 +1,60 @@ + +.openerp .oe_application .oe_form_sheetbg { + /* Establish a stacking context on top of which the + payment_acquirers::before element can be positioned */ + position: relative; + z-index: 0; +} + +.openerp .payment_acquirers { + margin: -40px 0 -32px -24px; + position: relative; + padding: 10px 15px; + right: -153px; + + background: #729FCF; + background-image: -webkit-gradient(linear, left top, left bottom, from(#729FCF), to(#3465A4)); + background-image: -webkit-linear-gradient(top, #729FCF, #3465A4); + background-image: -moz-linear-gradient(top, #729FCF, #3465A4); + background-image: -ms-linear-gradient(top, #729FCF, #3465A4); + background-image: -o-linear-gradient(top, #729FCF, #3465A4); + background-image: linear-gradient(to bottom, #729FCF, #3465A4); + border-bottom: 1px solid #043574; + + -webkit-box-shadow: 0 4px 20px rgba(0, 0, 0, 0.45); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.45); +} + +.openerp .payment_acquirers::after { + content: " "; + display: block; + width: 10px; + height: 20px; + position: absolute; + bottom: 0; + right: 1px; + + margin-bottom: -6px; + background: #043574; + + -webkit-transform: skewY(-45deg); + -moz-transform: skewY(-45deg); + -ms-transform: skewY(-45deg); + -o-transform: skewY(-45deg); + transform: skewY(-45deg); + + -webkit-box-shadow: inset 1px -1px 2px black, -1px 1px 3px black; + box-shadow: inset 1px -1px 2px black, -1px 1px 3px black; + + /* push it under all its siblings, just on top of its root + in the z-index stack: div.oe_form_sheetbg */ + z-index: -1; +} + +.openerp .payment_acquirers .payment_header { + font-weight: bold; + font-size: 110%; + padding-right: 15px; + color: white; + text-shadow: 0 1px 1px #729FCF, 0 -1px 1px #3465A4; +} \ No newline at end of file From 21e48c3b96f12878f84909cf09a734eb7032657c Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 24 Oct 2012 15:16:47 +0200 Subject: [PATCH 014/191] [ADD] portal_sale: integrate payment acquirer options in sale.order forms bzr revid: odo@openerp.com-20121024131647-hfqgjpftwgygx976 --- addons/portal_sale/__init__.py | 1 + addons/portal_sale/portal_sale.py | 40 +++++++++++++++++++++++++ addons/portal_sale/portal_sale_view.xml | 12 ++++++++ 3 files changed, 53 insertions(+) create mode 100644 addons/portal_sale/portal_sale.py diff --git a/addons/portal_sale/__init__.py b/addons/portal_sale/__init__.py index 26c654db9dd..8cc32ba971e 100644 --- a/addons/portal_sale/__init__.py +++ b/addons/portal_sale/__init__.py @@ -19,3 +19,4 @@ # ############################################################################## +import portal_sale \ No newline at end of file diff --git a/addons/portal_sale/portal_sale.py b/addons/portal_sale/portal_sale.py new file mode 100644 index 00000000000..4e41fd53332 --- /dev/null +++ b/addons/portal_sale/portal_sale.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 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 +# 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 . +# +############################################################################## + +from openerp.osv import osv, fields + +class sale_order(osv.Model): + _inherit = 'sale.order' + + _payment_block_proxy = lambda self,*a,**kw: self._portal_payment_block(*a, **kw) + + _columns = { + 'portal_payment_options': fields.function(_payment_block_proxy, type="html", string="Portal Payment Options"), + } + + def _portal_payment_block(self, cr, uid, ids, fieldname, arg, context=None): + result = dict.fromkeys(ids, False) + payment_acquirer = self.pool.get('portal.payment.acquirer') + for this in self.browse(cr, uid, ids, context=context): + if this.state != 'draft': + result[this.id] = payment_acquirer.render_payment_block(cr, uid, this, this.name, + this.pricelist_id.currency_id, this.amount_total, context=context) + return result \ No newline at end of file diff --git a/addons/portal_sale/portal_sale_view.xml b/addons/portal_sale/portal_sale_view.xml index 6b25d87b791..f4607590717 100644 --- a/addons/portal_sale/portal_sale_view.xml +++ b/addons/portal_sale/portal_sale_view.xml @@ -2,6 +2,18 @@ + + sale.order.form.payment + sale.order + + + + + + + + + KW(kg?tSvon^iHzELPNhq{mgIN{$#?B~IOw5AGs1I9JxQ z^o>o~5%r86vnc`;))MvuLRzy$QkM+blO$-0`;Vp8>%VU8d9iOd#`-W>3T}1oWdfd8 zwj~=AGjmoNSShj8FKvBR|gq`h>+Dh&}xwpW2u zzV&AVkg#HwX3gzd~p!OJq9HGGwZ8Di(8-fQTQ7*^IY&7+0J2olCO8VPFwTf+k_eWFi_d3I$9I{7M69mbT|0myhM2$) z`o`($(r%+3{)#v3uJ7Tt>+&-Tr9GL zK-J%nNKU!8+47DV&=_8Wzekq~SaCc0`4)-+1|IO^gWo0M@@ofr_>NWxs5i}v(8FG6 zM?LAzQl?s$z?q+0KCBkf1;yRh=bO*Dn%~TUaOUIg<@=MvFwk6uH3(ZM2lF!~qGaFc zC>@db<#o|Vj)rR*v)MdEtsUq2tX`c7M{z$6M$YWapbAxBNFv78X}kS0;i>#gD;@AL zOGgeV?Y`&TUX9@KI2VA|HED2QeRFZi*`R1}C-E){gcn0it^MY&Gf_EGF2rxipbVz| zAzSpC+>f8h4IV`G`hi8lecnZCLOHPFoJVO$03yf!^g$7`VZ)bn|BeBgTyw$07mWeU zo(E+XL_d3L{H2f|-BXMbH+@(*OQm~fqitQ&V{XR!-y)?5+z6C3`WcwEF)a*sm4tM6 zJ&U~`N1AwgG5h}3`$)j!f8u`shS~m(FYDIC(4HGGUl1=(-Dnpe@R;IpYo%{sVhDij Wnj<@$ntjj@VI)Lkzf}n7`~4p&g3;9g literal 0 HcmV?d00001 diff --git a/addons/web/doc/images/tests2.png b/addons/web/doc/images/tests2.png new file mode 100644 index 0000000000000000000000000000000000000000..59835b257b5e18f236303a34c66ede42f0bbc29e GIT binary patch literal 37769 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 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 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 literal 0 HcmV?d00001 diff --git a/addons/web/doc/index.rst b/addons/web/doc/index.rst index 6a0d8dee2d4..a0c66be12fd 100644 --- a/addons/web/doc/index.rst +++ b/addons/web/doc/index.rst @@ -24,6 +24,8 @@ Contents: guides/client-action + testing + Indices and tables ================== diff --git a/addons/web/doc/testing.rst b/addons/web/doc/testing.rst new file mode 100644 index 00000000000..34826201dc9 --- /dev/null +++ b/addons/web/doc/testing.rst @@ -0,0 +1,479 @@ +.. highlight:: javascript + +Testing in OpenERP Web +====================== + +Javascript Unit Testing +----------------------- + +OpenERP Web 7.0 includes means to unit-test both the core code of +OpenERP Web and your own javascript modules. On the javascript side, +unit-testing is based on QUnit_ with a number of helpers and +extensions for better integration with OpenERP. + +To see what the runner looks like, find (or start) an OpenERP server +with the web client enabled, and navigate to ``/web/tests`` e.g. `on +OpenERP's CI `_. This will +show the runner selector, which lists all modules with javascript unit +tests, and allows starting any of them (or all javascript tests in all +modules at once). + +.. image:: ./images/runner.png + :align: center + +Clicking any runner button will launch the corresponding tests in the +bundled QUnit_ runner: + +.. image:: ./images/tests.png + :align: center + +Writing a test case +------------------- + +The first step is to list the test file(s). This is done through the +``test`` key of the openerp manifest, by adding javascript files to it +(next to the usual YAML files, if any): + +.. code-block:: python + + { + 'name': "Demonstration of web/javascript tests", + 'category': 'Hidden', + 'depends': ['web'], + 'test': ['static/test/demo.js'], + } + +and to create the corresponding test file(s) + +.. note:: + + test files which do not exist will be ignored, if all test files + of a module are ignored (can not be found), the test runner will + consider that the module has no javascript tests + +After that, refreshing the runner selector will display the new module +and allow running all of its (0 so far) tests: + +.. image:: ./images/runner2.png + :align: center + +The next step is to create a test case:: + + openerp.testing.section('basic section', function (test) { + test('my first test', function () { + ok(false, "this test has run"); + }); + }); + +All testing helpers and structures live in the ``openerp.testing`` +module. OpenERP tests live in a :js:func:`~openerp.testing.section`, +which is itself part of a module. The first argument to a section is +the name of the section, the second one is the section body. + +:js:func:`~openerp.testing.test`, provided by the +:js:func:`~openerp.testing.section` to the callback, is used to +register a given test case which will be run whenever the test runner +actually does its job. OpenERP Web test case use standard `QUnit +assertions`_ within them. + +Launching the test runner at this point will run the test and display +the corresponding assertion message, with red colors indicating the +test failed: + +.. image:: ./images/tests2.png + :align: center + +Fixing the test (by replacing ``false`` to ``true`` in the assertion) +will make it pass: + +.. image:: ./images/tests3.png + :align: center + +Assertions +---------- + +As noted above, OpenERP Web's tests use `qunit assertions`_. They are +available globally (so they can just be called without references to +anything). The following list is available: + +.. js:function:: ok(state[, message]) + + checks that ``state`` is truthy (in the javascript sense) + +.. js:function:: strictEqual(actual, expected[, message]) + + checks that the actual (produced by a method being tested) and + expected values are identical (roughly equivalent to ``ok(actual + === expected, message)``) + +.. js:function:: notStrictEqual(actual, expected[, message]) + + checks that the actual and expected values are *not* identical + (roughly equivalent to ``ok(actual !== expected, message)``) + +.. js:function:: deepEqual(actual, expected[, message]) + + deep comparison between actual and expected: recurse into + containers (objects and arrays) to ensure that they have the same + keys/number of elements, and the values match. + +.. js:function:: notDeepEqual(actual, expected[, message]) + + inverse operation to :js:func:`deepEqual` + +.. js:function:: throws(block[, expected][, message]) + + checks that, when called, the ``block`` throws an + error. Optionally validates that error against ``expected``. + + :param Function block: + :param expected: if a regexp, checks that the thrown error's + message matches the regular expression. If an + error type, checks that the thrown error is of + that type. + :type expected: Error | RegExp + +.. js:function:: equal(actual, expected[, message]) + + checks that ``actual`` and ``expected`` are loosely equal, using + the ``==`` operator and its coercion rules. + +.. js:function:: notEqual(actual, expected[, message]) + + inverse operation to :js:func:`equal` + +Getting an OpenERP instance +--------------------------- + +The OpenERP instance is the base through which most OpenERP Web +modules behaviors (functions, objects, …) are accessed. As a result, +the test framework automatically builds one, and loads the module +being tested and all of its dependencies inside it. This new instance +is provided as the first positional parameter to your test +cases. Let's observe by adding javascript code (not test code) to the +test module: + +.. code-block:: python + + { + 'name': "Demonstration of web/javascript tests", + 'category': 'Hidden', + 'depends': ['web'], + 'js': ['static/src/js/demo.js'], + 'test': ['static/test/demo.js'], + } + +:: + + // src/js/demo.js + openerp.web_tests_demo = function (instance) { + instance.web_tests_demo = { + value_true: true, + SomeType: instance.web.Class.extend({ + init: function (value) { + this.value = value; + } + }) + }; + }; + +and then adding a new test case, which simply checks that the +``instance`` contains all the expected stuff we created in the +module:: + + // test/demo.js + test('module content', function (instance) { + ok(instance.web_tests_demo.value_true, "should have a true value"); + var type_instance = new instance.web_tests_demo.SomeType(42); + strictEqual(type_instance.value, 42, "should have provided value"); + }); + +DOM Scratchpad +-------------- + +As in the wider client, arbitrarily accessing document content is +strongly discouraged during tests. But DOM access is still needed to +e.g. fully initialize :js:class:`widgets <~openerp.web.Widget>` before +testing them. + +Thus, test cases get a DOM scratchpad as its second positional +parameter, in a jQuery instance. That scratchpad is fully cleaned up +before each test, and as long as it doesn't do anything outside the +scrartchpad your code can do whatever it wants:: + + // test/demo.js + test('DOM content', function (instance, $scratchpad) { + $scratchpad.html('

    ok
    '); + ok($scratchpad.find('span').hasClass('foo'), + "should have provided class"); + }); + test('clean scratchpad', function (instance, $scratchpad) { + ok(!$scratchpad.children().length, "should have no content"); + ok(!$scratchpad.text(), "should have no text"); + }); + +.. note:: + + the top-level element of the scratchpad is not cleaned up, test + cases can add text or DOM children but shoud not alter + ``$scratchpad`` itself. + +Loading templates +----------------- + +To avoid the corresponding processing costs, by default templates are +not loaded into QWeb. If you need to render e.g. widgets making use of +QWeb templates, you can request their loading through the +:js:attr:`~TestOptions.templates` option. + +This will automatically load all relevant templates in the instance's +qweb before running the test case: + +.. code-block:: python + + { + 'name': "Demonstration of web/javascript tests", + 'category': 'Hidden', + 'depends': ['web'], + 'js': ['static/src/js/demo.js'], + 'test': ['static/test/demo.js'], + 'qweb': ['static/src/xml/demo.xml'], + } + +.. code-block:: xml + + + + + +

    +
    +
    +
    + +:: + + // test/demo.js + test('templates', {templates: true}, function (instance) { + var s = instance.web.qweb.render('DemoTemplate'); + var texts = $(s).find('p').map(function () { + return $(this).text(); + }).get(); + + deepEqual(texts, ['0', '1', '2', '3', '4']); + }); + +Asynchronous cases +------------------ + +The test case examples so far are all synchronous, they execute from +the first to the last line and once the last line has executed the +test is done. But the web client is full of :doc:`asynchronous code +`, and thus test cases need to be async-aware. + +This is done by returning a :js:class:`deferred ` from the +case callback:: + + // test/demo.js + test('asynchronous', { + asserts: 1 + }, function () { + var d = $.Deferred(); + setTimeout(function () { + ok(true); + d.resolve(); + }, 100); + return d; + }); + +This example also introduces an options object to the test case. In +this case, it's used to specify the number of assertions the test case +should expect, if less or more assertions are specified the case will +count as failed. + +Asynchronous test cases *must* specify the number of assertions they +will run. This allows more easily catching situations where e.g. the +test architecture was not warned about asynchronous operations. + +.. note:: + + asynchronous test cases also have a 2 seconds timeout: if the test + does not finish within 2 seconds, it will be considered + failed. This pretty much always means the test will not resolve. + +.. note:: + + if the returned deferred is rejected, the test will be failed + unless :js:attr:`~TestOptions.fail_on_rejection` is set to + ``false``. + +RPC +--- + +An important subset of asynchronous test cases is test cases which +need to perform (and chain, to an extent) RPC calls. + +.. note:: + + because they are a subset of asynchronous cases, RPC cases must + also provide a valid :js:attr:`assertions count + ` + +By default, test cases will fail when trying to perform an RPC +call. The ability to perform RPC calls must be explicitly requested by +a test case (or its containing test suite) through +:js:attr:`~TestOptions.rpc`, and can be one of two modes: ``mock`` or +``rpc``. + +Mock RPC +++++++++ + +The preferred (and most fastest from a setup and execution time point +of view) way to do RPC during tests is to mock the RPC calls: while +setting up the test case, provide what the RPC responses "should" be, +and only test the code between the "user" (the test itself) and the +RPC call, before the call is effectively done. + +To do this, set the :js:attr:`rpc option <~TestOptions.rpc>` to +``mock``. This will add a third parameter to the test case callback: + +.. js:function:: mock(rpc_spec, handler) + + Can be used in two different ways depending on the shape of the + first parameter: + + * If it matches the pattern ``model:method`` (if it contains a + colon, essentially) the call will set up the mocking of an RPC + call straight to the OpenERP server (through XMLRPC) as + performed via e.g. :js:func:`openerp.web.Model.call`. + + In that case, ``handler`` should be a function taking two + arguments ``args`` and ``kwargs``, matching the corresponding + arguments on the server side. Hander should simply return the + value as if it were returned by the Python XMLRPC handler:: + + test('XML-RPC', {rpc: 'mock', asserts: 3}, function (instance, $s, mock) { + // set up mocking + mock('people.famous:name_search', function (args, kwargs) { + strictEqual(kwargs.name, 'bob'); + return [ + [1, "Microsoft Bob"], + [2, "Bob the Builder"], + [3, "Silent Bob"] + ]; + }); + + // actual test code + return new instance.web.Model('people.famous') + .call('name_search', {name: 'bob'}).pipe(function (result) { + strictEqual(result.length, 3, "shoud return 3 people"); + strictEqual(result[0][1], "Microsoft Bob", + "the most famous bob should be Microsoft Bob"); + }); + }); + + * Otherwise, if it matches an absolute path (e.g. ``/a/b/c``) it + will mock a JSON-RPC call to a web client controller, such as + ``/web/webclient/translations``. In that case, the handler takes + a single ``params`` argument holding all of the parameters + provided over JSON-RPC. + + As previously, the handler should simply return the result value + as if returned by the original JSON-RPC handler:: + + test('JSON-RPC', {rpc: 'mock', asserts: 3, templates: true}, function (instance, $s, mock) { + var fetched_dbs = false, fetched_langs = false; + mock('/web/database/get_list', function () { + fetched_dbs = true; + return ['foo', 'bar', 'baz']; + }); + mock('/web/session/get_lang_list', function () { + fetched_langs = true; + return [['vo_IS', 'Hopelandic / Vonlenska']]; + }); + + // widget needs that or it blows up + instance.webclient = {toggle_bars: openerp.testing.noop}; + var dbm = new instance.web.DatabaseManager({}); + return dbm.appendTo($s).pipe(function () { + ok(fetched_dbs, "should have fetched databases"); + ok(fetched_langs, "should have fetched languages"); + deepEqual(dbm.db_list, ['foo', 'bar', 'baz']); + }); + }); + +.. note:: + + mock handlers can contain assertions, these assertions should be + part of the assertions count (and if multiple calls are made to a + handler containing assertions, it multiplies the effective number + of assertions) + +Actual RPC +++++++++++ + +.. TODO:: rpc to database (implement & document) + +Testing API +----------- + +.. todo:: implement options on sections + +.. js:class:: TestOptions + + the various options which can be passed to + :js:func:`~openerp.testing.section` or + :js:func:`~openerp.testing.case` + + .. js:attribute:: TestOptions.asserts + + An integer, the number of assertions which should run during a + normal execution of the test. Mandatory for asynchronous tests. + + .. js:attribute:: TestOptions.setup + + .. todo:: implement & document setup (async?) + + .. js:attribute:: TestOptions.teardown + + .. todo:: implement & document teardown (async?) + + .. js:attribute:: TestOptions.fail_on_rejection + + If the test is asynchronous and its resulting promise is + rejected, fail the test. Defaults to ``true``, set to + ``false`` to not fail the test in case of rejection:: + + // test/demo.js + test('unfail rejection', { + asserts: 1, + fail_on_rejection: false + }, function () { + var d = $.Deferred(); + setTimeout(function () { + ok(true); + d.reject(); + }, 100); + return d; + }); + + .. js:attribute:: TestOptions.rpc + + RPC method to use during tests, one of ``"mock"`` or + ``"rpc"``. Any other value will disable RPC for the test (if + they were enabled by the suite for instance). + + .. js:attribute:: TestOptions.templates + + Whether the current module (and its dependencies)'s templates + should be loaded into QWeb before starting the test. A + boolean, ``false`` by default. + +Running through Python +---------------------- + +.. todo:: make that work and document it + +.. _qunit: http://qunitjs.com/ + +.. _qunit assertions: http://api.qunitjs.com/category/assert/ diff --git a/addons/web/http.py b/addons/web/http.py index 65630768737..b1d50597bf1 100644 --- a/addons/web/http.py +++ b/addons/web/http.py @@ -537,7 +537,7 @@ class Root(object): :rtype: ``Controller | None`` """ if l: - ps = '/' + '/'.join(l) + ps = '/' + '/'.join(filter(None, l)) meth = 'index' while ps: c = controllers_path.get(ps) diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index 3cb4437d1c3..80f965a6200 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -296,14 +296,14 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ $('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty(); var fetch_db = this.rpc("/web/database/get_list", {}).pipe( function(result) { - self.db_list = result.db_list; + self.db_list = result; }, function (_, ev) { ev.preventDefault(); self.db_list = null; }); var fetch_langs = this.rpc("/web/session/get_lang_list", {}).then(function(result) { - self.lang_list = result.lang_list; + self.lang_list = result; }); return $.when(fetch_db, fetch_langs).then(self.do_render); }, diff --git a/addons/web/static/src/js/testing.js b/addons/web/static/src/js/testing.js new file mode 100644 index 00000000000..4eadc3a5415 --- /dev/null +++ b/addons/web/static/src/js/testing.js @@ -0,0 +1,179 @@ +// Test support structures and methods for OpenERP +openerp.testing = {}; +(function (testing) { + var dependencies = { + corelib: [], + coresetup: ['corelib'], + data: ['corelib', 'coresetup'], + dates: [], + formats: ['coresetup', 'dates'], + chrome: ['corelib', 'coresetup'], + views: ['corelib', 'coresetup', 'data', 'chrome'], + search: ['data', 'coresetup', 'formats'], + list: ['views', 'data'], + form: ['data', 'views', 'list', 'formats'], + list_editable: ['list', 'form', 'data'], + }; + + testing.dependencies = window['oe_all_dependencies'] || []; + testing.current_module = null; + testing.templates = { }; + testing.add_template = function (name) { + var xhr = QWeb2.Engine.prototype.get_xhr(); + xhr.open('GET', name, false); + xhr.send(null); + (testing.templates[testing.current_module] = + testing.templates[testing.current_module] || []) + .push(xhr.responseXML); + }; + /** + * Function which does not do anything + */ + testing.noop = function () { }; + /** + * Alter provided instance's ``session`` attribute to make response + * mockable: + * + * * The ``responses`` parameter can be used to provide a map of (RPC) + * paths (e.g. ``/web/view/load``) to a function returning a response + * to the query. + * * ``instance.session`` grows a ``responses`` attribute which is + * a map of the same (and is in fact initialized to the ``responses`` + * parameter if one is provided) + * + * Note that RPC requests to un-mocked URLs will be rejected with an + * error message: only explicitly specified urls will get a response. + * + * Mocked sessions will *never* perform an actual RPC connection. + * + * @param instance openerp instance being initialized + * @param {Object} [responses] + */ + testing.mockifyRPC = function (instance, responses) { + var session = instance.session; + session.responses = responses || {}; + session.rpc_function = function (url, payload) { + var fn, params; + var needle = payload.params.model + ':' + payload.params.method; + if (url.url === '/web/dataset/call_kw' + && needle in this.responses) { + fn = this.responses[needle]; + params = [ + payload.params.args || [], + payload.params.kwargs || {} + ]; + } else { + fn = this.responses[url.url]; + params = [payload]; + } + + if (!fn) { + return $.Deferred().reject({}, 'failed', + _.str.sprintf("Url %s not found in mock responses, with arguments %s", + url.url, JSON.stringify(payload.params)) + ).promise(); + } + try { + return $.when(fn.apply(null, params)).pipe(function (result) { + // Wrap for RPC layer unwrapper thingy + return {result: result}; + }); + } catch (e) { + // not sure why this looks like that + return $.Deferred().reject({}, 'failed', String(e)); + } + }; + }; + + var _load = function (instance, module, loaded) { + if (!loaded) { loaded = []; } + + var deps = dependencies[module]; + if (!deps) { throw new Error("Unknown dependencies for " + module); } + + var to_load = _.difference(deps, loaded); + while (!_.isEmpty(to_load)) { + _load(instance, to_load[0], loaded); + to_load = _.difference(deps, loaded); + } + openerp.web[module](instance); + loaded.push(module); + }; + + testing.section = function (name, body) { + QUnit.module(testing.current_module + '.' + name); + body(testing.case); + }; + testing.case = function (name, options, callback) { + if (_.isFunction(options)) { + callback = options; + options = {}; + } + + var module = testing.current_module; + var module_index = _.indexOf(testing.dependencies, module); + var module_deps = testing.dependencies.slice( + // If module not in deps (because only tests, no JS) -> indexOf + // returns -1 -> index becomes 0 -> replace with ``undefined`` so + // Array#slice returns a full copy + 0, module_index + 1 || undefined); + QUnit.test(name, function (env) { + var instance = openerp.init(module_deps); + if (_.isNumber(options.asserts)) { + expect(options.asserts) + } + + if (options.templates) { + for(var i=0; i - - - - OpenERP Web Test Suite - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - - - - - - - - - - diff --git a/addons/web/static/test/testing.js b/addons/web/static/test/testing.js deleted file mode 100644 index 1be8e94e74b..00000000000 --- a/addons/web/static/test/testing.js +++ /dev/null @@ -1,97 +0,0 @@ -// Test support structures and methods for OpenERP -openerp.testing = (function () { - var xhr = QWeb2.Engine.prototype.get_xhr(); - xhr.open('GET', '/web/static/src/xml/base.xml', false); - xhr.send(null); - var doc = xhr.responseXML; - - var dependencies = { - corelib: [], - coresetup: ['corelib'], - data: ['corelib', 'coresetup'], - dates: [], - formats: ['coresetup', 'dates'], - chrome: ['corelib', 'coresetup'], - views: ['corelib', 'coresetup', 'data', 'chrome'], - search: ['data', 'coresetup', 'formats'], - list: ['views', 'data'], - form: ['data', 'views', 'list', 'formats'], - list_editable: ['list', 'form', 'data'], - }; - - return { - /** - * Function which does not do anything - */ - noop: function () { }, - /** - * Loads 'base.xml' template file into qweb for the provided instance - * - * @param instance openerp instance being initialized, to load the template file in - */ - loadTemplate: function (instance) { - instance.web.qweb.add_template(doc); - }, - /** - * Alter provided instance's ``session`` attribute to make response - * mockable: - * - * * The ``responses`` parameter can be used to provide a map of (RPC) - * paths (e.g. ``/web/view/load``) to a function returning a response - * to the query. - * * ``instance.session`` grows a ``responses`` attribute which is - * a map of the same (and is in fact initialized to the ``responses`` - * parameter if one is provided) - * - * Note that RPC requests to un-mocked URLs will be rejected with an - * error message: only explicitly specified urls will get a response. - * - * Mocked sessions will *never* perform an actual RPC connection. - * - * @param instance openerp instance being initialized - * @param {Object} [responses] - */ - mockifyRPC: function (instance, responses) { - var session = instance.session; - session.responses = responses || {}; - session.rpc_function = function (url, payload) { - var fn = this.responses[url.url + ':' + payload.params.method] - || this.responses[url.url]; - - if (!fn) { - return $.Deferred().reject({}, 'failed', - _.str.sprintf("Url %s not found in mock responses, with arguments %s", - url.url, JSON.stringify(payload.params)) - ).promise(); - } - return $.when(fn(payload)); - }; - }, - /** - * Creates an openerp web instance loading the specified module after - * all of its dependencies. - * - * @param {String} module - * @returns OpenERP Web instance - */ - instanceFor: function (module) { - var instance = openerp.init([]); - this._load(instance, module); - return instance; - }, - _load: function (instance, module, loaded) { - if (!loaded) { loaded = []; } - - var deps = dependencies[module]; - if (!deps) { throw new Error("Unknown dependencies for " + module); } - - var to_load = _.difference(deps, loaded); - while (!_.isEmpty(to_load)) { - this._load(instance, to_load[0], loaded); - to_load = _.difference(deps, loaded); - } - openerp.web[module](instance); - loaded.push(module); - } - } -})(); diff --git a/addons/web_graph/static/lib/flotr2/lib/bean.js b/addons/web_graph/static/lib/flotr2/lib/bean.js index 6e6e3ef4eb5..1a854771006 100644 --- a/addons/web_graph/static/lib/flotr2/lib/bean.js +++ b/addons/web_graph/static/lib/flotr2/lib/bean.js @@ -9,9 +9,7 @@ */ /*global module:true, define:true*/ !function (name, context, definition) { - if (typeof module !== 'undefined') module.exports = definition(name, context); - else if (typeof define === 'function' && typeof define.amd === 'object') define(definition); - else context[name] = definition(name, context); + context[name] = definition(name, context); }('bean', this, function (name, context) { var win = window , old = context[name] diff --git a/addons/web_tests_demo/__init__.py b/addons/web_tests_demo/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/addons/web_tests_demo/__openerp__.py b/addons/web_tests_demo/__openerp__.py new file mode 100644 index 00000000000..7404ef6b4fc --- /dev/null +++ b/addons/web_tests_demo/__openerp__.py @@ -0,0 +1,8 @@ +{ + 'name': "Demonstration of web/javascript tests", + 'category': 'Hidden', + 'depends': ['web'], + 'js': ['static/src/js/demo.js'], + 'test': ['static/test/demo.js'], + 'qweb': ['static/src/xml/demo.xml'], +} diff --git a/addons/web_tests_demo/static/src/js/demo.js b/addons/web_tests_demo/static/src/js/demo.js new file mode 100644 index 00000000000..b35b44b80f8 --- /dev/null +++ b/addons/web_tests_demo/static/src/js/demo.js @@ -0,0 +1,11 @@ +// static/src/js/demo.js +openerp.web_tests_demo = function (instance) { + instance.web_tests_demo = { + value_true: true, + SomeType: instance.web.Class.extend({ + init: function (value) { + this.value = value; + } + }) + }; +}; diff --git a/addons/web_tests_demo/static/src/xml/demo.xml b/addons/web_tests_demo/static/src/xml/demo.xml new file mode 100644 index 00000000000..1bd3862d70e --- /dev/null +++ b/addons/web_tests_demo/static/src/xml/demo.xml @@ -0,0 +1,7 @@ + +
    + +

    +
    +
    +
    diff --git a/addons/web_tests_demo/static/test/demo.js b/addons/web_tests_demo/static/test/demo.js new file mode 100644 index 00000000000..0bf5a1a58b6 --- /dev/null +++ b/addons/web_tests_demo/static/test/demo.js @@ -0,0 +1,87 @@ +openerp.testing.section('basic section', function (test) { + test('my first test', function () { + ok(true, "this test has run"); + }); + test('module content', function (instance) { + ok(instance.web_tests_demo.value_true, "should have a true value"); + var type_instance = new instance.web_tests_demo.SomeType(42); + strictEqual(type_instance.value, 42, "should have provided value"); + }); + test('DOM content', function (instance, $scratchpad) { + $scratchpad.html('
    ok
    '); + ok($scratchpad.find('span').hasClass('foo'), + "should have provided class"); + }); + test('clean scratchpad', function (instance, $scratchpad) { + ok(!$scratchpad.children().length, "should have no content"); + ok(!$scratchpad.text(), "should have no text"); + }); + + test('templates', {templates: true}, function (instance) { + var s = instance.web.qweb.render('DemoTemplate'); + var texts = $(s).find('p').map(function () { + return $(this).text(); + }).get(); + + deepEqual(texts, ['0', '1', '2', '3', '4']); + }); + + test('asynchronous', { + asserts: 1 + }, function () { + var d = $.Deferred(); + setTimeout(function () { + ok(true); + d.resolve(); + }, 100); + return d; + }); + test('unfail rejection', { + asserts: 1, + fail_on_rejection: false + }, function () { + var d = $.Deferred(); + setTimeout(function () { + ok(true); + d.reject(); + }, 100); + return d; + }); + + test('XML-RPC', {rpc: 'mock', asserts: 3}, function (instance, $s, mock) { + mock('people.famous:name_search', function (args, kwargs) { + strictEqual(kwargs.name, 'bob'); + return [ + [1, "Microsoft Bob"], + [2, "Bob the Builder"], + [3, "Silent Bob"] + ]; + }); + return new instance.web.Model('people.famous') + .call('name_search', {name: 'bob'}).pipe(function (result) { + strictEqual(result.length, 3, "shoud return 3 people"); + strictEqual(result[0][1], "Microsoft Bob", + "the most famous bob should be Microsoft Bob"); + }); + }); + test('JSON-RPC', {rpc: 'mock', asserts: 3, templates: true}, function (instance, $s, mock) { + var fetched_dbs = false, fetched_langs = false; + mock('/web/database/get_list', function () { + fetched_dbs = true; + return ['foo', 'bar', 'baz']; + }); + mock('/web/session/get_lang_list', function () { + fetched_langs = true; + return [['vo_IS', 'Hopelandic / Vonlenska']]; + }); + + // widget needs that or it blows up + instance.webclient = {toggle_bars: openerp.testing.noop}; + var dbm = new instance.web.DatabaseManager({}); + return dbm.appendTo($s).pipe(function () { + ok(fetched_dbs, "should have fetched databases"); + ok(fetched_langs, "should have fetched languages"); + deepEqual(dbm.db_list, ['foo', 'bar', 'baz']); + }); + }); +}); From 04282ff00e8fa70c3eb0e9a48ba0cd32aa0e1cd7 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 26 Oct 2012 10:45:43 +0200 Subject: [PATCH 017/191] [IMP] port search test to new framework, add handling of web.{submodule} dependencies bzr revid: xmo@openerp.com-20121026084543-fobkc3ta5q2jc3q6 --- addons/web/doc/testing.rst | 35 +- addons/web/static/src/js/boot.js | 10 +- addons/web/static/src/js/testing.js | 111 +++-- addons/web/static/src/js/view_list.js | 2 +- addons/web/static/src/js/views.js | 3 +- addons/web/static/test/search.js | 655 +++++++++++--------------- 6 files changed, 395 insertions(+), 421 deletions(-) diff --git a/addons/web/doc/testing.rst b/addons/web/doc/testing.rst index 34826201dc9..582960846e4 100644 --- a/addons/web/doc/testing.rst +++ b/addons/web/doc/testing.rst @@ -417,26 +417,49 @@ Actual RPC Testing API ----------- -.. todo:: implement options on sections +.. js:function:: openerp.testing.section(name[, options], body) + + :param String name: + :param TestOptions options: + :param body: + :type body: Function<:js:func:`~openerp.testing.case`, void> + +.. js:function:: openerp.testing.case(name[, options], callback) + + :param String name: + :param TestOptions options: + :param callback: + :type callback: Function> .. js:class:: TestOptions the various options which can be passed to :js:func:`~openerp.testing.section` or - :js:func:`~openerp.testing.case` + :js:func:`~openerp.testing.case`. Except for + :js:attr:`~TestOptions.setup` and + :js:attr:`~TestOptions.teardown`, an option on + :js:func:`~openerp.testing.case` will overwrite the corresponding + option on :js:func:`~openerp.testing.section` so + e.g. :js:attr:`~TestOptions.rpc` can be set for a + :js:func:`~openerp.testing.section` and then differently set for + some :js:func:`~openerp.testing.case` of that + :js:func:`~openerp.testing.section` .. js:attribute:: TestOptions.asserts - + An integer, the number of assertions which should run during a normal execution of the test. Mandatory for asynchronous tests. - + .. js:attribute:: TestOptions.setup - .. todo:: implement & document setup (async?) + Test case setup, run right before each test case. A section's + :js:func:`~TestOptions.setup` is run before the case's own, if + both are specified. .. js:attribute:: TestOptions.teardown - .. todo:: implement & document teardown (async?) + Test case teardown, a case's :js:func:`~TestOptions.teardown` + is run before the corresponding section if both are present. .. js:attribute:: TestOptions.fail_on_rejection diff --git a/addons/web/static/src/js/boot.js b/addons/web/static/src/js/boot.js index 05ce709e80a..631a7f8c6f5 100644 --- a/addons/web/static/src/js/boot.js +++ b/addons/web/static/src/js/boot.js @@ -19,12 +19,14 @@ /** * OpenERP instance constructor * - * @param {Array} modules list of modules to initialize + * @param {Array|String} modules list of modules to initialize */ init: function(modules) { - // By default only web will be loaded, the rest will be by loaded - // by openerp.web.Session on the first session_authenticate - modules = _.union(['web'], modules || []); + if (modules === "fuck your shit, don't load anything you cunt") { + modules = []; + } else { + modules = _.union(['web'], modules || []); + } var new_instance = { // links to the global openerp _openerp: openerp, diff --git a/addons/web/static/src/js/testing.js b/addons/web/static/src/js/testing.js index 4eadc3a5415..e4f327bf0c3 100644 --- a/addons/web/static/src/js/testing.js +++ b/addons/web/static/src/js/testing.js @@ -85,23 +85,17 @@ openerp.testing = {}; }; }; - var _load = function (instance, module, loaded) { - if (!loaded) { loaded = []; } - - var deps = dependencies[module]; - if (!deps) { throw new Error("Unknown dependencies for " + module); } - - var to_load = _.difference(deps, loaded); - while (!_.isEmpty(to_load)) { - _load(instance, to_load[0], loaded); - to_load = _.difference(deps, loaded); + testing.section = function (name, options, body) { + if (_.isFunction(options)) { + body = options; + options = {}; } - openerp.web[module](instance); - loaded.push(module); - }; + _.defaults(options, { + setup: testing.noop, + teardown: testing.noop + }); - testing.section = function (name, body) { - QUnit.module(testing.current_module + '.' + name); + QUnit.module(testing.current_module + '.' + name, {_oe: options}); body(testing.case); }; testing.case = function (name, options, callback) { @@ -109,6 +103,10 @@ openerp.testing = {}; callback = options; options = {}; } + _.defaults(options, { + setup: testing.noop, + teardown: testing.noop + }); var module = testing.current_module; var module_index = _.indexOf(testing.dependencies, module); @@ -117,13 +115,66 @@ openerp.testing = {}; // returns -1 -> index becomes 0 -> replace with ``undefined`` so // Array#slice returns a full copy 0, module_index + 1 || undefined); - QUnit.test(name, function (env) { - var instance = openerp.init(module_deps); - if (_.isNumber(options.asserts)) { - expect(options.asserts) + QUnit.test(name, function () { + // module testing environment + var self = this; + var opts = _.defaults({ + // section setup + // case setup + // test + // case teardown + // section teardown + setup: function () { + if (self._oe.setup.apply(null, arguments)) { + throw new Error("Asynchronous setup not implemented"); + } + if (options.setup.apply(null, arguments)) { + throw new Error("Asynchronous setup not implemented"); + } + }, + teardown: function () { + if (options.teardown.apply(null, arguments)) { + throw new Error("Asynchronous teardown not implemented"); + } + if (self._oe.teardown(null, arguments)) { + throw new Error("Asynchronous teardown not implemented"); + } + } + }, options, this._oe); + + var instance; + if (!opts.dependencies) { + instance = openerp.init(module_deps); + } else { + // empty-but-specified dependencies actually allow running + // without loading any module into the instance + + // TODO: clean up this mess + var d = opts.dependencies.slice(); + var di = 0; + while (di < d.length) { + var m = /^web\.(\w+)$/.exec(d[di]); + if (m) { + d[di] = m[1]; + } + d.splice.apply(d, [di+1, 0].concat( + _(dependencies[d[di]]).reverse())); + ++di; + } + + instance = openerp.init("fuck your shit, don't load anything you cunt"); + _(d).chain() + .reverse() + .uniq() + .each(function (module) { + openerp.web[module](instance); + }); + } + if (_.isNumber(opts.asserts)) { + expect(opts.asserts); } - if (options.templates) { + if (opts.templates) { for(var i=0; i Date: Fri, 26 Oct 2012 11:21:44 +0200 Subject: [PATCH 018/191] [IMP] port basic/existing tests to new framework, apart from editor stuff bzr revid: xmo@openerp.com-20121026092144-0arxx2v9oi28e3d6 --- addons/web/static/src/js/testing.js | 3 + addons/web/static/test/Widget.js | 158 +++++++++++-------- addons/web/static/test/class.js | 67 ++++---- addons/web/static/test/evals.js | 39 ++--- addons/web/static/test/form.js | 40 ++--- addons/web/static/test/formats.js | 199 +++++++++++------------- addons/web/static/test/list-utils.js | 223 +++++++++++---------------- addons/web/static/test/registry.js | 77 +++++---- addons/web/static/test/rpc.js | 48 +++--- 9 files changed, 395 insertions(+), 459 deletions(-) diff --git a/addons/web/static/src/js/testing.js b/addons/web/static/src/js/testing.js index e4f327bf0c3..45728902e7f 100644 --- a/addons/web/static/src/js/testing.js +++ b/addons/web/static/src/js/testing.js @@ -151,6 +151,9 @@ openerp.testing = {}; // TODO: clean up this mess var d = opts.dependencies.slice(); + // dependencies list should be in deps order, reverse to make + // loading order from last + d.reverse(); var di = 0; while (di < d.length) { var m = /^web\.(\w+)$/.exec(d[di]); diff --git a/addons/web/static/test/Widget.js b/addons/web/static/test/Widget.js index 3e6cc262565..44b7cfe0dfa 100644 --- a/addons/web/static/test/Widget.js +++ b/addons/web/static/test/Widget.js @@ -1,32 +1,7 @@ -$(document).ready(function () { - var $fix = $('#qunit-fixture'); - var mod = { - setup: function () { - instance = window.openerp.init([]); - window.openerp.web.corelib(instance); - - instance.web.qweb = new QWeb2.Engine(); - instance.web.qweb.add_template( - '' + - '' + - '
      ' + - '
    1. ' + - '' + - '' + - '
    2. ' + - '
    ' + - '
    ' + - '' + - '

    ' + - '
    ' + - '
    '); - } - }; - var instance; - - module('Widget.proxy', mod); - test('(String)', function () { +openerp.testing.section('Widget.proxy', { + dependencies: ['web.corelib'] +}, function (test) { + test('(String)', function (instance) { var W = instance.web.Widget.extend({ exec: function () { this.executed = true; @@ -37,7 +12,7 @@ $(document).ready(function () { fn(); ok(w.executed, 'should execute the named method in the right context'); }); - test('(String)(*args)', function () { + test('(String)(*args)', function (instance) { var W = instance.web.Widget.extend({ exec: function (arg) { this.executed = arg; @@ -49,7 +24,7 @@ $(document).ready(function () { ok(w.executed, "should execute the named method in the right context"); equal(w.executed, 42, "should be passed the proxy's arguments"); }); - test('(String), include', function () { + test('(String), include', function (instance) { // the proxy function should handle methods being changed on the class // and should always proxy "by name", to the most recent one var W = instance.web.Widget.extend({ @@ -67,23 +42,43 @@ $(document).ready(function () { equal(w.executed, 2, "should be lazily resolved"); }); - test('(Function)', function () { + test('(Function)', function (instance) { var w = new (instance.web.Widget.extend({ })); var fn = w.proxy(function () { this.executed = true; }); fn(); ok(w.executed, "should set the function's context (like Function#bind)"); }); - test('(Function)(*args)', function () { + test('(Function)(*args)', function (instance) { var w = new (instance.web.Widget.extend({ })); var fn = w.proxy(function (arg) { this.executed = arg; }); fn(42); equal(w.executed, 42, "should be passed the proxy's arguments"); }); - - module('Widget.renderElement', mod); - test('no template, default', function () { +}); +openerp.testing.section('Widget.renderElement', { + dependencies: ['web.corelib'], + setup: function (instance) { + instance.web.qweb = new QWeb2.Engine(); + instance.web.qweb.add_template( + '' + + '' + + '
      ' + + '
    1. ' + + '' + + '' + + '
    2. ' + + '
    ' + + '
    ' + + '' + + '

    ' + + '
    ' + + '
    '); + } +}, function (test) { + test('no template, default', function (instance) { var w = new (instance.web.Widget.extend({ })); var $original = w.$el; @@ -98,7 +93,7 @@ $(document).ready(function () { equal(w.el.attributes.length, 0, "should not have generated any attribute"); ok(_.isEmpty(w.$el.html(), "should not have generated any content")); }); - test('no template, custom tag', function () { + test('no template, custom tag', function (instance) { var w = new (instance.web.Widget.extend({ tagName: 'ul' })); @@ -106,7 +101,7 @@ $(document).ready(function () { equal(w.el.nodeName, 'UL', "should have generated the custom element tag"); }); - test('no template, @id', function () { + test('no template, @id', function (instance) { var w = new (instance.web.Widget.extend({ id: 'foo' })); @@ -116,7 +111,7 @@ $(document).ready(function () { equal(w.$el.attr('id'), 'foo', "should have generated the id attribute"); equal(w.el.id, 'foo', "should also be available via property"); }); - test('no template, @className', function () { + test('no template, @className', function (instance) { var w = new (instance.web.Widget.extend({ className: 'oe_some_class' })); @@ -125,7 +120,7 @@ $(document).ready(function () { equal(w.el.className, 'oe_some_class', "should have the right property"); equal(w.$el.attr('class'), 'oe_some_class', "should have the right attribute"); }); - test('no template, bunch of attributes', function () { + test('no template, bunch of attributes', function (instance) { var w = new (instance.web.Widget.extend({ attributes: { 'id': 'some_id', @@ -152,7 +147,7 @@ $(document).ready(function () { equal(w.$el.attr('spoiler'), 'snape kills dumbledore'); }); - test('template', function () { + test('template', function (instance) { var w = new (instance.web.Widget.extend({ template: 'test.widget.template' })); @@ -162,9 +157,41 @@ $(document).ready(function () { equal(w.$el.children().length, 5); equal(w.el.textContent, '01234'); }); - - module('Widget.$', mod); - test('basic-alias', function () { + test('repeated', { asserts: 4 }, function (instance, $fix) { + var w = new (instance.web.Widget.extend({ + template: 'test.widget.template-value' + })); + w.value = 42; + return w.appendTo($fix) + .done(function () { + equal($fix.find('p').text(), '42', "DOM fixture should contain initial value"); + equal(w.$el.text(), '42', "should set initial value"); + w.value = 36; + w.renderElement(); + equal($fix.find('p').text(), '36', "DOM fixture should use new value"); + equal(w.$el.text(), '36', "should set new value"); + }); + }); +}); +openerp.testing.section('Widget.$', { + dependencies: ['web.corelib'], + setup: function (instance) { + instance.web.qweb = new QWeb2.Engine(); + instance.web.qweb.add_template( + '' + + '' + + '
      ' + + '
    1. ' + + '' + + '' + + '
    2. ' + + '
    ' + + '
    ' + + '
    '); + } +}, function (test) { + test('basic-alias', function (instance) { var w = new (instance.web.Widget.extend({ template: 'test.widget.template' })); @@ -173,9 +200,26 @@ $(document).ready(function () { ok(w.$('li:eq(3)').is(w.$el.find('li:eq(3)')), "should do the same thing as calling find on the widget root"); }); - - module('Widget.events', mod); - test('delegate', function () { +}); +openerp.testing.section('Widget.events', { + dependencies: ['web.corelib'], + setup: function (instance) { + instance.web.qweb = new QWeb2.Engine(); + instance.web.qweb.add_template( + '' + + '' + + '
      ' + + '
    1. ' + + '' + + '' + + '
    2. ' + + '
    ' + + '
    ' + + '
    '); + } +}, function (test) { + test('delegate', function (instance) { var a = []; var w = new (instance.web.Widget.extend({ template: 'test.widget.template', @@ -199,7 +243,7 @@ $(document).ready(function () { ok(a[i], "should pass test " + i); } }); - test('undelegate', function () { + test('undelegate', function (instance) { var clicked = false, newclicked = false; var w = new (instance.web.Widget.extend({ template: 'test.widget.template', @@ -218,22 +262,4 @@ $(document).ready(function () { ok(!clicked, "undelegate should unbind events delegated"); ok(newclicked, "undelegate should only unbind events it created"); }); - - module('Widget.renderElement', mod); - asyncTest('repeated', 4, function () { - var w = new (instance.web.Widget.extend({ - template: 'test.widget.template-value' - })); - w.value = 42; - w.appendTo($fix) - .always(start) - .done(function () { - equal($fix.find('p').text(), '42', "DOM fixture should contain initial value"); - equal(w.$el.text(), '42', "should set initial value"); - w.value = 36; - w.renderElement(); - equal($fix.find('p').text(), '36', "DOM fixture should use new value"); - equal(w.$el.text(), '36', "should set new value"); - }); - }); }); diff --git a/addons/web/static/test/class.js b/addons/web/static/test/class.js index faad4421e31..bb3fb7e12b9 100644 --- a/addons/web/static/test/class.js +++ b/addons/web/static/test/class.js @@ -1,30 +1,25 @@ -$(document).ready(function () { - var openerp; - module('web-class', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - } - }); - test('Basic class creation', function () { - var C = openerp.web.Class.extend({ +openerp.testing.section('class', { + dependencies: ['web.corelib'] +}, function (test) { + test('Basic class creation', function (instance) { + var C = instance.web.Class.extend({ foo: function () { return this.somevar; } }); - var instance = new C(); - instance.somevar = 3; + var i = new C(); + i.somevar = 3; - ok(instance instanceof C); - strictEqual(instance.foo(), 3); + ok(i instanceof C); + strictEqual(i.foo(), 3); }); - test('Class initialization', function () { - var C1 = openerp.web.Class.extend({ + test('Class initialization', function (instance) { + var C1 = instance.web.Class.extend({ init: function () { this.foo = 3; } }); - var C2 = openerp.web.Class.extend({ + var C2 = instance.web.Class.extend({ init: function (arg) { this.foo = arg; } @@ -36,8 +31,8 @@ $(document).ready(function () { strictEqual(i1.foo, 3); strictEqual(i2.foo, 42); }); - test('Inheritance', function () { - var C0 = openerp.web.Class.extend({ + test('Inheritance', function (instance) { + var C0 = instance.web.Class.extend({ foo: function () { return 1; } @@ -57,8 +52,8 @@ $(document).ready(function () { strictEqual(new C1().foo(), 2); strictEqual(new C2().foo(), 3); }); - test('In-place extension', function () { - var C0 = openerp.web.Class.extend({ + test('In-place extension', function (instance) { + var C0 = instance.web.Class.extend({ foo: function () { return 3; }, @@ -83,8 +78,8 @@ $(document).ready(function () { strictEqual(new C0().foo(), 5); strictEqual(new C0().qux(), 5); }); - test('In-place extension and inheritance', function () { - var C0 = openerp.web.Class.extend({ + test('In-place extension and inheritance', function (instance) { + var C0 = instance.web.Class.extend({ foo: function () { return 1; }, bar: function () { return 1; } }); @@ -101,24 +96,24 @@ $(document).ready(function () { strictEqual(new C1().foo(), 4); strictEqual(new C1().bar(), 2); }); - test('In-place extensions alter existing instances', function () { - var C0 = openerp.web.Class.extend({ + test('In-place extensions alter existing instances', function (instance) { + var C0 = instance.web.Class.extend({ foo: function () { return 1; }, bar: function () { return 1; } }); - var instance = new C0(); - strictEqual(instance.foo(), 1); - strictEqual(instance.bar(), 1); + var i = new C0(); + strictEqual(i.foo(), 1); + strictEqual(i.bar(), 1); C0.include({ foo: function () { return 2; }, bar: function () { return 2 + this._super(); } }); - strictEqual(instance.foo(), 2); - strictEqual(instance.bar(), 3); + strictEqual(i.foo(), 2); + strictEqual(i.bar(), 3); }); - test('In-place extension of subclassed types', function () { - var C0 = openerp.web.Class.extend({ + test('In-place extension of subclassed types', function (instance) { + var C0 = instance.web.Class.extend({ foo: function () { return 1; }, bar: function () { return 1; } }); @@ -126,13 +121,13 @@ $(document).ready(function () { foo: function () { return 1 + this._super(); }, bar: function () { return 1 + this._super(); } }); - var instance = new C1(); - strictEqual(instance.foo(), 2); + var i = new C1(); + strictEqual(i.foo(), 2); C0.include({ foo: function () { return 2; }, bar: function () { return 2 + this._super(); } }); - strictEqual(instance.foo(), 3); - strictEqual(instance.bar(), 4); + strictEqual(i.foo(), 3); + strictEqual(i.bar(), 4); }); }); diff --git a/addons/web/static/test/evals.js b/addons/web/static/test/evals.js index 757d98c5bcc..6eec615b199 100644 --- a/addons/web/static/test/evals.js +++ b/addons/web/static/test/evals.js @@ -1,18 +1,11 @@ -$(document).ready(function () { - var openerp; - - module("eval.contexts", { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - } - }); - test('context_sequences', function () { +openerp.testing.section('eval.contexts', { + dependencies: ['web.coresetup'] +}, function (test) { + test('context_sequences', function (instance) { // Context n should have base evaluation context + all of contexts // 0..n-1 in its own evaluation context var active_id = 4; - var result = openerp.session.test_eval_contexts([ + var result = instance.session.test_eval_contexts([ { "__contexts": [ { @@ -55,8 +48,8 @@ $(document).ready(function () { record_id: active_id }); }); - test('non-literal_eval_contexts', function () { - var result = openerp.session.test_eval_contexts([{ + test('non-literal_eval_contexts', function (instance) { + var result = instance.session.test_eval_contexts([{ "__ref": "compound_context", "__contexts": [ {"__ref": "context", "__debug": "{'type':parent.type}", @@ -133,17 +126,15 @@ $(document).ready(function () { }]); deepEqual(result, {type: 'out_invoice'}); }); - module('eval.domains', { - setup: function () { - openerp = window.openerp.testing.instanceFor('coresetup'); - window.openerp.web.dates(openerp); - } - }); - test('current_date', function () { - var current_date = openerp.web.date_to_str(new Date()); - var result = openerp.session.test_eval_domains( +}); +openerp.testing.section('eval.contexts', { + dependencies: ['web.coresetup', 'web.dates'] +}, function (test) { + test('current_date', function (instance) { + var current_date = instance.web.date_to_str(new Date()); + var result = instance.session.test_eval_domains( [[],{"__ref":"domain","__debug":"[('name','>=',current_date),('name','<=',current_date)]","__id":"5dedcfc96648"}], - openerp.session.test_eval_get_context()); + instance.session.test_eval_get_context()); deepEqual(result, [ ['name', '>=', current_date], ['name', '<=', current_date] diff --git a/addons/web/static/test/form.js b/addons/web/static/test/form.js index aace926a96a..0c8107ce9bc 100644 --- a/addons/web/static/test/form.js +++ b/addons/web/static/test/form.js @@ -1,33 +1,21 @@ -$(document).ready(function () { - var openerp; - module("form.widget", { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.chrome(openerp); - // views loader stuff - window.openerp.web.data(openerp); - window.openerp.web.views(openerp); - window.openerp.web.list(openerp); - window.openerp.web.form(openerp); - } - }); - test("compute_domain", function () { +openerp.testing.section('compute_domain', { + dependencies: ['web.form'] +}, function (test) { + test("basic", function (instance) { var fields = { 'a': {value: 3}, 'group_method': {value: 'line'}, 'select1': {value: 'day'}, 'rrule_type': {value: 'monthly'} }; - ok(openerp.web.form.compute_domain( + ok(instance.web.form.compute_domain( [['a', '=', 3]], fields)); - ok(openerp.web.form.compute_domain( + ok(instance.web.form.compute_domain( [['group_method','!=','count']], fields)); - ok(openerp.web.form.compute_domain( + ok(instance.web.form.compute_domain( [['select1','=','day'], ['rrule_type','=','monthly']], fields)); }); - test("compute_domain or", function () { + test("or", function (instance) { var web = { 'section_id': {value: null}, 'user_id': {value: null}, @@ -38,22 +26,22 @@ $(document).ready(function () { '|', ['user_id','=',3], ['member_ids', 'in', [3]]]; - ok(openerp.web.form.compute_domain(domain, _.extend( + ok(instance.web.form.compute_domain(domain, _.extend( {}, web, {'section_id': {value: 42}}))); - ok(openerp.web.form.compute_domain(domain, _.extend( + ok(instance.web.form.compute_domain(domain, _.extend( {}, web, {'user_id': {value: 3}}))); - ok(openerp.web.form.compute_domain(domain, _.extend( + ok(instance.web.form.compute_domain(domain, _.extend( {}, web, {'member_ids': {value: 3}}))); }); - test("compute_domain not", function () { + test("not", function (instance) { var fields = { 'a': {value: 5}, 'group_method': {value: 'line'} }; - ok(openerp.web.form.compute_domain( + ok(instance.web.form.compute_domain( ['!', ['a', '=', 3]], fields)); - ok(openerp.web.form.compute_domain( + ok(instance.web.form.compute_domain( ['!', ['group_method','=','count']], fields)); }); }); diff --git a/addons/web/static/test/formats.js b/addons/web/static/test/formats.js index 4fe07ff0a9c..4536501d71d 100644 --- a/addons/web/static/test/formats.js +++ b/addons/web/static/test/formats.js @@ -1,16 +1,8 @@ -$(document).ready(function () { - var openerp; - - module('server-formats', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.dates(openerp); - } - }); - test('Parse server datetime', function () { - var date = openerp.web.str_to_datetime("2009-05-04 12:34:23"); +openerp.testing.section('server-formats', { + dependencies: ['web.coresetup', 'web.dates'] +}, function (test) { + test('Parse server datetime', function (instance) { + var date = instance.web.str_to_datetime("2009-05-04 12:34:23"); deepEqual( [date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()], @@ -20,92 +12,86 @@ $(document).ready(function () { date.getHours(), date.getMinutes(), date.getSeconds()], [2009, 5 - 1, 4, 12 - (date.getTimezoneOffset() / 60), 34, 23]); - var date2 = openerp.web.str_to_datetime('2011-12-10 00:00:00'); + var date2 = instance.web.str_to_datetime('2011-12-10 00:00:00'); deepEqual( [date2.getUTCFullYear(), date2.getUTCMonth(), date2.getUTCDate(), date2.getUTCHours(), date2.getUTCMinutes(), date2.getUTCSeconds()], [2011, 12 - 1, 10, 0, 0, 0]); }); - test('Parse server date', function () { - var date = openerp.web.str_to_date("2009-05-04"); + test('Parse server date', function (instance) { + var date = instance.web.str_to_date("2009-05-04"); deepEqual( [date.getFullYear(), date.getMonth(), date.getDate()], [2009, 5 - 1, 4]); }); - test('Parse server time', function () { - var date = openerp.web.str_to_time("12:34:23"); + test('Parse server time', function (instance) { + var date = instance.web.str_to_time("12:34:23"); deepEqual( [date.getHours(), date.getMinutes(), date.getSeconds()], [12, 34, 23]); }); - - module('web-formats', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.dates(openerp); - window.openerp.web.formats(openerp); - } - }); - test("format_datetime", function () { - var date = openerp.web.str_to_datetime("2009-05-04 12:34:23"); - var str = openerp.web.format_value(date, {type:"datetime"}); +}); +openerp.testing.section('web-formats', { + dependencies: ['web.formats'] +}, function (test) { + test("format_datetime", function (instance) { + var date = instance.web.str_to_datetime("2009-05-04 12:34:23"); + var str = instance.web.format_value(date, {type:"datetime"}); equal(str, date.toString("MM/dd/yyyy HH:mm:ss")); }); - test("format_date", function () { - var date = openerp.web.str_to_datetime("2009-05-04 12:34:23"); - var str = openerp.web.format_value(date, {type:"date"}); + test("format_date", function (instance) { + var date = instance.web.str_to_datetime("2009-05-04 12:34:23"); + var str = instance.web.format_value(date, {type:"date"}); equal(str, date.toString("MM/dd/yyyy")); }); - test("format_time", function () { - var date = openerp.web.str_to_datetime("2009-05-04 12:34:23"); - var str = openerp.web.format_value(date, {type:"time"}); + test("format_time", function (instance) { + var date = instance.web.str_to_datetime("2009-05-04 12:34:23"); + var str = instance.web.format_value(date, {type:"time"}); equal(str, date.toString("HH:mm:ss")); }); - test("format_float_time", function () { + test("format_float_time", function (instance) { strictEqual( - openerp.web.format_value(1.0, {type:'float', widget:'float_time'}), + instance.web.format_value(1.0, {type:'float', widget:'float_time'}), '01:00'); strictEqual( - openerp.web.format_value(0.9853, {type:'float', widget:'float_time'}), + instance.web.format_value(0.9853, {type:'float', widget:'float_time'}), '00:59'); strictEqual( - openerp.web.format_value(0.0085, {type:'float', widget:'float_time'}), + instance.web.format_value(0.0085, {type:'float', widget:'float_time'}), '00:01'); strictEqual( - openerp.web.format_value(-1.0, {type:'float', widget:'float_time'}), + instance.web.format_value(-1.0, {type:'float', widget:'float_time'}), '-01:00'); strictEqual( - openerp.web.format_value(-0.9853, {type:'float', widget:'float_time'}), + instance.web.format_value(-0.9853, {type:'float', widget:'float_time'}), '-00:59'); strictEqual( - openerp.web.format_value(-0.0085, {type:'float', widget:'float_time'}), + instance.web.format_value(-0.0085, {type:'float', widget:'float_time'}), '-00:01'); }); - test("format_float", function () { + test("format_float", function (instance) { var fl = 12.1234; - var str = openerp.web.format_value(fl, {type:"float"}); + var str = instance.web.format_value(fl, {type:"float"}); equal(str, "12.12"); - equal(openerp.web.format_value(12.02, {type: 'float'}), + equal(instance.web.format_value(12.02, {type: 'float'}), '12.02'); - equal(openerp.web.format_value(0.0002, {type: 'float', digits: [1, 3]}), + equal(instance.web.format_value(0.0002, {type: 'float', digits: [1, 3]}), '0.000'); - equal(openerp.web.format_value(0.0002, {type: 'float', digits: [1, 4]}), + equal(instance.web.format_value(0.0002, {type: 'float', digits: [1, 4]}), '0.0002'); - equal(openerp.web.format_value(0.0002, {type: 'float', digits: [1, 6]}), + equal(instance.web.format_value(0.0002, {type: 'float', digits: [1, 6]}), '0.000200'); - equal(openerp.web.format_value(1, {type: 'float', digits: [1, 6]}), + equal(instance.web.format_value(1, {type: 'float', digits: [1, 6]}), '1.000000'); - equal(openerp.web.format_value(1, {type: 'float'}), + equal(instance.web.format_value(1, {type: 'float'}), '1.00'); - equal(openerp.web.format_value(-11.25, {type: 'float'}), + equal(instance.web.format_value(-11.25, {type: 'float'}), "-11.25"); - openerp.web._t.database.parameters.grouping = [1, 2, -1]; - equal(openerp.web.format_value(1111111.25, {type: 'float'}), + instance.web._t.database.parameters.grouping = [1, 2, -1]; + equal(instance.web.format_value(1111111.25, {type: 'float'}), "1111,11,1.25"); - openerp.web._t.database.parameters.grouping = [1, 0]; - equal(openerp.web.format_value(-11.25, {type: 'float'}), + instance.web._t.database.parameters.grouping = [1, 0]; + equal(instance.web.format_value(-11.25, {type: 'float'}), "-1,1.25"); }); // test("parse_datetime", function () { @@ -123,29 +109,29 @@ $(document).ready(function () { // var res = openerp.web.parse_value(val.toString("HH:mm:ss"), {type:"time"}); // equal(val.toString("HH:mm:ss"), res.toString("HH:mm:ss")); // }); - test('parse_integer', function () { - var val = openerp.web.parse_value('123,456', {type: 'integer'}); + test('parse_integer', function (instance) { + var val = instance.web.parse_value('123,456', {type: 'integer'}); equal(val, 123456); - openerp.web._t.database.parameters.thousands_sep = '|'; - var val2 = openerp.web.parse_value('123|456', {type: 'integer'}); + instance.web._t.database.parameters.thousands_sep = '|'; + var val2 = instance.web.parse_value('123|456', {type: 'integer'}); equal(val2, 123456); }); - test("parse_float", function () { + test("parse_float", function (instance) { var str = "134,112.1234"; - var val = openerp.web.parse_value(str, {type:"float"}); + var val = instance.web.parse_value(str, {type:"float"}); equal(val, 134112.1234); var str = "-134,112.1234"; - var val = openerp.web.parse_value(str, {type:"float"}); + var val = instance.web.parse_value(str, {type:"float"}); equal(val, -134112.1234); - _.extend(openerp.web._t.database.parameters, { + _.extend(instance.web._t.database.parameters, { decimal_point: ',', thousands_sep: '.' }); - var val3 = openerp.web.parse_value('123.456,789', {type: 'float'}); + var val3 = instance.web.parse_value('123.456,789', {type: 'float'}); equal(val3, 123456.789); }); - test('intersperse', function () { - var g = openerp.web.intersperse; + test('intersperse', function (instance) { + var g = instance.web.intersperse; equal(g("", []), ""); equal(g("0", []), "0"); equal(g("012", []), "012"); @@ -176,60 +162,61 @@ $(document).ready(function () { equal(g("12345678", [3,3,3,3], '.'), '12.345.678'); equal(g("12345678", [3,0], '.'), '12.345.678'); }); - test('format_integer', function () { - openerp.web._t.database.parameters.grouping = [3, 3, 3, 3]; - equal(openerp.web.format_value(1000000, {type: 'integer'}), + test('format_integer', function (instance) { + instance.web._t.database.parameters.grouping = [3, 3, 3, 3]; + equal(instance.web.format_value(1000000, {type: 'integer'}), '1,000,000'); - openerp.web._t.database.parameters.grouping = [3, 2, -1]; - equal(openerp.web.format_value(106500, {type: 'integer'}), + instance.web._t.database.parameters.grouping = [3, 2, -1]; + equal(instance.web.format_value(106500, {type: 'integer'}), '1,06,500'); - openerp.web._t.database.parameters.grouping = [1, 2, -1]; - equal(openerp.web.format_value(106500, {type: 'integer'}), + instance.web._t.database.parameters.grouping = [1, 2, -1]; + equal(instance.web.format_value(106500, {type: 'integer'}), '106,50,0'); }); - test('format_float', function () { - openerp.web._t.database.parameters.grouping = [3, 3, 3, 3]; - equal(openerp.web.format_value(1000000, {type: 'float'}), + test('format_float', function (instance) { + instance.web._t.database.parameters.grouping = [3, 3, 3, 3]; + equal(instance.web.format_value(1000000, {type: 'float'}), '1,000,000.00'); - openerp.web._t.database.parameters.grouping = [3, 2, -1]; - equal(openerp.web.format_value(106500, {type: 'float'}), + instance.web._t.database.parameters.grouping = [3, 2, -1]; + equal(instance.web.format_value(106500, {type: 'float'}), '1,06,500.00'); - openerp.web._t.database.parameters.grouping = [1, 2, -1]; - equal(openerp.web.format_value(106500, {type: 'float'}), + instance.web._t.database.parameters.grouping = [1, 2, -1]; + equal(instance.web.format_value(106500, {type: 'float'}), '106,50,0.00'); - _.extend(openerp.web._t.database.parameters, { + _.extend(instance.web._t.database.parameters, { grouping: [3, 0], decimal_point: ',', thousands_sep: '.' }); - equal(openerp.web.format_value(6000, {type: 'float'}), + equal(instance.web.format_value(6000, {type: 'float'}), '6.000,00'); }); - module('custom-date-formats', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.dates(openerp); - window.openerp.web.formats(openerp); - } +}); +openerp.testing.section('web-formats', { + dependencies: ['web.formats'] +}, function (test) { + test('format stripper', function (instance) { + strictEqual(instance.web.strip_raw_chars('%a, %Y %b %d'), + '%a, %Y %b %d'); + strictEqual(instance.web.strip_raw_chars('%a, %Y.eko %bren %da'), + '%a, %Y. %b %d'); }); - test('format stripper', function () { - strictEqual(openerp.web.strip_raw_chars('%a, %Y %b %d'), '%a, %Y %b %d'); - strictEqual(openerp.web.strip_raw_chars('%a, %Y.eko %bren %da'), '%a, %Y. %b %d'); + test('ES date format', function (instance) { + instance.web._t.database.parameters.date_format = '%a, %Y %b %d'; + var date = instance.web.str_to_date("2009-05-04"); + strictEqual(instance.web.format_value(date, {type:"date"}), + 'Mon, 2009 May 04'); + strictEqual(instance.web.parse_value('Mon, 2009 May 04', {type: 'date'}), + '2009-05-04'); }); - test('ES date format', function () { - openerp.web._t.database.parameters.date_format = '%a, %Y %b %d'; - var date = openerp.web.str_to_date("2009-05-04"); - strictEqual(openerp.web.format_value(date, {type:"date"}), 'Mon, 2009 May 04'); - strictEqual(openerp.web.parse_value('Mon, 2009 May 04', {type: 'date'}), '2009-05-04'); - }); - test('extended ES date format', function () { - openerp.web._t.database.parameters.date_format = '%a, %Y.eko %bren %da'; - var date = openerp.web.str_to_date("2009-05-04"); - strictEqual(openerp.web.format_value(date, {type:"date"}), 'Mon, 2009. May 04'); - strictEqual(openerp.web.parse_value('Mon, 2009. May 04', {type: 'date'}), '2009-05-04'); + test('extended ES date format', function (instance) { + instance.web._t.database.parameters.date_format = '%a, %Y.eko %bren %da'; + var date = instance.web.str_to_date("2009-05-04"); + strictEqual(instance.web.format_value(date, {type:"date"}), + 'Mon, 2009. May 04'); + strictEqual(instance.web.parse_value('Mon, 2009. May 04', {type: 'date'}), + '2009-05-04'); }); }); diff --git a/addons/web/static/test/list-utils.js b/addons/web/static/test/list-utils.js index 6200ba788ce..84661138315 100644 --- a/addons/web/static/test/list-utils.js +++ b/addons/web/static/test/list-utils.js @@ -1,45 +1,34 @@ -$(document).ready(function () { - var openerp, - create = function (o) { - if (typeof Object.create === 'function') { - return Object.create(o); - } - function Cls() {} - Cls.prototype = o; - return new Cls; - }; - module('list-events', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.chrome(openerp); - // views loader stuff - window.openerp.web.data(openerp); - window.openerp.web.views(openerp); - window.openerp.web.list(openerp); +openerp.testing.section('list.events', { + dependencies: ['web.list'] +}, function (test) { + var create = function (o) { + if (typeof Object.create === 'function') { + return Object.create(o); } - }); - test('Simple event triggering', function () { - var e = create(openerp.web.list.Events), passed = false; + function Cls() {} + Cls.prototype = o; + return new Cls; + }; + test('Simple event triggering', function (instance) { + var e = create(instance.web.list.Events), passed = false; e.bind('foo', function () { passed = true; }); e.trigger('foo'); ok(passed); }); - test('Bind all', function () { - var e = create(openerp.web.list.Events), event = null; + test('Bind all', function (instance) { + var e = create(instance.web.list.Events), event = null; e.bind(null, function (ev) { event = ev; }); e.trigger('foo'); strictEqual(event, 'foo'); }); - test('Propagate trigger params', function () { - var e = create(openerp.web.list.Events), p = false; + test('Propagate trigger params', function (instance) { + var e = create(instance.web.list.Events), p = false; e.bind(null, function (_, param) { p = param }); e.trigger('foo', true); strictEqual(p, true) }); - test('Bind multiple callbacks', function () { - var e = create(openerp.web.list.Events), count; + test('Bind multiple callbacks', function (instance) { + var e = create(instance.web.list.Events), count; e.bind('foo', function () { count++; }) .bind('bar', function () { count++; }) .bind(null, function () { count++; }) @@ -59,20 +48,20 @@ $(document).ready(function () { e.trigger('baz'); strictEqual(count, 3); }); - test('Mixin events', function () { - var cls = openerp.web.Class.extend({ + test('Mixin events', function (instance) { + var cls = instance.web.Class.extend({ method: function () { this.trigger('e'); } }); - cls.include(openerp.web.list.Events); - var instance = new cls, triggered = false; + cls.include(instance.web.list.Events); + var i = new cls, triggered = false; - instance.bind('e', function () { triggered = true; }); - instance.method(); + i.bind('e', function () { triggered = true; }); + i.method(); ok(triggered); }); - test('Unbind all handlers', function () { - var e = create(openerp.web.list.Events), passed = 0; + test('Unbind all handlers', function (instance) { + var e = create(instance.web.list.Events), passed = 0; e.bind('foo', function () { passed++; }); e.trigger('foo'); strictEqual(passed, 1); @@ -80,8 +69,8 @@ $(document).ready(function () { e.trigger('foo'); strictEqual(passed, 1); }); - test('Unbind one handler', function () { - var e = create(openerp.web.list.Events), p1 = 0, p2 = 0, + test('Unbind one handler', function (instance) { + var e = create(instance.web.list.Events), p1 = 0, p2 = 0, h1 = function () { p1++; }, h2 = function () { p2++; }; e.bind('foo', h1); e.bind('foo', h2); @@ -93,29 +82,20 @@ $(document).ready(function () { strictEqual(p1, 1); strictEqual(p2, 2); }); - - module('list-records', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.chrome(openerp); - // views loader stuff - window.openerp.web.data(openerp); - window.openerp.web.views(openerp); - window.openerp.web.list(openerp); - } - }); - test('Basic record initialization', function () { - var r = new openerp.web.list.Record({qux: 3}); +}); +openerp.testing.section('list.records', { + dependencies: ['web.list'] +}, function (test) { + test('Basic record initialization', function (instance) { + var r = new instance.web.list.Record({qux: 3}); r.set('foo', 1); r.set('bar', 2); strictEqual(r.get('foo'), 1); strictEqual(r.get('bar'), 2); strictEqual(r.get('qux'), 3); }); - test('Change all the things', function () { - var r = new openerp.web.list.Record(), changed = false, field; + test('Change all the things', function (instance) { + var r = new instance.web.list.Record(), changed = false, field; r.bind('change', function () { changed = true; }); r.bind(null, function (e) { field = field || e.split(':')[1]}); r.set('foo', 1); @@ -123,8 +103,8 @@ $(document).ready(function () { ok(changed); strictEqual(field, 'foo'); }); - test('Change single field', function () { - var r = new openerp.web.list.Record(), changed = 0; + test('Change single field', function (instance) { + var r = new instance.web.list.Record(), changed = 0; r.bind('change:foo', function () { changed++; }); r.set('foo', 1); r.set('bar', 1); @@ -132,21 +112,12 @@ $(document).ready(function () { strictEqual(r.get('bar'), 1); strictEqual(changed, 1); }); - - module('list-collections', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.chrome(openerp); - // views loader stuff - window.openerp.web.data(openerp); - window.openerp.web.views(openerp); - window.openerp.web.list(openerp); - } - }); - test('degenerate-fetch', function () { - var c = new openerp.web.list.Collection(); +}); +openerp.testing.section('list.collections', { + dependencies: ['web.list'] +}, function (test) { + test('degenerate-fetch', function (instance) { + var c = new instance.web.list.Collection(); strictEqual(c.length, 0); c.add({id: 1, value: 2}); c.add({id: 2, value: 3}); @@ -155,16 +126,16 @@ $(document).ready(function () { strictEqual(c.length, 4); var r = c.at(2), r2 = c.get(1); - ok(r instanceof openerp.web.list.Record); + ok(r instanceof instance.web.list.Record); strictEqual(r.get('id'), 3); strictEqual(r.get('value'), 5); - ok(r2 instanceof openerp.web.list.Record); + ok(r2 instanceof instance.web.list.Record); strictEqual(r2.get('id'), 1); strictEqual(r2.get('value'), 2); }); - test('degenerate-indexed-add', function () { - var c = new openerp.web.list.Collection([ + test('degenerate-indexed-add', function (instance) { + var c = new instance.web.list.Collection([ {id: 1, value: 5}, {id: 2, value: 10}, {id: 3, value: 20} @@ -175,8 +146,8 @@ $(document).ready(function () { strictEqual(c.at(1).get('value'), 55); strictEqual(c.at(3).get('value'), 20); }); - test('degenerate-remove', function () { - var c = new openerp.web.list.Collection([ + test('degenerate-remove', function (instance) { + var c = new instance.web.list.Collection([ {id: 1, value: 5}, {id: 2, value: 10}, {id: 3, value: 20} @@ -188,9 +159,9 @@ $(document).ready(function () { equal(c.get(2), undefined); strictEqual(c.at(1).get('value'), 20); }); - test('degenerate-remove-bound', function () { + test('degenerate-remove-bound', function (instance) { var changed = false, - c = new openerp.web.list.Collection([ {id: 1, value: 5} ]); + c = new instance.web.list.Collection([ {id: 1, value: 5} ]); c.bind('change', function () { changed = true; }); var record = c.get(1); c.remove(record); @@ -198,8 +169,8 @@ $(document).ready(function () { ok(!changed, 'removed records should not trigger events in their ' + 'parent collection'); }); - test('degenerate-reset', function () { - var event, obj, c = new openerp.web.list.Collection([ + test('degenerate-reset', function (instance) { + var event, obj, c = new instance.web.list.Collection([ {id: 1, value: 5}, {id: 2, value: 10}, {id: 3, value: 20} @@ -218,9 +189,9 @@ $(document).ready(function () { strictEqual(c.length, 1); strictEqual(c.get(42).get('value'), 55); }); - test('degenerate-reset-bound', function () { + test('degenerate-reset-bound', function (instance) { var changed = false, - c = new openerp.web.list.Collection([ {id: 1, value: 5} ]); + c = new instance.web.list.Collection([ {id: 1, value: 5} ]); c.bind('change', function () { changed = true; }); var record = c.get(1); c.reset(); @@ -229,9 +200,9 @@ $(document).ready(function () { 'parent collection'); }); - test('degenerate-propagations', function () { + test('degenerate-propagations', function (instance) { var values = []; - var c = new openerp.web.list.Collection([ + var c = new instance.web.list.Collection([ {id: 1, value: 5}, {id: 2, value: 10}, {id: 3, value: 20} @@ -244,8 +215,8 @@ $(document).ready(function () { c.get(3).set('value', 21); deepEqual(values, [6, 11, 21]); }); - test('BTree', function () { - var root = new openerp.web.list.Collection(), + test('BTree', function (instance) { + var root = new instance.web.list.Collection(), c = root.proxy('admin'), total = 0; c.add({id: 1, name: "Administrator", login: 'admin'}); @@ -260,8 +231,8 @@ $(document).ready(function () { c.at(1).set('wealth', 5); strictEqual(total, 47); }); - test('degenerate-successor', function () { - var root = new openerp.web.list.Collection([ + test('degenerate-successor', function (instance) { + var root = new instance.web.list.Collection([ {id: 1, value: 1}, {id: 2, value: 2}, {id: 3, value: 3}, @@ -282,8 +253,8 @@ $(document).ready(function () { root.at(3).attributes, "wraparound should have no effect if not succ(last_record)"); }); - test('successor', function () { - var root = new openerp.web.list.Collection(); + test('successor', function (instance) { + var root = new instance.web.list.Collection(); root.proxy('first').add([{id: 1, value: 1}, {id: 2, value: 2}]); root.proxy('second').add([{id: 3, value: 3}, {id: 4, value: 5}]); root.proxy('third').add([{id: 5, value: 8}, {id: 6, value: 13}]); @@ -298,8 +269,8 @@ $(document).ready(function () { root.get(3).attributes, "should wraparound within a collection"); }); - test('degenerate-predecessor', function () { - var root = new openerp.web.list.Collection([ + test('degenerate-predecessor', function (instance) { + var root = new instance.web.list.Collection([ {id: 1, value: 1}, {id: 2, value: 2}, {id: 3, value: 3}, @@ -320,8 +291,8 @@ $(document).ready(function () { root.at(0).attributes, "wraparound should have no effect if not pred(first_record)"); }); - test('predecessor', function () { - var root = new openerp.web.list.Collection(); + test('predecessor', function (instance) { + var root = new instance.web.list.Collection(); root.proxy('first').add([{id: 1, value: 1}, {id: 2, value: 2}]); root.proxy('second').add([{id: 3, value: 3}, {id: 4, value: 5}]); root.proxy('third').add([{id: 5, value: 8}, {id: 6, value: 13}]); @@ -336,21 +307,12 @@ $(document).ready(function () { root.get(4).attributes, "should wraparound within a collection"); }); - - module('list-hofs', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.chrome(openerp); - // views loader stuff - window.openerp.web.data(openerp); - window.openerp.web.views(openerp); - window.openerp.web.list(openerp); - } - }); - test('each, degenerate', function () { - var c = new openerp.web.list.Collection([ +}); +openerp.testing.section('list.collections.hom', { + dependencies: ['web.list'] +}, function (test) { + test('each, degenerate', function (instance) { + var c = new instance.web.list.Collection([ {id: 1, value: 5}, {id: 2, value: 10}, {id: 3, value: 20} @@ -362,8 +324,8 @@ $(document).ready(function () { ids, [1, 2, 3], 'degenerate collections should be iterated in record order'); }); - test('each, deep', function () { - var root = new openerp.web.list.Collection(), + test('each, deep', function (instance) { + var root = new instance.web.list.Collection(), ids = []; root.proxy('foo').add([ {id: 1, value: 5}, @@ -382,8 +344,8 @@ $(document).ready(function () { ids, [1, 2, 3, 10, 20, 30], 'tree collections should be deeply iterated'); }); - test('map, degenerate', function () { - var c = new openerp.web.list.Collection([ + test('map, degenerate', function (instance) { + var c = new instance.web.list.Collection([ {id: 1, value: 5}, {id: 2, value: 10}, {id: 3, value: 20} @@ -395,8 +357,8 @@ $(document).ready(function () { ids, [1, 2, 3], 'degenerate collections should be iterated in record order'); }); - test('map, deep', function () { - var root = new openerp.web.list.Collection(); + test('map, deep', function (instance) { + var root = new instance.web.list.Collection(); root.proxy('foo').add([ {id: 1, value: 5}, {id: 2, value: 10}, @@ -414,29 +376,20 @@ $(document).ready(function () { ids, [1, 2, 3, 10, 20, 30], 'tree collections should be deeply iterated'); }); - - module("list-weirds", { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.chrome(openerp); - // views loader stuff - window.openerp.web.data(openerp); - window.openerp.web.views(openerp); - window.openerp.web.list(openerp); - } - }); - test('set-from-noid', function () { - var root = new openerp.web.list.Collection(); +}); +openerp.testing.section('list.collection.weirdoes', { + dependencies: ['web.list'] +}, function (test) { + test('set-from-noid', function (instance) { + var root = new instance.web.list.Collection(); root.add({v: 3}); root.at(0).set('id', 42); var record = root.get(42); equal(root.length, 1); equal(record.get('v'), 3, "should have fetched the original record"); }); - test('set-from-previd', function () { - var root = new openerp.web.list.Collection(); + test('set-from-previd', function (instance) { + var root = new instance.web.list.Collection(); root.add({id: 1, v: 2}); root.get(1).set('id', 42); var record = root.get(42); diff --git a/addons/web/static/test/registry.js b/addons/web/static/test/registry.js index ca689a351e0..2ad69a541bc 100644 --- a/addons/web/static/test/registry.js +++ b/addons/web/static/test/registry.js @@ -1,58 +1,55 @@ -$(document).ready(function () { - var openerp; - module('Registry', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - openerp.web.Foo = {}; - openerp.web.Bar = {}; - openerp.web.Foo2 = {}; - } - }); - test('key set', function () { - var reg = new openerp.web.Registry(); +openerp.testing.section('registry', { + dependencies: ['web.corelib'], + setup: function (instance) { + instance.web.Foo = {}; + instance.web.Bar = {}; + instance.web.Foo2 = {}; + } +}, function (test) { + test('key set', function (instance) { + var reg = new instance.web.Registry(); - reg.add('foo', 'openerp.web.Foo') - .add('bar', 'openerp.web.Bar'); - strictEqual(reg.get_object('bar'), openerp.web.Bar); + reg.add('foo', 'instance.web.Foo') + .add('bar', 'instance.web.Bar'); + strictEqual(reg.get_object('bar'), instance.web.Bar); }); - test('extension', function () { - var reg = new openerp.web.Registry({ - foo: 'openerp.web.Foo', - bar: 'openerp.web.Bar' + test('extension', function (instance) { + var reg = new instance.web.Registry({ + foo: 'instance.web.Foo', + bar: 'instance.web.Bar' }); - var reg2 = reg.extend({ 'foo': 'openerp.web.Foo2' }); - strictEqual(reg.get_object('foo'), openerp.web.Foo); - strictEqual(reg2.get_object('foo'), openerp.web.Foo2); + var reg2 = reg.extend({ 'foo': 'instance.web.Foo2' }); + strictEqual(reg.get_object('foo'), instance.web.Foo); + strictEqual(reg2.get_object('foo'), instance.web.Foo2); }); - test('remain-linked', function () { - var reg = new openerp.web.Registry({ - foo: 'openerp.web.Foo', - bar: 'openerp.web.Bar' + test('remain-linked', function (instance) { + var reg = new instance.web.Registry({ + foo: 'instance.web.Foo', + bar: 'instance.web.Bar' }); var reg2 = reg.extend(); - reg.add('foo2', 'openerp.web.Foo2'); - strictEqual(reg.get_object('foo2'), openerp.web.Foo2); - strictEqual(reg2.get_object('foo2'), openerp.web.Foo2); + reg.add('foo2', 'instance.web.Foo2'); + strictEqual(reg.get_object('foo2'), instance.web.Foo2); + strictEqual(reg2.get_object('foo2'), instance.web.Foo2); }); - test('multiget', function () { - var reg = new openerp.web.Registry({ - foo: 'openerp.web.Foo', - bar: 'openerp.web.Bar' + test('multiget', function (instance) { + var reg = new instance.web.Registry({ + foo: 'instance.web.Foo', + bar: 'instance.web.Bar' }); strictEqual(reg.get_any(['qux', 'grault', 'bar', 'foo']), - openerp.web.Bar); + instance.web.Bar); }); - test('extended-multiget', function () { - var reg = new openerp.web.Registry({ - foo: 'openerp.web.Foo', - bar: 'openerp.web.Bar' + test('extended-multiget', function (instance) { + var reg = new instance.web.Registry({ + foo: 'instance.web.Foo', + bar: 'instance.web.Bar' }); var reg2 = reg.extend(); strictEqual(reg2.get_any(['qux', 'grault', 'bar', 'foo']), - openerp.web.Bar); + instance.web.Bar); }); }); diff --git a/addons/web/static/test/rpc.js b/addons/web/static/test/rpc.js index fbe7147f2e6..84b4afa8baf 100644 --- a/addons/web/static/test/rpc.js +++ b/addons/web/static/test/rpc.js @@ -1,16 +1,8 @@ -$(document).ready(function () { - var openerp; - - module('Misordered resolution management', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.data(openerp); - } - }); - test('Resolve all correctly ordered, sync', function () { - var dm = new openerp.web.DropMisordered(), flag = false; +openerp.testing.section('misordered resolution managemeng', { + dependencies: ['web.data'] +}, function (test) { + test('Resolve all correctly ordered, sync', function (instance) { + var dm = new instance.web.DropMisordered(), flag = false; var d1 = $.Deferred(), d2 = $.Deferred(), r1 = dm.add(d1), r2 = dm.add(d2); @@ -23,8 +15,8 @@ $(document).ready(function () { ok(flag); }); - test("Don't resolve mis-ordered, sync", function () { - var dm = new openerp.web.DropMisordered(), + test("Don't resolve mis-ordered, sync", function (instance) { + var dm = new instance.web.DropMisordered(), done1 = false, done2 = false, fail1 = false, fail2 = false; @@ -44,8 +36,8 @@ $(document).ready(function () { ok(done2); ok(!fail2); }); - test('Fail mis-ordered flag, sync', function () { - var dm = new openerp.web.DropMisordered(true), + test('Fail mis-ordered flag, sync', function (instance) { + var dm = new instance.web.DropMisordered(true), done1 = false, done2 = false, fail1 = false, fail2 = false; @@ -66,8 +58,8 @@ $(document).ready(function () { ok(!fail2); }); - asyncTest('Resolve all correctly ordered, async', 1, function () { - var dm = new openerp.web.DropMisordered(); + test('Resolve all correctly ordered, async', {asserts: 1}, function (instance) { + var dm = new instance.web.DropMisordered(); var d1 = $.Deferred(), d2 = $.Deferred(), r1 = dm.add(d1), r2 = dm.add(d2); @@ -75,13 +67,12 @@ $(document).ready(function () { setTimeout(function () { d1.resolve(); }, 100); setTimeout(function () { d2.resolve(); }, 200); - $.when(r1, r2).done(function () { - start(); + return $.when(r1, r2).done(function () { ok(true); }); }); - asyncTest("Don't resolve mis-ordered, async", 4, function () { - var dm = new openerp.web.DropMisordered(), + test("Don't resolve mis-ordered, async", {asserts: 4}, function (instance) { + var dm = new instance.web.DropMisordered(), done1 = false, done2 = false, fail1 = false, fail2 = false; @@ -94,18 +85,20 @@ $(document).ready(function () { setTimeout(function () { d1.resolve(); }, 200); setTimeout(function () { d2.resolve(); }, 100); + var done = $.Deferred(); setTimeout(function () { - start(); // d1 is in limbo ok(!done1); ok(!fail1); // d2 is resolved ok(done2); ok(!fail2); + done.resolve(); }, 400); + return $.when(d1, d2, done); }); - asyncTest('Fail mis-ordered flag, async', 4, function () { - var dm = new openerp.web.DropMisordered(true), + test('Fail mis-ordered flag, async', {asserts: 4}, function (instance) { + var dm = new instance.web.DropMisordered(true), done1 = false, done2 = false, fail1 = false, fail2 = false; @@ -118,6 +111,7 @@ $(document).ready(function () { setTimeout(function () { d1.resolve(); }, 200); setTimeout(function () { d2.resolve(); }, 100); + var done = $.Deferred(); setTimeout(function () { start(); // d1 is failed @@ -126,6 +120,8 @@ $(document).ready(function () { // d2 is resolved ok(done2); ok(!fail2); + done.resolve(); }, 400); + return $.when(d1, d2, done) }); }); From 6abc1134f5193d0a80066834a17f344ce24b09d8 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 26 Oct 2012 12:04:52 +0200 Subject: [PATCH 019/191] [IMP] port list-edition tests to new framework bzr revid: xmo@openerp.com-20121026100452-8s4j9g0mxzfuy834 --- addons/web/static/test/list-editable.js | 260 ++++++++++-------------- 1 file changed, 113 insertions(+), 147 deletions(-) diff --git a/addons/web/static/test/list-editable.js b/addons/web/static/test/list-editable.js index 0388e56048d..0fc930793f2 100644 --- a/addons/web/static/test/list-editable.js +++ b/addons/web/static/test/list-editable.js @@ -1,16 +1,13 @@ -$(document).ready(function () { - var $fix = $('#qunit-fixture'); - - var instance; - var baseSetup = function () { - instance = openerp.testing.instanceFor('list_editable'); - - openerp.testing.loadTemplate(instance); - - openerp.testing.mockifyRPC(instance); - }; - - +openerp.testing.section('editor', { + dependencies: ['web.list_editable'], + rpc: 'mock', + templates: true, + setup: function (instance, $s, mock) { + mock('test.model:create', function () { + return 42; + }); + } +}, function (test) { /** * * @param {String} name @@ -30,7 +27,7 @@ $(document).ready(function () { } /** - * @param {Array} fields + * @param {Array} [fields] * @return {Object} */ function makeFormView(fields) { @@ -67,46 +64,37 @@ $(document).ready(function () { }; } - module('editor', { - setup: baseSetup - }); - asyncTest('base-state', 2, function () { + test('base-state', {asserts: 2}, function (instance, $fix) { var e = new instance.web.list.Editor({ dataset: {ids: []}, edition_view: function () { return makeFormView(); } }); - e.appendTo($fix) - .always(start) - .fail(function (error) { ok(false, error && error.message); }) + return e.appendTo($fix) .done(function () { ok(!e.is_editing(), "should not be editing"); ok(e.form instanceof instance.web.FormView, "should use default form type"); }); }); - asyncTest('toggle-edition-save', 4, function () { - instance.session.responses['/web/dataset/call_kw:create'] = function () { - return { result: 42 }; - }; - instance.session.responses['/web/dataset/call_kw:read'] = function () { - return { result: [{ - id: 42, - a: false, - b: false, - c: false - }]}; - }; + test('toggle-edition-save', { + asserts: 4, + setup: function (instance, $s, mock) { + mock('test.model:read', function () { + return [{id: 42, a: false, b: false, c: false}]; + }); + } + }, function (instance, $fix) { var e = new instance.web.list.Editor({ - dataset: new instance.web.DataSetSearch(), + dataset: new instance.web.DataSetSearch(null, 'test.model'), prepends_on_create: function () { return false; }, edition_view: function () { return makeFormView([ field('a'), field('b'), field('c') ]); } }); var counter = 0; - e.appendTo($fix) + return e.appendTo($fix) .pipe(function () { return e.edit({}, function () { ++counter; @@ -117,26 +105,21 @@ $(document).ready(function () { equal(counter, 3, "should have configured all fields"); return e.save(); }) - .always(start) - .fail(function (error) { ok(false, error && error.message); }) .done(function (record) { ok(!e.is_editing(), "should have stopped editing"); equal(record.id, 42, "should have newly created id"); }) }); - asyncTest('toggle-edition-cancel', 2, function () { - instance.session.responses['/web/dataset/call_kw:create'] = function () { - return { result: 42 }; - }; + test('toggle-edition-cancel', { asserts: 2 }, function (instance, $fix) { var e = new instance.web.list.Editor({ - dataset: new instance.web.DataSetSearch(), + dataset: new instance.web.DataSetSearch(null, 'test.model'), prepends_on_create: function () { return false; }, edition_view: function () { return makeFormView([ field('a'), field('b'), field('c') ]); } }); var counter = 0; - e.appendTo($fix) + return e.appendTo($fix) .pipe(function () { return e.edit({}, function () { ++counter; @@ -145,22 +128,20 @@ $(document).ready(function () { .pipe(function (form) { return e.cancel(); }) - .always(start) - .fail(function (error) { ok(false, error && error.message); }) .done(function (record) { ok(!e.is_editing(), "should have stopped editing"); ok(!record.id, "should have no id"); }) }); - asyncTest('toggle-save-required', 2, function () { - instance.session.responses['/web/dataset/call_kw:create'] = function () { - return { result: 42 }; - }; + test('toggle-save-required', { + asserts: 2, + fail_on_rejection: false + }, function (instance, $fix) { var e = new instance.web.list.Editor({ do_warn: function () { warnings++; }, - dataset: new instance.web.DataSetSearch(), + dataset: new instance.web.DataSetSearch(null, 'test.model'), prepends_on_create: function () { return false; }, edition_view: function () { return makeFormView([ @@ -169,7 +150,7 @@ $(document).ready(function () { }); var counter = 0; var warnings = 0; - e.appendTo($fix) + return e.appendTo($fix) .pipe(function () { return e.edit({}, function () { ++counter; @@ -178,78 +159,73 @@ $(document).ready(function () { .pipe(function (form) { return e.save(); }) - .always(start) .done(function () { ok(false, "cancel should not succeed"); }) .fail(function () { equal(warnings, 1, "should have been warned"); ok(e.is_editing(), "should have kept editing"); - }) + }); }); - - module('list-edition', { - setup: function () { - baseSetup(); - - var records = {}; - _.extend(instance.session.responses, { - '/web/view/load': function () { - return {result: { - type: 'tree', - fields: { - a: {type: 'char', string: "A"}, - b: {type: 'char', string: "B"}, - c: {type: 'char', string: "C"} - }, - arch: { - tag: 'tree', - attrs: {}, - children: [ - {tag: 'field', attrs: {name: 'a'}}, - {tag: 'field', attrs: {name: 'b'}}, - {tag: 'field', attrs: {name: 'c'}} - ] - } - }}; +}); +openerp.testing.section('list.edition', { + dependencies: ['web.list_editable'], + rpc: 'mock', + templates: true, + setup: function (instance, $s, mock) { + var records = {}; + mock('demo:create', function (args) { + records[42] = _.extend({}, args[0]); + return 42; + }); + mock('demo:read', function (args) { + var id = args[0][0]; + if (id in records) { + return [records[id]]; + } + return []; + }); + mock('/web/view/load', function () { + return { + type: 'tree', + fields: { + a: {type: 'char', string: "A"}, + b: {type: 'char', string: "B"}, + c: {type: 'char', string: "C"} }, - '/web/dataset/call_kw:create': function (params) { - records[42] = _.extend({}, params.params.args[0]); - return {result: 42}; - }, - '/web/dataset/call_kw:read': function (params) { - var id = params.params.args[0][0]; - if (id in records) { - return {result: [records[id]]}; - } - return {result: []}; + arch: { + tag: 'tree', + attrs: {}, + children: [ + {tag: 'field', attrs: {name: 'a'}}, + {tag: 'field', attrs: {name: 'b'}}, + {tag: 'field', attrs: {name: 'c'}} + ] } - }) - } - }); - asyncTest('newrecord', 6, function () { + }; + }); + } +}, function (test) { + test('newrecord', {asserts: 6}, function (instance, $fix, mock) { var got_defaults = false; - instance.session.responses['/web/dataset/call_kw:default_get'] = function (params) { - var fields = params.params.args[0]; + mock('demo:default_get', function (args) { + var fields = args[0]; deepEqual( fields, ['a', 'b', 'c'], "should ask defaults for all fields"); got_defaults = true; - return {result: { - a: "qux", - b: "quux" - }}; - }; + return { a: "qux", b: "quux" }; + }); var ds = new instance.web.DataSetStatic(null, 'demo', null, [1]); var l = new instance.web.ListView({}, ds, false, {editable: 'top'}); - l.appendTo($fix) + return l.appendTo($fix) .pipe(l.proxy('reload_content')) .pipe(function () { return l.start_edition(); }) - .always(start) .pipe(function () { ok(got_defaults, "should have fetched default values for form"); + return l.save_edition(); }) .pipe(function (result) { @@ -260,45 +236,39 @@ $(document).ready(function () { "should have used default values"); ok(!result.record.get('c'), "should have no value if there was no default"); - }) - .fail(function (e) { ok(false, e && e.message || e); }); - }); - - module('list-edition-events', { - setup: function () { - baseSetup(); - _.extend(instance.session.responses, { - '/web/view/load': function () { - return {result: { - type: 'tree', - fields: { - a: {type: 'char', string: "A"}, - b: {type: 'char', string: "B"}, - c: {type: 'char', string: "C"} - }, - arch: { - tag: 'tree', - attrs: {}, - children: [ - {tag: 'field', attrs: {name: 'a'}}, - {tag: 'field', attrs: {name: 'b'}}, - {tag: 'field', attrs: {name: 'c'}} - ] - } - }}; - }, - '/web/dataset/call_kw:read': function (params) { - return {result: [{ - id: 1, - a: 'foo', - b: 'bar', - c: 'baz' - }]}; - } }); - } }); - asyncTest('edition events', 4, function () { +}); +openerp.testing.section('list.edition.events', { + dependencies: ['web.list_editable'], + rpc: 'mock', + templates: true, + setup: function (instance, $s, mock) { + mock('demo:read', function () { + return [{ id: 1, a: 'foo', b: 'bar', c: 'baz' }]; + }); + mock('/web/view/load', function () { + return { + type: 'tree', + fields: { + a: {type: 'char', string: "A"}, + b: {type: 'char', string: "B"}, + c: {type: 'char', string: "C"} + }, + arch: { + tag: 'tree', + attrs: {}, + children: [ + {tag: 'field', attrs: {name: 'a'}}, + {tag: 'field', attrs: {name: 'b'}}, + {tag: 'field', attrs: {name: 'c'}} + ] + } + }; + }); + } +}, function (test) { + test('edition events', {asserts: 4}, function (instance, $fix) { var ds = new instance.web.DataSetStatic(null, 'demo', null, [1]); var o = { counter: 0, @@ -306,9 +276,8 @@ $(document).ready(function () { }; var l = new instance.web.ListView({}, ds, false, {editable: 'top'}); l.on('edit:before edit:after', o, o.onEvent); - l.appendTo($fix) + return l.appendTo($fix) .pipe(l.proxy('reload_content')) - .always(start) .pipe(function () { ok(l.options.editable, "should be editable"); equal(o.counter, 0, "should have seen no event yet"); @@ -317,11 +286,10 @@ $(document).ready(function () { .pipe(function () { ok(l.editor.is_editing(), "should be editing"); equal(o.counter, 2, "should have seen two edition events"); - }) - .fail(function (e) { ok(false, e && e.message); }); + }); }); - asyncTest('edition events: cancelling', 3, function () { + test('edition events: cancelling', {asserts: 3}, function (instance, $fix) { var edit_after = false; var ds = new instance.web.DataSetStatic(null, 'demo', null, [1]); var l = new instance.web.ListView({}, ds, false, {editable: 'top'}); @@ -331,9 +299,8 @@ $(document).ready(function () { l.on('edit:after', {}, function () { edit_after = true; }); - l.appendTo($fix) + return l.appendTo($fix) .pipe(l.proxy('reload_content')) - .always(start) .pipe(function () { ok(l.options.editable, "should be editable"); return l.start_edition(); @@ -343,7 +310,6 @@ $(document).ready(function () { ok(!l.editor.is_editing(), "should not be editing"); ok(!edit_after, "should not have fired the edit:after event"); return $.when(); - }) - .fail(function (e) { ok(false, e && e.message || e); }); + }); }); }); From 75629866d24ca1189265841992e43e7e99efe60f Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Fri, 26 Oct 2012 15:03:04 +0200 Subject: [PATCH 020/191] [IMP] payment.acquirer: code simplification/cleanup bzr revid: odo@openerp.com-20121026130304-p1tp8pkruj60dbv2 --- addons/portal/acquirer.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/addons/portal/acquirer.py b/addons/portal/acquirer.py index 7bf44bb2527..3217b49d846 100644 --- a/addons/portal/acquirer.py +++ b/addons/portal/acquirer.py @@ -40,7 +40,7 @@ class acquirer(osv.Model): _columns = { 'name': fields.char('Name', required=True), - 'form_template': fields.text('Payment form template (HTML)', translate=True), + 'form_template': fields.text('Payment form template (HTML)', translate=True, required=True), 'visible': fields.boolean('Visible', help="Whether this payment acquirer is currently displayed in portal forms"), } @@ -57,8 +57,7 @@ class acquirer(osv.Model): 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, + result = MakoTemplate(this.form_template).render_unicode(object=object, reference=reference, currency=currency, amount=amount, @@ -67,12 +66,9 @@ class acquirer(osv.Model): # context kw would clash with mako internals ctx=context, format_exceptions=True) - result = result.strip() - if result == u'False': - result = u'' - return result + return result.strip() except Exception: - _logger.exception("failed to render mako template value for payment.acquirer %s: %r", this.name, template) + _logger.exception("failed to render mako template value for payment.acquirer %s: %r", this.name, this.form_template) return def _wrap_payment_block(self, cr, uid, html_block, amount, currency, context=None): From 0f3ed1e09addc905b85c6188ef0b652782fcace3 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Fri, 26 Oct 2012 17:28:27 +0200 Subject: [PATCH 021/191] [IMP] portal_sale: better menu labels, add group to restrict visibility of payment options, improve module description bzr revid: odo@openerp.com-20121026152827-iq5ku8q3atqisw6m --- addons/portal_sale/__openerp__.py | 20 +++++++++++++--- addons/portal_sale/portal_sale_view.xml | 24 +++++++++---------- .../portal_sale/security/portal_security.xml | 12 ++++++++++ 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/addons/portal_sale/__openerp__.py b/addons/portal_sale/__openerp__.py index 8d7cd8b3935..ba5d25c56ed 100644 --- a/addons/portal_sale/__openerp__.py +++ b/addons/portal_sale/__openerp__.py @@ -26,8 +26,22 @@ 'category': 'Tools', 'complexity': 'easy', 'description': """ -This module adds sale menu and features to your portal if sale and portal are installed. -======================================================================================== +This module adds a Sales menu to your portal as soon as sale and portal are installed. +====================================================================================== + +After installing this module, portal users will be able to access their own documents +via the following menus: + + - Quotations + - Sale Orders + - Delivery Orders + - Products (public ones) + - Invoices + - Payments/Refunds + +If online payment acquirers are configured, portal users will also be given the opportunity to +pay online on their Sale Orders and Invoices that are not paid yet. Paypal is included +by default, you simply need to configure a Paypal account in the Accounting settings. """, 'author': 'OpenERP SA', 'depends': ['sale_stock','portal'], @@ -36,8 +50,8 @@ This module adds sale menu and features to your portal if sale and portal are in 'portal_sale_view.xml', 'security/ir.model.access.csv', ], - 'installable': True, 'auto_install': True, 'category': 'Hidden', } + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/portal_sale/portal_sale_view.xml b/addons/portal_sale/portal_sale_view.xml index f4607590717..e1c5a233eb9 100644 --- a/addons/portal_sale/portal_sale_view.xml +++ b/addons/portal_sale/portal_sale_view.xml @@ -8,7 +8,7 @@ - + @@ -29,7 +29,7 @@ - Sales Orders + Sale Orders ir.actions.act_window sale.order tree,form,calendar,graph @@ -57,11 +57,11 @@ kanban,tree,form - No public products. + There are no public products. - Customer Invoices + Invoices account.invoice tree,form,calendar,graph [('type','=','out_invoice')] @@ -71,7 +71,7 @@ - Customer Payment + Refunds/Payments account.voucher [('journal_id.type', 'in', ['bank', 'cash']), ('type','=','receipt')] {'type':'receipt'} @@ -80,19 +80,17 @@ You don't have any payment. - - - - - - - -
    diff --git a/addons/portal_sale/security/portal_security.xml b/addons/portal_sale/security/portal_security.xml index c4bf6131ba1..07fb49b64d6 100644 --- a/addons/portal_sale/security/portal_security.xml +++ b/addons/portal_sale/security/portal_security.xml @@ -2,6 +2,18 @@ + + View Online Payment Options + + Members of this group see the online payment options +on Sale Orders and Customer Invoices. These options are meant for customers who are accessing +their documents through the portal. + + + + + + Portal Personal Quotations/Sales Orders From 52ee9769f69144ab8050ed70572ddcaffe0dd6c5 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Fri, 26 Oct 2012 18:48:03 +0200 Subject: [PATCH 022/191] [IMP] sale_portal: add payment option on invoices, fix state check to display it on sale.order bzr revid: odo@openerp.com-20121026164803-625usw34bnyzmxqu --- addons/portal_sale/portal_sale.py | 21 +++++++++++++++- addons/portal_sale/portal_sale_view.xml | 33 +++++++++++++++++++++---- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/addons/portal_sale/portal_sale.py b/addons/portal_sale/portal_sale.py index d45653bd817..09d71fc1d2b 100644 --- a/addons/portal_sale/portal_sale.py +++ b/addons/portal_sale/portal_sale.py @@ -34,7 +34,26 @@ class sale_order(osv.Model): result = dict.fromkeys(ids, False) payment_acquirer = self.pool.get('portal.payment.acquirer') for this in self.browse(cr, uid, ids, context=context): - if this.state != 'draft' and not this.invoiced: + if this.state not in ('draft','cancel') and not this.invoiced: result[this.id] = payment_acquirer.render_payment_block(cr, uid, this, this.name, this.pricelist_id.currency_id, this.amount_total, context=context) + return result + + +class account_invoice(osv.Model): + _inherit = 'account.invoice' + + _payment_block_proxy = lambda self,*a,**kw: self._portal_payment_block(*a, **kw) + + _columns = { + 'portal_payment_options': fields.function(_payment_block_proxy, type="html", string="Portal Payment Options"), + } + + def _portal_payment_block(self, cr, uid, ids, fieldname, arg, context=None): + result = dict.fromkeys(ids, False) + payment_acquirer = self.pool.get('portal.payment.acquirer') + for this in self.browse(cr, uid, ids, context=context): + if this.state not in ('draft','done') and not this.reconciled: + result[this.id] = payment_acquirer.render_payment_block(cr, uid, this, this.number, + this.currency_id, this.residual, context=context) return result \ No newline at end of file diff --git a/addons/portal_sale/portal_sale_view.xml b/addons/portal_sale/portal_sale_view.xml index e1c5a233eb9..f01b35eff12 100644 --- a/addons/portal_sale/portal_sale_view.xml +++ b/addons/portal_sale/portal_sale_view.xml @@ -1,7 +1,8 @@ - + + sale.order.form.payment sale.order @@ -12,6 +13,16 @@
    + + account.invoice.form.payment + account.invoice + + + + + + + + + account.config.settings + + + +
    + +
    +
    +
    +
    + + + \ No newline at end of file From bf4a26d3e8613c24b3a360013525cfef28458176 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 29 Oct 2012 12:01:54 +0100 Subject: [PATCH 024/191] [ADD] actual-RPC tests, with UI. Bump QUnit timeout to 10s so RPC can run at all bzr revid: xmo@openerp.com-20121029110154-5927gaix8k0ijl0c --- addons/web/controllers/testing.py | 2 +- addons/web/doc/images/db-query.png | Bin 0 -> 7142 bytes addons/web/doc/images/tests2.png | Bin 37769 -> 20114 bytes addons/web/doc/images/tests3.png | Bin 37827 -> 20382 bytes addons/web/doc/testing.rst | 86 +++++++++- addons/web/static/src/js/testing.js | 182 ++++++++++++++++------ addons/web_tests_demo/__init__.py | 14 ++ addons/web_tests_demo/static/test/demo.js | 15 ++ 8 files changed, 242 insertions(+), 57 deletions(-) create mode 100644 addons/web/doc/images/db-query.png diff --git a/addons/web/controllers/testing.py b/addons/web/controllers/testing.py index 0cfc633a492..9947f1e1ed5 100644 --- a/addons/web/controllers/testing.py +++ b/addons/web/controllers/testing.py @@ -53,7 +53,7 @@ TESTING = Template(u""" diff --git a/addons/web/doc/images/db-query.png b/addons/web/doc/images/db-query.png new file mode 100644 index 0000000000000000000000000000000000000000..e063b724001871f3846ead587810069347a8eab6 GIT binary patch literal 7142 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 U?IK$f`NBkzl~#}{kudW8FIjHf#{d8T literal 0 HcmV?d00001 diff --git a/addons/web/doc/images/tests2.png b/addons/web/doc/images/tests2.png index 59835b257b5e18f236303a34c66ede42f0bbc29e..c8a6f8ae9eefbeff46a9b45e3a9948676d820714 100644 GIT binary patch literal 20114 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 diff --git a/addons/web/doc/images/tests3.png b/addons/web/doc/images/tests3.png index 10b45eddedcbaf300d54245b85134efb2736c3cb..247f70716af614d48ed6255832cb2d71dce5d9f2 100644 GIT binary patch literal 20382 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 diff --git a/addons/web/doc/testing.rst b/addons/web/doc/testing.rst index 582960846e4..61077072ad2 100644 --- a/addons/web/doc/testing.rst +++ b/addons/web/doc/testing.rst @@ -297,8 +297,8 @@ test architecture was not warned about asynchronous operations. .. note:: - asynchronous test cases also have a 2 seconds timeout: if the test - does not finish within 2 seconds, it will be considered + asynchronous test cases also have a 10 seconds timeout: if the + test does not finish within 10 seconds, it will be considered failed. This pretty much always means the test will not resolve. .. note:: @@ -328,8 +328,8 @@ a test case (or its containing test suite) through Mock RPC ++++++++ -The preferred (and most fastest from a setup and execution time point -of view) way to do RPC during tests is to mock the RPC calls: while +The preferred (and fastest from a setup and execution time point of +view) way to do RPC during tests is to mock the RPC calls: while setting up the test case, provide what the RPC responses "should" be, and only test the code between the "user" (the test itself) and the RPC call, before the call is effectively done. @@ -412,7 +412,83 @@ To do this, set the :js:attr:`rpc option <~TestOptions.rpc>` to Actual RPC ++++++++++ -.. TODO:: rpc to database (implement & document) +A more realistic (but significantly slower and more expensive) way to +perform RPC calls is to perform actual calls to an actually running +OpenERP server. To do this, set the :js:attr:`rpc option +<~TestOptions.rpc>` to ``rpc``, it will not provide any new parameter +but will enable actual RPC, and the automatic creation and destruction +of databases (from a specified source) around tests. + +First, create a basic model we can test stuff with: + +.. code-block:: javascript + + from openerp.osv import orm, fields + + class TestObject(orm.Model): + _name = 'web_tests_demo.model' + + _columns = { + 'name': fields.char("Name", required=True), + 'thing': fields.char("Thing"), + 'other': fields.char("Other", required=True) + } + _defaults = { + 'other': "bob" + } + +then the actual test:: + + test('actual RPC', {rpc: 'rpc', asserts: 4}, function (instance) { + var Model = new instance.web.Model('web_tests_demo.model'); + return Model.call('create', [{name: "Bob"}]) + .pipe(function (id) { + return Model.call('read', [[id]]); + }).pipe(function (records) { + strictEqual(records.length, 1); + var record = records[0]; + strictEqual(record.name, "Bob"); + strictEqual(record.thing, false); + // default value + strictEqual(record.other, 'bob'); + }); + }); + +This test looks like a "mock" RPC test but for the lack of mock +response (and the different ``rpc`` type), however it has further +ranging consequences in that it will copy an existing database to a +new one, run the test in full on that temporary database and destroy +the database, to simulate an isolated and transactional context and +avoid affecting other tests. One of the consequences is that it takes +a *long* time to run (5~10s, most of that time being spent waiting for +a database duplication). + +Furthermore, as the test needs to clone a database, it also has to ask +which database to clone, the database/super-admin password and the +password of the ``admin`` user (in order to authenticate as said +user). As a result, the first time the test runner encounters an +``rpc: "rpc"`` test configuration it will produce the following +prompt: + +.. image:: ./images/db-query.png + :align: center + +and stop the testing process until the necessary information has been +provided. + +The prompt will only appear once per test run, all tests will use the +same "source" database. + +.. note:: + + The handling of that information is currently rather brittle and + unchecked, incorrect values will likely crash the runner. + +.. note:: + + The runner does not currently store this information (for any + longer than a test run that is), the prompt will have to be filled + every time. Testing API ----------- diff --git a/addons/web/static/src/js/testing.js b/addons/web/static/src/js/testing.js index 45728902e7f..744bd9fa2cd 100644 --- a/addons/web/static/src/js/testing.js +++ b/addons/web/static/src/js/testing.js @@ -85,6 +85,7 @@ openerp.testing = {}; }; }; + var db = window['oe_db_info'] || undefined; testing.section = function (name, options, body) { if (_.isFunction(options)) { body = options; @@ -115,32 +116,69 @@ openerp.testing = {}; // returns -1 -> index becomes 0 -> replace with ``undefined`` so // Array#slice returns a full copy 0, module_index + 1 || undefined); + + // Serialize options for this precise test case + // WARNING: typo is from jquery, do not fix! + var env = QUnit.config.currentModuleTestEnviroment; + var opts = _.defaults({ + // section setup + // case setup + // test + // case teardown + // section teardown + setup: function () { + var args = [].slice.call(arguments); + return $.when(env._oe.setup.apply(null, args)) + .pipe(function () { + return options.setup.apply(null, args); + }); + }, + teardown: function () { + var args = [].slice.call(arguments); + return $.when(options.teardown.apply(null, args)) + .pipe(function () { + return env._oe.teardown.apply(null, args); + }); + } + }, options, env._oe); + // FIXME: if this test is ignored, will still query + if (opts.rpc === 'rpc' && !db) { + QUnit.config.autostart = false; + db = { + source: null, + supadmin: null, + password: null + }; + var $msg = $('

    ') + .append('

    A test needs to clone a database

    ') + .append('

    Please provide the source clone information

    ') + .append(' Source DB: ').append('').append('
    ') + .append(' DB Password: ').append('').append('
    ') + .append('Admin Password: ').append('').append('
    ') + .append('') + .submit(function (e) { + e.preventDefault(); + e.stopPropagation(); + db.source = $msg.find('input[name=source]').val(); + db.supadmin = $msg.find('input[name=supadmin]').val(); + db.password = $msg.find('input[name=password]').val(); + QUnit.start(); + $.unblockUI(); + }); + $.blockUI({ + message: $msg, + css: { + fontFamily: 'monospace', + textAlign: 'left', + whiteSpace: 'pre-wrap', + cursor: 'default' + } + }); + } + QUnit.test(name, function () { // module testing environment var self = this; - var opts = _.defaults({ - // section setup - // case setup - // test - // case teardown - // section teardown - setup: function () { - if (self._oe.setup.apply(null, arguments)) { - throw new Error("Asynchronous setup not implemented"); - } - if (options.setup.apply(null, arguments)) { - throw new Error("Asynchronous setup not implemented"); - } - }, - teardown: function () { - if (options.teardown.apply(null, arguments)) { - throw new Error("Asynchronous teardown not implemented"); - } - if (self._oe.teardown(null, arguments)) { - throw new Error("Asynchronous teardown not implemented"); - } - } - }, options, this._oe); var instance; if (!opts.dependencies) { @@ -202,38 +240,80 @@ openerp.testing = {}; break; case 'rpc': async = true; + (function (setup, teardown) { + // Bunch of random base36 characters + var dbname = 'test_' + Math.random().toString(36).slice(2); + opts.setup = function (instance, $s) { + // FIXME hack: don't want the session to go through shitty loading process of everything + instance.session.session_init = testing.noop; + instance.session.load_modules = testing.noop; + instance.session.session_bind(); + return instance.session.rpc('/web/database/duplicate', { + fields: [ + {name: 'super_admin_pwd', value: db.supadmin}, + {name: 'db_original_name', value: db.source}, + {name: 'db_name', value: dbname} + ] + }).pipe(function (result) { + if (result.error) { + return $.Deferred().reject(result.error).promise(); + } + return instance.session.session_authenticate( + dbname, 'admin', db.password, true); + }).pipe(function () { + return setup(instance, $s); + }); + }; + opts.teardown = function (instance, $s) { + return $.when(teardown(instance, $s)).pipe(function () { + return instance.session.session_logout() + }).pipe(function () { + return instance.session.rpc('/web/database/drop', { + fields: [ + {name: 'drop_pwd', value: db.supadmin}, + {name: 'drop_db', value: db.dbname} + ] + }); + }).pipe(function (result) { + if (result.error) { + return $.Deferred().reject(result.error).promise(); + } + return result; + }); + }; + })(opts.setup, opts.teardown); } - // TODO: async setup/teardown - opts.setup(instance, $fixture, mock); - - var result = callback(instance, $fixture, mock); - - // TODO: cleanup which works on errors - if (!(result && _.isFunction(result.then))) { - if (async) { - ok(false, "asynchronous test cases must return a promise"); - } - opts.teardown(instance, $fixture, mock); - return; - } - + // Always execute tests asynchronously stop(); - if (!_.isNumber(opts.asserts)) { - ok(false, "asynchronous test cases must specify the " - + "number of assertions they expect"); - } - result.always(function () { - start(); - opts.teardown(instance, $fixture, mock); - }).fail(function (error) { - if (options.fail_on_rejection === false) { - return; + $.when(opts.setup(instance, $fixture, mock)) + .pipe(function () { + var result = callback(instance, $fixture, mock); + if (!(result && _.isFunction(result.then))) { + if (async) { + ok(false, "asynchronous test cases must return a promise"); + } + } else { + if (!_.isNumber(opts.asserts)) { + ok(false, "asynchronous test cases must specify the " + + "number of assertions they expect"); + } } - ok(false, typeof error === 'object' && error.message - ? error.message - : JSON.stringify([].slice.call(arguments))); - }) + return $.when(result).fail(function (error) { + if (options.fail_on_rejection === false) { + return; + } + ok(false, typeof error === 'object' && error.message + ? error.message + : JSON.stringify([].slice.call(arguments))); + }) + }).pipe(function () { + return opts.teardown(instance, $fixture, mock); + }, function () { + return opts.teardown(instance, $fixture, mock); + }).always(function () { + start(); + }); }); }; })(openerp.testing); diff --git a/addons/web_tests_demo/__init__.py b/addons/web_tests_demo/__init__.py index e69de29bb2d..137d472ec74 100644 --- a/addons/web_tests_demo/__init__.py +++ b/addons/web_tests_demo/__init__.py @@ -0,0 +1,14 @@ +from openerp.osv import orm, fields + +class TestObject(orm.Model): + _name = 'web_tests_demo.model' + + _columns = { + 'name': fields.char("Name", required=True), + 'thing': fields.char("Thing"), + 'other': fields.char("Other", required=True) + } + _defaults = { + 'other': "bob" + } + diff --git a/addons/web_tests_demo/static/test/demo.js b/addons/web_tests_demo/static/test/demo.js index 0bf5a1a58b6..62cecf6f088 100644 --- a/addons/web_tests_demo/static/test/demo.js +++ b/addons/web_tests_demo/static/test/demo.js @@ -84,4 +84,19 @@ openerp.testing.section('basic section', function (test) { deepEqual(dbm.db_list, ['foo', 'bar', 'baz']); }); }); + + test('actual RPC', {rpc: 'rpc', asserts: 4}, function (instance) { + var Model = new instance.web.Model('web_tests_demo.model'); + return Model.call('create', [{name: "Bob"}]) + .pipe(function (id) { + return Model.call('read', [[id]]); + }).pipe(function (records) { + strictEqual(records.length, 1); + var record = records[0]; + strictEqual(record.name, "Bob"); + strictEqual(record.thing, false); + // default value + strictEqual(record.other, 'bob'); + }); + }); }); From 21e776a56b9fedb25c48b1ea362227ed76de6321 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 29 Oct 2012 12:09:02 +0100 Subject: [PATCH 025/191] [IMP] document test runner global conf keys bzr revid: xmo@openerp.com-20121029110902-ob21050di74vcyqc --- addons/web/doc/testing.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/addons/web/doc/testing.rst b/addons/web/doc/testing.rst index 61077072ad2..4760f0780eb 100644 --- a/addons/web/doc/testing.rst +++ b/addons/web/doc/testing.rst @@ -409,6 +409,8 @@ To do this, set the :js:attr:`rpc option <~TestOptions.rpc>` to handler containing assertions, it multiplies the effective number of assertions) +.. _testing-rpc-rpc: + Actual RPC ++++++++++ @@ -568,6 +570,19 @@ Testing API should be loaded into QWeb before starting the test. A boolean, ``false`` by default. +The test runner can also use two global configuration values set +directly on the ``window`` object: + +* ``oe_all_dependencies`` is an ``Array`` of all modules with a web + component, ordered by dependency (for a module ``A`` with + dependencies ``A'``, any module of ``A'`` must come before ``A`` in + the array) + +* ``oe_db_info`` is an object with 3 keys ``source``, ``supadmin`` and + ``password``. It is used to pre-configure :ref:`actual RPC + ` tests, to avoid a prompt being displayed + (especially for headless situations). + Running through Python ---------------------- From a9f1489fd0dc32901823fbe09fc323479a444048 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 29 Oct 2012 13:00:04 +0100 Subject: [PATCH 026/191] [FIX] typos bzr revid: xmo@openerp.com-20121029120004-d8tv0hks9d6684az --- addons/web/doc/testing.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/web/doc/testing.rst b/addons/web/doc/testing.rst index 4760f0780eb..2d5876776a3 100644 --- a/addons/web/doc/testing.rst +++ b/addons/web/doc/testing.rst @@ -199,7 +199,7 @@ testing them. Thus, test cases get a DOM scratchpad as its second positional parameter, in a jQuery instance. That scratchpad is fully cleaned up before each test, and as long as it doesn't do anything outside the -scrartchpad your code can do whatever it wants:: +scratchpad your code can do whatever it wants:: // test/demo.js test('DOM content', function (instance, $scratchpad) { @@ -334,7 +334,7 @@ setting up the test case, provide what the RPC responses "should" be, and only test the code between the "user" (the test itself) and the RPC call, before the call is effectively done. -To do this, set the :js:attr:`rpc option <~TestOptions.rpc>` to +To do this, set the :js:attr:`rpc option ` to ``mock``. This will add a third parameter to the test case callback: .. js:function:: mock(rpc_spec, handler) @@ -349,8 +349,8 @@ To do this, set the :js:attr:`rpc option <~TestOptions.rpc>` to In that case, ``handler`` should be a function taking two arguments ``args`` and ``kwargs``, matching the corresponding - arguments on the server side. Hander should simply return the - value as if it were returned by the Python XMLRPC handler:: + arguments on the server side and should simply return the value + as if it were returned by the Python XMLRPC handler:: test('XML-RPC', {rpc: 'mock', asserts: 3}, function (instance, $s, mock) { // set up mocking From e7fbd7290f430dd67e9b3f093aea837bdef6f81d Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 29 Oct 2012 13:04:26 +0100 Subject: [PATCH 027/191] [IMP] add a doc blurb to testing.section and testing.case bzr revid: xmo@openerp.com-20121029120426-xdk03e5gvv1n03jw --- addons/web/doc/testing.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/addons/web/doc/testing.rst b/addons/web/doc/testing.rst index 2d5876776a3..e870ec091b7 100644 --- a/addons/web/doc/testing.rst +++ b/addons/web/doc/testing.rst @@ -497,6 +497,13 @@ Testing API .. js:function:: openerp.testing.section(name[, options], body) + A test section, serves as shared namespace for related tests (for + constants or values to only set up once). The ``body`` function + should contain the tests themselves. + + Note that the order in which tests are run is essentially + undefined, do *not* rely on it. + :param String name: :param TestOptions options: :param body: @@ -504,6 +511,10 @@ Testing API .. js:function:: openerp.testing.case(name[, options], callback) + Registers a test case callback in the test runner, the callback + will only be run once the runner is started (or maybe not at all, + if the test is filtered out). + :param String name: :param TestOptions options: :param callback: From 55ce6a560fb31e86b3a0ae8d21ccaec9e48db3da Mon Sep 17 00:00:00 2001 From: "pankita shah (Open ERP)" Date: Mon, 29 Oct 2012 18:12:26 +0530 Subject: [PATCH 028/191] [FIX] when user select lead and get draft mode bzr revid: shp@tinyerp.com-20121029124226-cjn6s3sj434rm5q7 --- addons/crm/crm_lead.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index 9ee5aeb2301..00ee7582c4d 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -50,7 +50,7 @@ class crm_lead(base_stage, format_address, osv.osv): def _get_default_stage_id(self, cr, uid, context=None): """ Gives default stage_id """ section_id = self._get_default_section_id(cr, uid, context=context) - return self.stage_find(cr, uid, [], section_id, [('state', '=', 'draft'), ('type', '=', 'both')], context=context) + return self.stage_find(cr, uid, [], section_id, [('state', '=', 'draft'),'|', ('type', '=', 'lead'),('type', '=', 'both'),], context=context) def _resolve_section_id_from_context(self, cr, uid, context=None): """ Returns ID of section based on the value of 'section_id' @@ -340,7 +340,7 @@ class crm_lead(base_stage, format_address, osv.osv): cases = self.browse(cr, uid, cases, context=context) # collect all section_ids section_ids = [] - types = ['both'] + types = ['lead','opportunity','both'] if section_id: section_ids.append(section_id) for lead in cases: From 084a4217a7022fbf9ecaca6bbfed135798fe856c Mon Sep 17 00:00:00 2001 From: "pankita shah (Open ERP)" Date: Mon, 29 Oct 2012 18:53:13 +0530 Subject: [PATCH 029/191] [FIX] fixed when select type opportunuty then opportunuty in draft mode bzr revid: shp@tinyerp.com-20121029132313-5ctreoq9nv2fxvxc --- addons/crm/crm_lead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index 00ee7582c4d..da0a3f7d756 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -50,7 +50,7 @@ class crm_lead(base_stage, format_address, osv.osv): def _get_default_stage_id(self, cr, uid, context=None): """ Gives default stage_id """ section_id = self._get_default_section_id(cr, uid, context=context) - return self.stage_find(cr, uid, [], section_id, [('state', '=', 'draft'),'|', ('type', '=', 'lead'),('type', '=', 'both'),], context=context) + return self.stage_find(cr, uid, [], section_id, [('state', '=', 'draft'),'|', ('type', '=', 'lead'),'|',('type', '=', 'both'),('type','=','opportunity')], context=context) def _resolve_section_id_from_context(self, cr, uid, context=None): """ Returns ID of section based on the value of 'section_id' From 97208c971b9155cffa57f403109bc92104af9938 Mon Sep 17 00:00:00 2001 From: Antonin Bourguignon Date: Mon, 29 Oct 2012 18:37:14 +0100 Subject: [PATCH 030/191] [IMP] avoid tabs (4 spaces) in the middle of help text bzr revid: abo@openerp.com-20121029173714-qpr4rrhll576jxmm --- addons/crm/crm_lead.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index da0a3f7d756..0366045b90f 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -224,11 +224,7 @@ class crm_lead(base_stage, format_address, osv.osv): multi='day_close', type="float", store=True), 'state': fields.related('stage_id', 'state', type="selection", store=True, selection=crm.AVAILABLE_STATES, string="Status", readonly=True, - help='The Status is set to \'Draft\', when a case is created.\ - If the case is in progress the Status is set to \'Open\'.\ - When the case is over, the Status is set to \'Done\'.\ - If the case needs to be reviewed then the Status is \ - set to \'Pending\'.'), + help='The Status is set to \'Draft\', when a case is created. If the case is in progress the Status is set to \'Open\'. When the case is over, the Status is set to \'Done\'. If the case needs to be reviewed then the Status is set to \'Pending\'.'), # Only used for type opportunity 'probability': fields.float('Success Rate (%)',group_operator="avg"), From 7a8d6753021ea86f72ab2a7aca374e70620aa03c Mon Sep 17 00:00:00 2001 From: Antonin Bourguignon Date: Mon, 29 Oct 2012 19:11:56 +0100 Subject: [PATCH 031/191] [IMP] code style remove mutable default remove useless whitespaces bzr revid: abo@openerp.com-20121029181156-ao52q3uun3ilp2qk --- addons/crm/crm_lead.py | 4 ++- addons/crm/crm_lead_view.xml | 66 ++++++++++++++++++------------------ 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index 0366045b90f..4074856ad81 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -324,7 +324,7 @@ class crm_lead(base_stage, format_address, osv.osv): cases = self.browse(cr, uid, ids2, context=context) return self._action(cr, uid, cases, False, context=context) - def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None): + def stage_find(self, cr, uid, cases, section_id, domain=None, order='sequence', context=None): """ Override of the base.stage method Parameter of the stage search taken from the lead: - type: stage type must be the same or 'both' @@ -332,6 +332,8 @@ class crm_lead(base_stage, format_address, osv.osv): be a default stage; if not set, stages must be default stages """ + if domain == None: + domain = [] if isinstance(cases, (int, long)): cases = self.browse(cr, uid, cases, context=context) # collect all section_ids diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml index b6890a7de07..a05af900bb9 100644 --- a/addons/crm/crm_lead_view.xml +++ b/addons/crm/crm_lead_view.xml @@ -14,9 +14,9 @@ action_crm_lead_unread - + action - + crm.lead client_action_multi @@ -32,9 +32,9 @@ action_crm_lead_read - + action - + crm.lead client_action_multi @@ -143,7 +143,7 @@
    - @@ -253,12 +253,12 @@ - + - + @@ -276,8 +276,8 @@ - - + + @@ -354,14 +354,14 @@ - + - + - - + + @@ -415,7 +415,7 @@ name="action_makeMeeting" type="object" context="{'search_default_attendee_id': active_id, 'default_attendee_id' : active_id}" - /> + />
    -