diff --git a/connector_carepoint/consumer.py b/connector_carepoint/consumer.py index f6184b7..6cc0ba3 100644 --- a/connector_carepoint/consumer.py +++ b/connector_carepoint/consumer.py @@ -3,14 +3,13 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # from openerp.addons.connector.connector import Binder -from .unit.export_synchronizer import (export_record, - ) +from .unit.export_synchronizer import export_record # from .unit.delete_synchronizer import export_delete_record # from .connector import get_environment -# from openerp.addons.connector.event import (on_record_write, -# on_record_create, -# on_record_unlink -# ) +from openerp.addons.connector.event import (on_record_write, + on_record_create, + # on_record_unlink + ) import logging @@ -21,10 +20,6 @@ # 'carepoint.carepoint.address', # 'carepoint.carepoint.address.patient', # ]) -# @on_record_write(model_names=['carepoint.medical.patient', -# 'carepoint.carepoint.address', -# 'carepoint.carepoint.address.patient', -# ]) def delay_export(session, model_name, record_id, vals): """ Delay a job which export a binding record. (A binding record being a ``carepoint.res.partner``, @@ -36,10 +31,10 @@ def delay_export(session, model_name, record_id, vals): export_record.delay(session, model_name, record_id, fields=fields) -# @on_record_write(model_names=['medical.patient', -# 'carepoint.address', -# 'carepoint.address.patient', -# ]) +@on_record_write(model_names=['medical.patient', + 'carepoint.address', + 'carepoint.address.patient', + ]) def delay_export_all_bindings(session, model_name, record_id, vals): """ Delay a job which export all the bindings of a record. In this case, it is called on records of normal models and will delay @@ -54,6 +49,25 @@ def delay_export_all_bindings(session, model_name, record_id, vals): fields=fields) +@on_record_create(model_names=['medical.patient', + 'carepoint.address', + 'carepoint.address.patient', + ]) +def delay_create(session, model_name, record_id, vals): + """ Create a new binding record, then trigger delayed export + In this case, it is called on records of normal models to create + binding record, and trigger external system export + """ + model_obj = session.env['carepoint.%s' % model_name].with_context( + connector_no_export=True, + ) + if not len(model_obj.search([('odoo_id', '=', record_id)])): + model_obj.create({ + 'odoo_id': record_id, + }) + delay_export_all_bindings(session, model_name, record_id, vals) + + # @on_record_unlink(model_names=['carepoint.medical.patient', # 'carepoint.carepoint.address', # 'carepoint.carepoint.address.patient', diff --git a/connector_carepoint/models/address.py b/connector_carepoint/models/address.py index 64d87f5..237e449 100644 --- a/connector_carepoint/models/address.py +++ b/connector_carepoint/models/address.py @@ -7,6 +7,7 @@ from openerp.addons.connector.connector import ConnectorUnit from openerp.addons.connector.unit.mapper import (mapping, + changed_by, ExportMapper ) from ..unit.backend_adapter import CarepointCRUDAdapter @@ -16,8 +17,7 @@ from ..unit.import_synchronizer import (DelayedBatchImporter, CarepointImporter, ) -from ..unit.export_synchronizer import (CarepointExporter) -from ..unit.delete_synchronizer import (CarepointDeleter) +from ..unit.export_synchronizer import CarepointExporter _logger = logging.getLogger(__name__) @@ -99,9 +99,11 @@ def _get_partner_sync_vals(self, partner): """ vals = {} for attr in self.PARTNER_ATTRS: - val = getattr(partner, attr, None) - if getattr(val, 'id', None): + val = getattr(partner, attr, False) + if getattr(val, 'id', False): val = val.id + if not val: + val = False vals[attr] = val return vals @@ -179,10 +181,33 @@ class CarepointAddressExportMapper(ExportMapper): direct = [ ('street', 'addr1'), - ('email', 'addr2'), + ('street2', 'addr2'), ('city', 'city'), ] + @mapping + @changed_by('state_id') + def state_cd(self, record): + return {'state_cd': record.state_id.code} + + @mapping + @changed_by('zip') + def zip_and_plus_four(self, record): + if not record.zip: + return + _zip = record.zip.replace('-', '') + if len(_zip) > 5: + return { + 'zip': _zip[0:5], + 'zip_plus4': _zip[5:], + } + return {'zip': _zip} + + @mapping + @changed_by('country_id') + def country_cd(self, record): + return {'country_cd': record.country_id.code} + @mapping def addr_id(self, record): return {'addr_id': record.carepoint_id} @@ -192,8 +217,3 @@ def addr_id(self, record): class CarepointAddressExporter(CarepointExporter): _model_name = ['carepoint.carepoint.address'] _base_mapper = CarepointAddressExportMapper - - -@carepoint -class CarepointAddressDeleteSynchronizer(CarepointDeleter): - _model_name = ['carepoint.carepoint.address'] diff --git a/connector_carepoint/models/address_abstract.py b/connector_carepoint/models/address_abstract.py index be240cb..983d8b0 100644 --- a/connector_carepoint/models/address_abstract.py +++ b/connector_carepoint/models/address_abstract.py @@ -3,9 +3,10 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import logging -from openerp import models, fields, api +from openerp import models, fields, api, _ from openerp.addons.connector.unit.mapper import (mapping, only_create, + ExportMapper, ) from ..unit.mapper import CarepointImportMapper from ..backend import carepoint @@ -37,11 +38,35 @@ class CarepointAddressAbstract(models.AbstractModel): string='Partner', comodel_name='res.partner', required=True, + store=True, compute='_compute_partner_id', inverse='_set_partner_id', ) + res_model = fields.Char( + string='Resource Model', + default=lambda s: s._default_res_model(), + ) + res_id = fields.Integer( + string='Resource PK', + compute='_compute_res_id', + store=True, + ) + + @property + @api.multi + def medical_entity_id(self): + self.ensure_one() + return self.env[self.res_model].browse(self.res_id) + + @api.model + def _default_res_model(self): + """ It returns the res model. Should be overloaded in children """ + raise NotImplementedError( + _('_default_res_model should be implemented in child classes') + ) @api.multi + @api.depends('partner_id') def _compute_partner_id(self): """ It sets the partner_id from the address_id """ for rec_id in self: @@ -71,6 +96,44 @@ def _set_partner_id(self): else: rec_id.address_id.write(vals) + @api.multi + @api.depends('partner_id', 'res_model') + def _compute_res_id(self): + """ It computes the resource ID from model """ + for rec_id in self: + if not all([rec_id.res_model, rec_id.partner_id]): + continue + medical_entity = self.env[rec_id.res_model].search([ + ('partner_id', '=', rec_id.partner_id.id), + ], + limit=1, + ) + rec_id.res_id = medical_entity.id + + @api.model + def _get_by_partner(self, partner, create=True, recurse=False): + """ It returns the address associated to the partner. + Params: + partner: Recordset singleton of partner to search for + create: Bool determining whether to create address if not exist + recurse: Bool determining whether to recurse into children (this + is only really useful when create=True) + Return: + Recordset of partner address + """ + address = self.search([('partner_id', '=', partner.id)], limit=1) + vals = self.address_id._get_partner_sync_vals(partner) + _logger.info('_get_by_partner %s, %s, %s' % (address, partner, vals)) + if not address and create: + vals['partner_id'] = partner.id + address = self.create(vals) + else: + address.write(vals) + if recurse: + for child in partner.child_ids: + self._get_by_partner(child, create, recurse) + return address + @carepoint class CarepointAddressAbstractImportMapper(CarepointImportMapper): @@ -116,6 +179,19 @@ def partner_id(self, record, medical_entity): partner = self.env['res.partner'].create(vals) return {'partner_id': partner.id} + def res_model_and_id(self, record, medical_entity): + """ It returns the vals dict for res_model and res_id + Params: + record: ``dict`` of carepoint record + medical_entity: Recordset with a ``partner_id`` column + Return: + ``dict`` of values for mapping + """ + return { + 'res_id': medical_entity.id, + 'res_model': medical_entity._name, + } + @mapping @only_create def address_id(self, record): @@ -139,3 +215,21 @@ def _create(self, data): self.session, binding._name, binding.id, binding.backend_id.id ) return binding + + +@carepoint +class CarepointAddressAbstractExportMapper(ExportMapper): + + @mapping + def addr_id(self, binding): + binder = self.binder_for('carepoint.carepoint.address') + rec_id = binder.to_backend(binding.address_id.id) + return {'addr_id': rec_id} + + @mapping + def static_defaults(self, binding): + return { + 'priority': 2, + 'addr_type_cn': 2, + 'app_flags': 0, + } diff --git a/connector_carepoint/models/address_patient.py b/connector_carepoint/models/address_patient.py index 32874e8..59dab5c 100644 --- a/connector_carepoint/models/address_patient.py +++ b/connector_carepoint/models/address_patient.py @@ -3,7 +3,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import logging -from openerp import models, fields +from openerp import models, fields, api, _ from openerp.addons.connector.connector import ConnectorUnit from openerp.addons.connector.unit.mapper import (mapping, only_create, @@ -11,9 +11,11 @@ from ..unit.backend_adapter import CarepointCRUDAdapter from ..backend import carepoint from ..unit.import_synchronizer import DelayedBatchImporter +from ..unit.export_synchronizer import CarepointExporter from .address_abstract import (CarepointAddressAbstractImportMapper, CarepointAddressAbstractImporter, + CarepointAddressAbstractExportMapper, ) _logger = logging.getLogger(__name__) @@ -49,6 +51,11 @@ class CarepointAddressPatient(models.Model): string='Carepoint Bindings', ) + @api.model + def _default_res_model(self): + """ It returns the res model. """ + return 'medical.patient' + @carepoint class CarepointAddressPatientAdapter(CarepointCRUDAdapter): @@ -81,6 +88,16 @@ def partner_id(self, record): record, patient_id, ) + @mapping + @only_create + def res_model_and_id(self, record): + binder = self.binder_for('carepoint.medical.patient') + patient_id = binder.to_odoo(record['pat_id'], browse=True) + _sup = super(CarepointAddressPatientImportMapper, self) + return _sup.res_model_and_id( + record, patient_id, + ) + @mapping def carepoint_id(self, record): return {'carepoint_id': '%d,%d' % (record['pat_id'], @@ -111,3 +128,35 @@ def _import_addresses(self, patient_id, partner_binding): address_ids = adapter.search(pat_id=patient_id) for address_id in address_ids: importer.run(address_id) + + +@carepoint +class CarepointAddressPatientExportMapper( + CarepointAddressAbstractExportMapper +): + _model_name = 'carepoint.carepoint.address.patient' + + @mapping + def pat_id(self, binding): + binder = self.binder_for('carepoint.medical.patient') + rec_id = binder.to_backend(binding.res_id) + return {'pat_id': rec_id} + + +@carepoint +class CarepointAddressPatientExporter(CarepointExporter): + _model_name = 'carepoint.carepoint.address.patient' + _base_mapper = CarepointAddressPatientExportMapper + + @mapping + def _export_dependencies(self): + model = self._model_name + if isinstance(model, list): + model = model[0] + _logger.debug( + _('_model_name was a list, using %s from %s') % ( + self._model_name, model, + ) + ) + self._export_dependency(self.binding_record.address_id, + model) diff --git a/connector_carepoint/models/medical_patient.py b/connector_carepoint/models/medical_patient.py index 792d8e9..d86e529 100644 --- a/connector_carepoint/models/medical_patient.py +++ b/connector_carepoint/models/medical_patient.py @@ -17,7 +17,7 @@ from ..unit.import_synchronizer import (DelayedBatchImporter, CarepointImporter, ) -from ..unit.export_synchronizer import (CarepointExporter) +from ..unit.export_synchronizer import CarepointExporter from .address_patient import CarepointAddressPatientUnit @@ -76,8 +76,13 @@ class MedicalPatientImportMapper(PersonImportMapper): (trim('email'), 'email'), ('birth_date', 'dob'), ('death_date', 'dod'), + ('pat_status_cn', 'active'), ] + @mapping + def safety_cap_yn(self, record): + return {'safety_caps_yn': not record['no_safety_caps_yn']} + @mapping def gender(self, record): gender = record.get('gender_cd') @@ -124,19 +129,40 @@ class MedicalPatientExportMapper(PersonExportMapper): ('email', 'email'), ('dob', 'birth_date'), ('dod', 'death_date'), + ('active', 'pat_status_cn') ] - @mapping - def pat_id(self, record): - return {'pat_id': record.carepoint_id} - @mapping @changed_by('gender') def gender_cd(self, record): - return {'gender_cd': record.get('gender').upper()} + if record.gender: + return {'gender_cd': record.gender.upper()} + + @mapping + def static_defaults(self, record): + """ It provides all static default mappings """ + return { + 'pat_type_cn': 1, + 'bad_check_yn': 0, + 'app_flags': 0, + 'comp_cn': 0, + 'status_cn': 0, + } + + @mapping + @changed_by('safety_cap_yn') + def no_safety_caps_yn(self, record): + return {'no_safety_caps_yn': not record.safety_cap_yn} @carepoint class MedicalPatientExporter(CarepointExporter): _model_name = ['carepoint.medical.patient'] _base_mapper = MedicalPatientExportMapper + + def _after_export(self): + self.env['carepoint.address.patient']._get_by_partner( + self.binding_record.commercial_partner_id, + create=True, + recurse=True, + ) diff --git a/connector_carepoint/tests/models/__init__.py b/connector_carepoint/tests/models/__init__.py index cb8ec4c..afbb76f 100644 --- a/connector_carepoint/tests/models/__init__.py +++ b/connector_carepoint/tests/models/__init__.py @@ -19,3 +19,4 @@ from . import test_address_patient from . import test_address_store from . import test_address_organization +from . import test_medical_patient diff --git a/connector_carepoint/tests/models/test_address_abstract.py b/connector_carepoint/tests/models/test_address_abstract.py index 2d3547c..8f58e9c 100644 --- a/connector_carepoint/tests/models/test_address_abstract.py +++ b/connector_carepoint/tests/models/test_address_abstract.py @@ -57,11 +57,12 @@ def new_address(self, partner=None): return self.env['carepoint.address'].create(vals) def new_patient_address(self): - patient = self.new_patient() - self.address = self.new_address(patient.partner_id) + self.patient = self.new_patient() + self.address = self.new_address(self.patient.partner_id) return self.model.create({ - 'partner_id': patient.partner_id, + 'partner_id': self.patient.partner_id.id, 'address_id': self.address.id, + 'res_model': 'medical.patient', }) def test_compute_partner_id(self): @@ -94,6 +95,21 @@ def test_set_partner_id_with_address(self): address.street, ) + def test_medical_entity_id(self): + """ It should return patient record """ + address = self.new_patient_address() + self.assertEqual( + self.patient, + address.medical_entity_id, + ) + + def test_compute_res_id(self): + address = self.new_patient_address() + self.assertEqual( + self.patient.id, + address.res_id, + ) + class TestAddressAbstractImportMapper(AddressAbstractTestBase): @@ -172,6 +188,16 @@ def test_address_id_return(self): expect = self.unit.binder_for().to_odoo() self.assertDictEqual({'address_id': expect}, res) + def test_res_model_and_id(self): + """ It should return values dict for medical entity """ + entity = mock.MagicMock() + expect = { + 'res_id': entity.id, + 'res_model': entity._name, + } + res = self.unit.res_model_and_id(None, entity) + self.assertDictEqual(expect, res) + class TestAddressAbstractImporter(AddressAbstractTestBase): @@ -191,3 +217,46 @@ def test_import_dependencies(self): 'carepoint.carepoint.address', ), ]) + + +class TestAddressAbstractExportMapper(AddressAbstractTestBase): + + def setUp(self): + super(TestAddressAbstractExportMapper, self).setUp() + self.Unit = address_abstract.CarepointAddressAbstractExportMapper + self.unit = self.Unit(self.mock_env) + self.record = mock.MagicMock() + + def test_addr_id_get_binder(self): + """ It should get binder for prescription line """ + with mock.patch.object(self.unit, 'binder_for'): + self.unit.binder_for.side_effect = EndTestException + with self.assertRaises(EndTestException): + self.unit.addr_id(self.record) + self.unit.binder_for.assert_called_once_with( + 'carepoint.carepoint.address' + ) + + def test_addr_id_to_backend(self): + """ It should get backend record for rx """ + with mock.patch.object(self.unit, 'binder_for'): + self.unit.binder_for().to_backend.side_effect = EndTestException + with self.assertRaises(EndTestException): + self.unit.addr_id(self.record) + self.unit.binder_for().to_backend.assert_called_once_with( + self.record.address_id.id, + ) + + def test_addr_id_return(self): + """ It should return formatted addr_id """ + with mock.patch.object(self.unit, 'binder_for'): + res = self.unit.addr_id(self.record) + expect = self.unit.binder_for().to_backend() + self.assertDictEqual({'addr_id': expect}, res) + + def test_static_defaults(self): + """ It should return a dict of default values """ + self.assertIsInstance( + self.unit.static_defaults(self.record), + dict, + ) diff --git a/connector_carepoint/tests/models/test_address_patient.py b/connector_carepoint/tests/models/test_address_patient.py index 9779d9b..b827334 100644 --- a/connector_carepoint/tests/models/test_address_patient.py +++ b/connector_carepoint/tests/models/test_address_patient.py @@ -82,16 +82,7 @@ def setUp(self): @mock.patch('%s.CarepointAddressAbstractImporter' % _file, spec=address_patient.CarepointAddressAbstractImporter, ) - def test_import_dependencies_super(self, _super): - """ It should call the super """ - _super()._import_dependencies.side_effect = EndTestException - with self.assertRaises(EndTestException): - self.unit._import_dependencies() - - @mock.patch('%s.CarepointAddressAbstractImporter' % _file, - spec=address_patient.CarepointAddressAbstractImporter, - ) - def test_import_dependencies_super(self, _super): + def test_import_dependencies_import(self, _super): """ It should import all dependencies """ with mock.patch.object(self.unit, '_import_dependency') as mk: self.unit._import_dependencies() @@ -139,3 +130,72 @@ def test_import_addresses_import(self): mk().search.return_value = [expect] self.unit._import_addresses(1, None) mk().run.assert_called_once_with(expect) + + +class TestAddressPatientExportMapper(AddressPatientTestBase): + + def setUp(self): + super(TestAddressPatientExportMapper, self).setUp() + self.Unit = address_patient.CarepointAddressPatientExportMapper + self.unit = self.Unit(self.mock_env) + self.record = mock.MagicMock() + + def test_pat_id_get_binder(self): + """ It should get binder for prescription line """ + with mock.patch.object(self.unit, 'binder_for'): + self.unit.binder_for.side_effect = EndTestException + with self.assertRaises(EndTestException): + self.unit.pat_id(self.record) + self.unit.binder_for.assert_called_once_with( + 'carepoint.medical.patient' + ) + + def test_pat_id_to_backend(self): + """ It should get backend record for rx """ + with mock.patch.object(self.unit, 'binder_for'): + self.unit.binder_for().to_backend.side_effect = EndTestException + with self.assertRaises(EndTestException): + self.unit.pat_id(self.record) + self.unit.binder_for().to_backend.assert_called_once_with( + self.record.res_id, + ) + + def test_pat_id_return(self): + """ It should return formatted pat_id """ + with mock.patch.object(self.unit, 'binder_for'): + res = self.unit.pat_id(self.record) + expect = self.unit.binder_for().to_backend() + self.assertDictEqual({'pat_id': expect}, res) + + +class TestAddressPatientExporter(AddressPatientTestBase): + + def setUp(self): + super(TestAddressPatientExporter, self).setUp() + self.Unit = address_patient.CarepointAddressPatientExporter + self.unit = self.Unit(self.mock_env) + self.record = mock.MagicMock() + self.unit.binding_record = self.record + + def test_export_dependencies_export(self): + """ It should export all dependencies """ + with mock.patch.object(self.unit, '_export_dependency') as mk: + self.unit._export_dependencies() + mk.assert_has_calls([ + mock.call( + self.record.address_id, + self.unit._model_name, + ), + ]) + + def test_export_dependencies_list(self): + """ It should correctly handle list _model_name as list """ + with mock.patch.object(self.unit, '_export_dependency') as mk: + self.unit._model_name = ['expect'] + self.unit._export_dependencies() + mk.assert_has_calls([ + mock.call( + self.record.address_id, + self.unit._model_name[0], + ), + ]) diff --git a/connector_carepoint/tests/models/test_medical_patient.py b/connector_carepoint/tests/models/test_medical_patient.py new file mode 100644 index 0000000..d912276 --- /dev/null +++ b/connector_carepoint/tests/models/test_medical_patient.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +# Copyright 2015-2016 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import mock + +from openerp.addons.connector_carepoint.models import medical_patient + +from ..common import SetUpCarepointBase + + +class EndTestException(Exception): + pass + + +class MedicalPatientTestBase(SetUpCarepointBase): + + def setUp(self): + super(MedicalPatientTestBase, self).setUp() + self.model = 'carepoint.medical.patient' + self.mock_env = self.get_carepoint_helper( + self.model + ) + self.record = { + 'no_safety_caps_yn': True, + 'pat_id': 1, + 'gender_cd': 'M', + 'birth_date': '2016-09-10', + 'fname': 'FirstName', + 'lname': 'LastName', + } + + +class TestMedicalPatientImportMapper(MedicalPatientTestBase): + + def setUp(self): + super(TestMedicalPatientImportMapper, self).setUp() + self.Unit = medical_patient.MedicalPatientImportMapper + self.unit = self.Unit(self.mock_env) + + def _create_patient(self): + return self.env['medical.patient'].create({ + 'name': '%s %s' % (self.record['fname'], + self.record['lname']), + 'dob': self.record['birth_date'], + }) + + def test_safety_caps_yn(self): + """ It should return proper dict vals """ + self.assertDictEqual( + {'safety_caps_yn': False}, + self.unit.safety_cap_yn(self.record), + ) + + def test_gender_exist(self): + """ It should return lowercase gender code """ + self.assertDictEqual( + {'gender': self.record['gender_cd'].lower()}, + self.unit.gender(self.record) + ) + + def test_gender_none(self): + """ It should return None when no gender """ + self.record['gender_cd'] = False + self.assertDictEqual( + {'gender': None}, + self.unit.gender(self.record) + ) + + def test_carepoint_id(self): + """ It should return lowercase gender code """ + self.assertDictEqual( + {'carepoint_id': self.record['pat_id']}, + self.unit.carepoint_id(self.record) + ) + + def test_odoo_id(self): + """ It should return odoo_id of patient with same name """ + expect = self._create_patient() + res = self.unit.odoo_id(self.record) + expect = {'odoo_id': expect.id} + self.assertDictEqual(expect, res) + + +class TestMedicalPatientImporter(MedicalPatientTestBase): + + def setUp(self): + super(TestMedicalPatientImporter, self).setUp() + self.Unit = medical_patient.MedicalPatientImporter + self.unit = self.Unit(self.mock_env) + self.unit.carepoint_record = self.record + + def test_after_import_get_unit(self): + """ It should get unit for address importer """ + expect = mock.MagicMock() + with mock.patch.object(self.unit, 'unit_for'): + self.unit._after_import(expect) + self.unit.unit_for.assert_called_once_with( + medical_patient.CarepointAddressPatientUnit, + model='carepoint.carepoint.address.patient', + ) + + def test_after_import_get_unit(self): + """ It should get unit for address importer """ + expect = mock.MagicMock() + with mock.patch.object(self.unit, 'unit_for'): + self.unit._after_import(expect) + self.unit.unit_for()._import_addresses.assert_called_once_with( + self.unit.carepoint_id, + expect, + ) + + +class TestMedicalPatientExportMapper(MedicalPatientTestBase): + + def setUp(self): + super(TestMedicalPatientExportMapper, self).setUp() + self.Unit = medical_patient.MedicalPatientExportMapper + self.unit = self.Unit(self.mock_env) + self.record = mock.MagicMock() + + def test_gender_cd(self): + """ It should return proper export vals dict """ + self.assertDictEqual( + {'gender_cd': self.record.gender.upper()}, + self.unit.gender_cd(self.record), + ) + + def test_static_defaults(self): + """ It should return a dict of default values """ + self.assertIsInstance( + self.unit.static_defaults(self.record), + dict, + ) + + def test_no_safety_caps_yn(self): + """ It should return negated safety_caps """ + self.assertFalse( + self.unit.no_safety_caps_yn(self.record)[ + 'no_safety_caps_yn' + ], + )