From f604dffbbd3a973ad6d42ede4fe040ef730fddf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 10:30:50 +0100 Subject: [PATCH 001/120] create_atc_case executable --- scripts/tomatic_uploadcases.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/tomatic_uploadcases.py diff --git a/scripts/tomatic_uploadcases.py b/scripts/tomatic_uploadcases.py old mode 100644 new mode 100755 From 1897c8646c64ec8b0c9c987e1aee20df1f421081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 12:30:10 +0100 Subject: [PATCH 002/120] test_createCrmCase --- TODO.md | 20 ++++++++ tomatic/claims.py | 30 ++++++++++++ tomatic/claims_test.py | 102 ++++++++++++++++++++++++++--------------- 3 files changed, 115 insertions(+), 37 deletions(-) diff --git a/TODO.md b/TODO.md index a9ccfb523..f3c4c82e9 100644 --- a/TODO.md +++ b/TODO.md @@ -57,3 +57,23 @@ - [ ] conisider joining getClaimTypes and getInfos +- [ ] moure scripts a una carpeta +- [ ] Antotacions UI: Radio button no resolt + tenia rao > no tenia rao +- [ ] Anotacions: Unificar Api anotacio en un punt d'entrada +- [ ] Anotacions: Unificar els fitxers d'anotacio i log de trucada +- [ ] Importar categories que falten de atc com a categorias de crmcases +- [ ] create crm: Inserir usuari correcte al CRM +- [ ] create crm: cas sense contracte +- [ ] create crm: cas sense partner +- [ ] create crm: renombrar user -> seccion/team (preguntar a AiS per terminologia de domini) +- [ ] create crm: extract seccio del reason and remove the field + + + + + + + + + + diff --git a/tomatic/claims.py b/tomatic/claims.py index 6f941c9a4..b71cc9356 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -101,6 +101,36 @@ def get_claims(self): return claims + def create_crm_case(self, case): + '' + partner_id = partnerId(self.erp, case.partner) + partner_address = partnerAddress(self.erp, partner_id) + crm_section_id = crmSectionID(self.erp, case.user) + claim_section_id = claimSectionID( + self.erp, case.reason.split('.')[-1].strip() + ) + + data_crm = { + 'section_id': crm_section_id, + 'name': sectionName(self.erp, claim_section_id), + 'canal_id': PHONE, + 'polissa_id': contractId(self.erp, case.contract), + 'partner_id': partner_id, + 'partner_address_id': partner_address.get('id'), + 'state': 'open', # TODO: 'done' if case.solved else 'open', + 'user_id': '', + } + crm_id = self.erp.CrmCase.create(data_crm).id + + data_history = { + 'case_id': crm_id, + 'description': case.observations, + } + crm_history_id = self.erp.CrmCaseHistory.create(data_history).id + + return crm_id + + def create_atc_case(self, case): ''' Expected case: diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index 24d320f9b..411300ef4 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -27,7 +27,7 @@ def setUp(self): return self.erp = ClientWST(**dbconfig.erppeek) self.erp.begin() - self.data_atc = dbconfig.data_atc + self.maxDiff = None def tearDown(self): try: @@ -37,6 +37,8 @@ def tearDown(self): if 'transaction block' not in e.faultCode: raise + from yamlns.testutils import assertNsEqual + def test_getAllClaims(self): claims = Claims(self.erp) reclamacions = claims.get_claims() @@ -44,44 +46,70 @@ def test_getAllClaims(self): nombre_reclamacions = Reclamacio.count() self.assertEqual(len(reclamacions), nombre_reclamacions) - def test_createAtcCase_basicCase(self): - file_name = "testdata/atc_basicCase.yaml" - if not os.path.isfile(file_name): - error("The file {} does not exists", file_name) - else: - atc_cases = ns.load(file_name) - for person in atc_cases: - for case in atc_cases[person]: - claims = Claims(self.erp) - case_id = claims.create_atc_case(case) - last_atc_case_id = self.erp.GiscedataAtc.search()[0] - self.assertEqual(case_id, last_atc_case_id) + def test_createAtcCase(self): + case = ns.loads("""\ + date: '2021-11-11T15:13:39.998Z' + person: gabriel + reason: '[RECLAMACIONS] 003. INCIDENCIA EN EQUIPOS DE MEDIDA' + partner: S001975 + contract: '0013117' + procedente: '' + improcedente: x + solved: x + user: RECLAMACIONS + cups: ES0031405524910014WM0F + observations: adfasd + """) + + claims = Claims(self.erp) + case_id = claims.create_atc_case(case) + last_case_id = self.erp.GiscedataAtc.search()[0] + self.assertEqual(case_id, last_case_id) + + def test_createCrmCase(self): + case = ns.loads("""\ + date: '2021-11-11T15:13:39.998Z' + phone: '' + person: gabriel + reason: '[RECLAMACIONS] 003. INCIDENCIA EN EQUIPOS DE MEDIDA' + partner: S001975 + contract: '0013117' + user: RECLAMACIONS + observations: adfasd + """) + claims = Claims(self.erp) + case_id = claims.create_crm_case(case) + self.assertTrue(case_id) + crmcase = ns(self.erp.CrmCase.read(case_id, [ + 'section_id', + 'name', + 'canal_id', + 'polissa_id', + 'partner_id', + 'partner_address_id', + 'state', + 'user_id', + ])) + def anonymize(text): return "..."+text[-3:] - def test_createAtcCase_multipleCases(self): - file_name = "testdata/atc_multipleCases.yaml" - if not os.path.isfile(file_name): - error("The file {} does not exists", file_name) - else: - atc_cases = ns.load(file_name) - for person in atc_cases: - for case in atc_cases[person]: - claims = Claims(self.erp) - case_id = claims.create_atc_case(case) - last_atc_case_id = self.erp.GiscedataAtc.search()[0] - self.assertEqual(case_id, last_atc_case_id) + crmcase.section_id = crmcase.section_id[1] + crmcase.canal_id = crmcase.canal_id[1] + crmcase.partner_id = anonymize(crmcase.partner_id[1]) + crmcase.partner_address_id = anonymize(crmcase.partner_address_id[1]) + crmcase.polissa_id = crmcase.polissa_id[1] + #crmcase.user_id = crmcase.user_id[1] + self.assertNsEqual(ns(crmcase), """\ + canal_id: Teléfono + id: {} + name: INCIDENCIA EN EQUIPOS DE MEDIDA + partner_address_id: ...spí + partner_id: ...osé + polissa_id: '0013117' + section_id: Atenció al Client / RECLAMACIONS + state: open + user_id: false + """.format(case_id)) - def test_createAtcCase_multiplePersons(self): - file_name = "testdata/atc_multiplePersons.yaml" - if not os.path.isfile(file_name): - error("The file {} does not exists", file_name) - else: - atc_cases = ns.load(file_name) - for person in atc_cases: - for case in atc_cases[person]: - claims = Claims(self.erp) - case_id = claims.create_atc_case(case) - last_atc_case_id = self.erp.GiscedataAtc.search()[0] - self.assertEqual(case_id, last_atc_case_id) # vim: et ts=4 sw=4 From 62b837f678ae07a478993f15202fb63d7dde2209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 12:33:12 +0100 Subject: [PATCH 003/120] extracted assertCrmCase --- tomatic/claims_test.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index 411300ef4..b0958c955 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -80,6 +80,19 @@ def test_createCrmCase(self): claims = Claims(self.erp) case_id = claims.create_crm_case(case) self.assertTrue(case_id) + self.assertCrmCase(case_id, """\ + canal_id: Teléfono + id: {} + name: INCIDENCIA EN EQUIPOS DE MEDIDA + partner_address_id: ...spí + partner_id: ...osé + polissa_id: '0013117' + section_id: Atenció al Client / RECLAMACIONS + state: open + user_id: false + """.format(case_id)) + + def assertCrmCase(self, case_id, expected): crmcase = ns(self.erp.CrmCase.read(case_id, [ 'section_id', 'name', @@ -98,17 +111,8 @@ def anonymize(text): return "..."+text[-3:] crmcase.partner_address_id = anonymize(crmcase.partner_address_id[1]) crmcase.polissa_id = crmcase.polissa_id[1] #crmcase.user_id = crmcase.user_id[1] - self.assertNsEqual(ns(crmcase), """\ - canal_id: Teléfono - id: {} - name: INCIDENCIA EN EQUIPOS DE MEDIDA - partner_address_id: ...spí - partner_id: ...osé - polissa_id: '0013117' - section_id: Atenció al Client / RECLAMACIONS - state: open - user_id: false - """.format(case_id)) + self.assertNsEqual(ns(crmcase), expected) + From 306cc961cce8570d76ce98344bc8d576aca87cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 12:47:57 +0100 Subject: [PATCH 004/120] test_createCrmCase_noContract --- TODO.md | 4 ++- tomatic/claims.py | 1 + tomatic/claims_test.py | 78 ++++++++++++++++++++++++++++++------------ 3 files changed, 61 insertions(+), 22 deletions(-) diff --git a/TODO.md b/TODO.md index f3c4c82e9..4ee536ff1 100644 --- a/TODO.md +++ b/TODO.md @@ -63,10 +63,12 @@ - [ ] Anotacions: Unificar els fitxers d'anotacio i log de trucada - [ ] Importar categories que falten de atc com a categorias de crmcases - [ ] create crm: Inserir usuari correcte al CRM -- [ ] create crm: cas sense contracte +- [x] create crm: cas amb tot +- [x] create crm: cas sense contracte - [ ] create crm: cas sense partner - [ ] create crm: renombrar user -> seccion/team (preguntar a AiS per terminologia de domini) - [ ] create crm: extract seccio del reason and remove the field +- [ ] create crm: cas contracte no existeix diff --git a/tomatic/claims.py b/tomatic/claims.py index b71cc9356..4ec2cb955 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -20,6 +20,7 @@ def partnerAddress(erp, partner_id): def contractId(erp, contract): + if not contract: return None contract_model = erp.GiscedataPolissa return contract_model.browse([("name", "=", contract)])[0].id diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index b0958c955..deab36754 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -39,6 +39,38 @@ def tearDown(self): from yamlns.testutils import assertNsEqual + def assertCrmCase(self, case_id, expected): + crmcase = ns(self.erp.CrmCase.read(case_id, [ + 'section_id', + 'name', + 'canal_id', + 'polissa_id', + 'partner_id', + 'partner_address_id', + 'state', + 'user_id', + ])) + def anonymize(text): + if not text: return text + return "..."+text[-3:] + + for attrib in [ + "section_id", + "canal_id", + "partner_id", + "partner_address_id", + "polissa_id", + "user_id", + ]: + if crmcase[attrib]: + crmcase[attrib] = crmcase[attrib][1] + + crmcase.partner_id = anonymize(crmcase.partner_id) + crmcase.partner_address_id = anonymize(crmcase.partner_address_id) + #crmcase.user_id = crmcase.user_id[1] + self.assertNsEqual(ns(crmcase), expected) + + def test_getAllClaims(self): claims = Claims(self.erp) reclamacions = claims.get_claims() @@ -92,27 +124,31 @@ def test_createCrmCase(self): user_id: false """.format(case_id)) - def assertCrmCase(self, case_id, expected): - crmcase = ns(self.erp.CrmCase.read(case_id, [ - 'section_id', - 'name', - 'canal_id', - 'polissa_id', - 'partner_id', - 'partner_address_id', - 'state', - 'user_id', - ])) - def anonymize(text): return "..."+text[-3:] - - crmcase.section_id = crmcase.section_id[1] - crmcase.canal_id = crmcase.canal_id[1] - crmcase.partner_id = anonymize(crmcase.partner_id[1]) - crmcase.partner_address_id = anonymize(crmcase.partner_address_id[1]) - crmcase.polissa_id = crmcase.polissa_id[1] - #crmcase.user_id = crmcase.user_id[1] - self.assertNsEqual(ns(crmcase), expected) - + def test_createCrmCase_noContract(self): + case = ns.loads("""\ + date: '2021-11-11T15:13:39.998Z' + phone: '' + person: gabriel + reason: '[RECLAMACIONS] 003. INCIDENCIA EN EQUIPOS DE MEDIDA' + partner: S001975 + contract: '' + user: RECLAMACIONS + observations: adfasd + """) + claims = Claims(self.erp) + case_id = claims.create_crm_case(case) + self.assertTrue(case_id) + self.assertCrmCase(case_id, """\ + canal_id: Teléfono + id: {} + name: INCIDENCIA EN EQUIPOS DE MEDIDA + partner_address_id: ...spí + partner_id: ...osé + polissa_id: False + section_id: Atenció al Client / RECLAMACIONS + state: open + user_id: false + """.format(case_id)) From fd804b5b8fe9bb54690777e6ce994f4973eb2c7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 12:57:32 +0100 Subject: [PATCH 005/120] test_createCrmCase_noPartner --- tomatic/claims.py | 12 +++++++----- tomatic/claims_test.py | 26 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index 4ec2cb955..df53f150c 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -6,12 +6,14 @@ RECLAMANTE = '01' -def partnerId(erp, partner): +def partnerId(erp, partner_id): + if not partner_id: return None partner_model = erp.ResPartner - return partner_model.browse([('ref', '=', partner)])[0].id + return partner_model.browse([('ref', '=', partner_id)])[0].id def partnerAddress(erp, partner_id): + if not partner_id: return None partner_address_model = erp.ResPartnerAddress return partner_address_model.read( [('partner_id', '=', partner_id)], @@ -117,7 +119,7 @@ def create_crm_case(self, case): 'canal_id': PHONE, 'polissa_id': contractId(self.erp, case.contract), 'partner_id': partner_id, - 'partner_address_id': partner_address.get('id'), + 'partner_address_id': partner_address.get('id') if partner_address else False, 'state': 'open', # TODO: 'done' if case.solved else 'open', 'user_id': '', } @@ -179,7 +181,7 @@ def create_atc_case(self, case): crm_history_id = self.erp.CrmCaseHistory.create(data_history).id data_atc = { - 'provincia': partner_address.get('state_id')[0], + 'provincia': partner_address.get('state_id')[0] if partner_address else False, 'total_cups': 1, 'cups_id': cupsId(self.erp, case.cups), 'subtipus_id': claim_section_id, @@ -190,7 +192,7 @@ def create_atc_case(self, case): case.improcedente ) if case.solved else "", 'date': case.date, - 'email_from': partner_address.get('email'), + 'email_from': partner_address.get('email') if partner_address else False, 'time_tracking_id': COMERCIALIZADORA } # user_id = userId(self.erp, self.emails, case.person) diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index deab36754..15a52be1c 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -150,6 +150,32 @@ def test_createCrmCase_noContract(self): user_id: false """.format(case_id)) + def test_createCrmCase_noPartner(self): + case = ns.loads("""\ + date: '2021-11-11T15:13:39.998Z' + phone: '' + person: gabriel + reason: '[RECLAMACIONS] 003. INCIDENCIA EN EQUIPOS DE MEDIDA' + partner: '' + contract: '' + user: RECLAMACIONS + observations: adfasd + """) + claims = Claims(self.erp) + case_id = claims.create_crm_case(case) + self.assertTrue(case_id) + self.assertCrmCase(case_id, """\ + canal_id: Teléfono + id: {} + name: INCIDENCIA EN EQUIPOS DE MEDIDA + partner_address_id: False + partner_id: False + polissa_id: False + section_id: Atenció al Client / RECLAMACIONS + state: open + user_id: false + """.format(case_id)) + # vim: et ts=4 sw=4 From 856a8ad805111a9e6d40e312a305d70cf93ee43a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 13:02:24 +0100 Subject: [PATCH 006/120] create_atc_case uses create_crm_case --- TODO.md | 3 ++- tomatic/claims.py | 19 +------------------ 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/TODO.md b/TODO.md index 4ee536ff1..dc92b029e 100644 --- a/TODO.md +++ b/TODO.md @@ -65,7 +65,8 @@ - [ ] create crm: Inserir usuari correcte al CRM - [x] create crm: cas amb tot - [x] create crm: cas sense contracte -- [ ] create crm: cas sense partner +- [x] create crm: cas sense partner +- [ ] create atc uses create crm - [ ] create crm: renombrar user -> seccion/team (preguntar a AiS per terminologia de domini) - [ ] create crm: extract seccio del reason and remove the field - [ ] create crm: cas contracte no existeix diff --git a/tomatic/claims.py b/tomatic/claims.py index df53f150c..805c115a3 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -157,28 +157,11 @@ def create_atc_case(self, case): ''' partner_id = partnerId(self.erp, case.partner) partner_address = partnerAddress(self.erp, partner_id) - crm_section_id = crmSectionID(self.erp, case.user) claim_section_id = claimSectionID( self.erp, case.reason.split('.')[-1].strip() ) - data_crm = { - 'section_id': crm_section_id, - 'name': sectionName(self.erp, claim_section_id), - 'canal_id': PHONE, - 'polissa_id': contractId(self.erp, case.contract), - 'partner_id': partner_id, - 'partner_address_id': partner_address.get('id'), - 'state': 'done' if case.solved else 'open', - 'user_id': '' - } - crm_id = self.erp.CrmCase.create(data_crm).id - - data_history = { - 'case_id': crm_id, - 'description': case.observations - } - crm_history_id = self.erp.CrmCaseHistory.create(data_history).id + crm_id = self.create_crm_case(case) data_atc = { 'provincia': partner_address.get('state_id')[0] if partner_address else False, From 117bc47b361887df246924ead7aa8addf748d6e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 13:55:25 +0100 Subject: [PATCH 007/120] assertAtcCase and common code assertCrmCase --- TODO.md | 3 +- tomatic/claims_test.py | 75 +++++++++++++++++++++++++++++------------- 2 files changed, 54 insertions(+), 24 deletions(-) diff --git a/TODO.md b/TODO.md index dc92b029e..fe97073e3 100644 --- a/TODO.md +++ b/TODO.md @@ -63,10 +63,11 @@ - [ ] Anotacions: Unificar els fitxers d'anotacio i log de trucada - [ ] Importar categories que falten de atc com a categorias de crmcases - [ ] create crm: Inserir usuari correcte al CRM +- [ ] anotate_case: sensitive to the case fields creates atc or not - [x] create crm: cas amb tot - [x] create crm: cas sense contracte - [x] create crm: cas sense partner -- [ ] create atc uses create crm +- [x] create atc uses create crm - [ ] create crm: renombrar user -> seccion/team (preguntar a AiS per terminologia de domini) - [ ] create crm: extract seccio del reason and remove the field - [ ] create crm: cas contracte no existeix diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index 15a52be1c..af96b1e2c 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -40,7 +40,11 @@ def tearDown(self): from yamlns.testutils import assertNsEqual def assertCrmCase(self, case_id, expected): - crmcase = ns(self.erp.CrmCase.read(case_id, [ + if not expected: + self.assertFalse(case_id) + return + self.assertTrue(case_id) + result = ns(self.erp.CrmCase.read(case_id, [ 'section_id', 'name', 'canal_id', @@ -50,25 +54,53 @@ def assertCrmCase(self, case_id, expected): 'state', 'user_id', ])) - def anonymize(text): - if not text: return text - return "..."+text[-3:] - - for attrib in [ - "section_id", - "canal_id", - "partner_id", - "partner_address_id", - "polissa_id", - "user_id", - ]: - if crmcase[attrib]: - crmcase[attrib] = crmcase[attrib][1] - - crmcase.partner_id = anonymize(crmcase.partner_id) - crmcase.partner_address_id = anonymize(crmcase.partner_address_id) - #crmcase.user_id = crmcase.user_id[1] - self.assertNsEqual(ns(crmcase), expected) + + def anonymize(attrib): + if not result[attrib]: return + result[attrib] = '...'+result[attrib][-3:] + + def fkname(attrib): + if not result[attrib]: return + result[attrib] = result[attrib][1] + + fkname("section_id") + fkname("canal_id") + fkname("partner_id") + anonymize('partner_id') + fkname("partner_address_id") + anonymize('partner_address_id') + fkname("polissa_id") + fkname("user_id") + + self.assertNsEqual(ns(result), expected) + + + def assertAtcCase(self, case_id, expected): + if not expected: + self.assertFalse(case_id) + return + self.assertTrue(case_id) + result = ns(self.erp.GiscedataAtc.read(case_id, [ + 'provincia', + 'total_cups', + 'cups_id', + 'subtipus_id', + 'reclamante', + 'resultat', + 'date', + 'email_from', + 'time_tracking_id', + ])) + + def fkname(attrib): + if not result[attrib]: return + result[attrib] = result[attrib][1] + + fkname("cups_id") + fkname("subtipus_id") + fkname("time_tracking_id") + + self.assertNsEqual(ns(result), expected) def test_getAllClaims(self): @@ -111,7 +143,6 @@ def test_createCrmCase(self): """) claims = Claims(self.erp) case_id = claims.create_crm_case(case) - self.assertTrue(case_id) self.assertCrmCase(case_id, """\ canal_id: Teléfono id: {} @@ -137,7 +168,6 @@ def test_createCrmCase_noContract(self): """) claims = Claims(self.erp) case_id = claims.create_crm_case(case) - self.assertTrue(case_id) self.assertCrmCase(case_id, """\ canal_id: Teléfono id: {} @@ -163,7 +193,6 @@ def test_createCrmCase_noPartner(self): """) claims = Claims(self.erp) case_id = claims.create_crm_case(case) - self.assertTrue(case_id) self.assertCrmCase(case_id, """\ canal_id: Teléfono id: {} From a28b2f70ec8ac8810002819336fab26e1e615d64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 14:05:56 +0100 Subject: [PATCH 008/120] extracted fkname and annonymize as global --- TODO.md | 1 + tomatic/claims_test.py | 56 +++++++++++++++++++++++++----------------- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/TODO.md b/TODO.md index fe97073e3..05c12e58e 100644 --- a/TODO.md +++ b/TODO.md @@ -68,6 +68,7 @@ - [x] create crm: cas sense contracte - [x] create crm: cas sense partner - [x] create atc uses create crm +- [ ] create atc: cover test cases - [ ] create crm: renombrar user -> seccion/team (preguntar a AiS per terminologia de domini) - [ ] create crm: extract seccio del reason and remove the field - [ ] create crm: cas contracte no existeix diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index af96b1e2c..7e0c3f3f4 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -12,6 +12,14 @@ except ImportError: dbconfig = None +def fkname(result, attrib): + if not result[attrib]: return + result[attrib] = result[attrib][1] + +def anonymize(result, attrib): + if not result[attrib]: return + result[attrib] = '...'+result[attrib][-3:] + @unittest.skipIf(os.environ.get("TRAVIS"), "Database not available in Travis") @unittest.skipIf( @@ -55,22 +63,14 @@ def assertCrmCase(self, case_id, expected): 'user_id', ])) - def anonymize(attrib): - if not result[attrib]: return - result[attrib] = '...'+result[attrib][-3:] - - def fkname(attrib): - if not result[attrib]: return - result[attrib] = result[attrib][1] - - fkname("section_id") - fkname("canal_id") - fkname("partner_id") - anonymize('partner_id') - fkname("partner_address_id") - anonymize('partner_address_id') - fkname("polissa_id") - fkname("user_id") + fkname(result, "section_id") + fkname(result, "canal_id") + fkname(result, "partner_id") + fkname(result, "partner_address_id") + fkname(result, "polissa_id") + fkname(result, "user_id") + anonymize(result, 'partner_id') + anonymize(result, 'partner_address_id') self.assertNsEqual(ns(result), expected) @@ -92,13 +92,12 @@ def assertAtcCase(self, case_id, expected): 'time_tracking_id', ])) - def fkname(attrib): - if not result[attrib]: return - result[attrib] = result[attrib][1] - fkname("cups_id") - fkname("subtipus_id") - fkname("time_tracking_id") + fkname(result, "cups_id") + fkname(result, "subtipus_id") + fkname(result, "time_tracking_id") + fkname(result, "provincia") + anonymize(result, "email_from") self.assertNsEqual(ns(result), expected) @@ -128,7 +127,18 @@ def test_createAtcCase(self): claims = Claims(self.erp) case_id = claims.create_atc_case(case) last_case_id = self.erp.GiscedataAtc.search()[0] - self.assertEqual(case_id, last_case_id) + self.assertAtcCase(case_id, """ + cups_id: ES0031405524910014WM0F + date: '2021-11-11 15:13:39.998' + email_from: ...oop + id: {} + provincia: Barcelona + reclamante: '01' + resultat: '02' + subtipus_id: '003' + time_tracking_id: Comercialitzadora + total_cups: 1 + """.format(case_id)) def test_createCrmCase(self): case = ns.loads("""\ From 976c3892149aa74b0d5385b3020fe4de4ead876e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 14:18:29 +0100 Subject: [PATCH 009/120] reboundary of resultat --- tomatic/claims.py | 18 ++++++------------ tomatic/claims_test.py | 3 --- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index 805c115a3..aa567005b 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -49,13 +49,11 @@ def userId(erp, emails, person): except IndexError: return None - -def resultat(erp, procedente, improcedente): - if procedente: - return '01' - if improcedente: - return '02' - return '03' +def resultat(case): + if not case.solved: return '' + if case.procedente: return '01' + if case.improcedente: return '02' + return '03' # cannot be solved def sectionName(erp, section_id): @@ -169,11 +167,7 @@ def create_atc_case(self, case): 'cups_id': cupsId(self.erp, case.cups), 'subtipus_id': claim_section_id, 'reclamante': RECLAMANTE, - 'resultat': resultat( - self.erp, - case.procedente, - case.improcedente - ) if case.solved else "", + 'resultat': resultat(case), 'date': case.date, 'email_from': partner_address.get('email') if partner_address else False, 'time_tracking_id': COMERCIALIZADORA diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index 7e0c3f3f4..b49d4e62e 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -92,7 +92,6 @@ def assertAtcCase(self, case_id, expected): 'time_tracking_id', ])) - fkname(result, "cups_id") fkname(result, "subtipus_id") fkname(result, "time_tracking_id") @@ -101,7 +100,6 @@ def assertAtcCase(self, case_id, expected): self.assertNsEqual(ns(result), expected) - def test_getAllClaims(self): claims = Claims(self.erp) reclamacions = claims.get_claims() @@ -216,5 +214,4 @@ def test_createCrmCase_noPartner(self): """.format(case_id)) - # vim: et ts=4 sw=4 From ac9c9ef5374a36333380ca09fa8d6eb50cd04d08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 14:34:04 +0100 Subject: [PATCH 010/120] claims_test: using base cases and variations idiom --- TODO.md | 1 + tomatic/claims_test.py | 106 ++++++++++++++++++++++++++++------------- 2 files changed, 74 insertions(+), 33 deletions(-) diff --git a/TODO.md b/TODO.md index 05c12e58e..333934929 100644 --- a/TODO.md +++ b/TODO.md @@ -68,6 +68,7 @@ - [x] create crm: cas sense contracte - [x] create crm: cas sense partner - [x] create atc uses create crm +- [ ] create crm: solved = True - [ ] create atc: cover test cases - [ ] create crm: renombrar user -> seccion/team (preguntar a AiS per terminologia de domini) - [ ] create crm: extract seccio del reason and remove the field diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index b49d4e62e..15ba3cf88 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -96,6 +96,7 @@ def assertAtcCase(self, case_id, expected): fkname(result, "subtipus_id") fkname(result, "time_tracking_id") fkname(result, "provincia") + anonymize(result, "cups_id") anonymize(result, "email_from") self.assertNsEqual(ns(result), expected) @@ -107,39 +108,88 @@ def test_getAllClaims(self): nombre_reclamacions = Reclamacio.count() self.assertEqual(len(reclamacions), nombre_reclamacions) - def test_createAtcCase(self): - case = ns.loads("""\ + def atc_base(self, **kwds): + return ns.loads(""" date: '2021-11-11T15:13:39.998Z' person: gabriel reason: '[RECLAMACIONS] 003. INCIDENCIA EN EQUIPOS DE MEDIDA' partner: S001975 contract: '0013117' - procedente: '' - improcedente: x + procedente: x + improcedente: '' solved: x user: RECLAMACIONS cups: ES0031405524910014WM0F observations: adfasd - """) + """).update(**kwds) + + def test_createAtcCase(self): + case = self.atc_base() claims = Claims(self.erp) case_id = claims.create_atc_case(case) last_case_id = self.erp.GiscedataAtc.search()[0] self.assertAtcCase(case_id, """ - cups_id: ES0031405524910014WM0F + cups_id: ...M0F date: '2021-11-11 15:13:39.998' email_from: ...oop id: {} provincia: Barcelona reclamante: '01' - resultat: '02' + resultat: '01' subtipus_id: '003' time_tracking_id: Comercialitzadora total_cups: 1 """.format(case_id)) - def test_createCrmCase(self): - case = ns.loads("""\ + def test_createAtcCase_improcedente(self): + case = self.atc_base( + procedente='', + improcedente='x', + solved='x', + ) + + claims = Claims(self.erp) + case_id = claims.create_atc_case(case) + last_case_id = self.erp.GiscedataAtc.search()[0] + self.assertAtcCase(case_id, """ + cups_id: ...M0F + date: '2021-11-11 15:13:39.998' + email_from: ...oop + id: {} + provincia: Barcelona + reclamante: '01' + resultat: '02' # THIS CHANGES + subtipus_id: '003' + time_tracking_id: Comercialitzadora + total_cups: 1 + """.format(case_id)) + + def test_createAtcCase_unsolved(self): + case = self.atc_base( + procedente='', + improcedente='', + solved='', + ) + + claims = Claims(self.erp) + case_id = claims.create_atc_case(case) + last_case_id = self.erp.GiscedataAtc.search()[0] + self.assertAtcCase(case_id, """ + cups_id: ...M0F + date: '2021-11-11 15:13:39.998' + email_from: ...oop + id: {} + provincia: Barcelona + reclamante: '01' + resultat: '03' # THIS CHANGES + subtipus_id: '003' + time_tracking_id: Comercialitzadora + total_cups: 1 + """.format(case_id)) + + def crm_base(self, **kwds): + return ns.loads("""\ date: '2021-11-11T15:13:39.998Z' phone: '' person: gabriel @@ -148,7 +198,10 @@ def test_createCrmCase(self): contract: '0013117' user: RECLAMACIONS observations: adfasd - """) + """).update(**kwds) + + def test_createCrmCase(self): + case = self.crm_base() claims = Claims(self.erp) case_id = claims.create_crm_case(case) self.assertCrmCase(case_id, """\ @@ -164,16 +217,9 @@ def test_createCrmCase(self): """.format(case_id)) def test_createCrmCase_noContract(self): - case = ns.loads("""\ - date: '2021-11-11T15:13:39.998Z' - phone: '' - person: gabriel - reason: '[RECLAMACIONS] 003. INCIDENCIA EN EQUIPOS DE MEDIDA' - partner: S001975 - contract: '' - user: RECLAMACIONS - observations: adfasd - """) + case = self.crm_base( + contract = '', + ) claims = Claims(self.erp) case_id = claims.create_crm_case(case) self.assertCrmCase(case_id, """\ @@ -182,23 +228,17 @@ def test_createCrmCase_noContract(self): name: INCIDENCIA EN EQUIPOS DE MEDIDA partner_address_id: ...spí partner_id: ...osé - polissa_id: False + polissa_id: False # THIS CHANGES section_id: Atenció al Client / RECLAMACIONS state: open user_id: false """.format(case_id)) def test_createCrmCase_noPartner(self): - case = ns.loads("""\ - date: '2021-11-11T15:13:39.998Z' - phone: '' - person: gabriel - reason: '[RECLAMACIONS] 003. INCIDENCIA EN EQUIPOS DE MEDIDA' - partner: '' - contract: '' - user: RECLAMACIONS - observations: adfasd - """) + case = self.crm_base( + contract = '', + partner = '', + ) claims = Claims(self.erp) case_id = claims.create_crm_case(case) self.assertCrmCase(case_id, """\ @@ -206,8 +246,8 @@ def test_createCrmCase_noPartner(self): id: {} name: INCIDENCIA EN EQUIPOS DE MEDIDA partner_address_id: False - partner_id: False - polissa_id: False + partner_id: False # THIS CHANGES + polissa_id: False # THIS CHANGES section_id: Atenció al Client / RECLAMACIONS state: open user_id: false From 7531f28787d8ad3cb949d951b516afa870b621b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 14:50:17 +0100 Subject: [PATCH 011/120] testing crmcase resolution --- TODO.md | 4 ++-- tomatic/claims_test.py | 47 +++++++++++++++++++++++++++++++++--------- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/TODO.md b/TODO.md index 333934929..3cf661f31 100644 --- a/TODO.md +++ b/TODO.md @@ -68,8 +68,8 @@ - [x] create crm: cas sense contracte - [x] create crm: cas sense partner - [x] create atc uses create crm -- [ ] create crm: solved = True -- [ ] create atc: cover test cases +- [ ] create crm: solved = True (lo comentamos cuando lo movimos a una funcion a parte) +- [x] create atc: cover test cases - [ ] create crm: renombrar user -> seccion/team (preguntar a AiS per terminologia de domini) - [ ] create crm: extract seccio del reason and remove the field - [ ] create crm: cas contracte no existeix diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index 15ba3cf88..94d90fea7 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -109,7 +109,7 @@ def test_getAllClaims(self): self.assertEqual(len(reclamacions), nombre_reclamacions) def atc_base(self, **kwds): - return ns.loads(""" + base = ns.loads(""" date: '2021-11-11T15:13:39.998Z' person: gabriel reason: '[RECLAMACIONS] 003. INCIDENCIA EN EQUIPOS DE MEDIDA' @@ -121,9 +121,11 @@ def atc_base(self, **kwds): user: RECLAMACIONS cups: ES0031405524910014WM0F observations: adfasd - """).update(**kwds) + """) + base.update(**kwds) + return base - def test_createAtcCase(self): + def test_createAtcCase_procedente(self): case = self.atc_base() claims = Claims(self.erp) @@ -159,7 +161,30 @@ def test_createAtcCase_improcedente(self): id: {} provincia: Barcelona reclamante: '01' - resultat: '02' # THIS CHANGES + resultat: '02' # <--------- THIS CHANGES + subtipus_id: '003' + time_tracking_id: Comercialitzadora + total_cups: 1 + """.format(case_id)) + + def test_createAtcCase_noSolution(self): + case = self.atc_base( + procedente='', + improcedente='', + solved='x', + ) + + claims = Claims(self.erp) + case_id = claims.create_atc_case(case) + last_case_id = self.erp.GiscedataAtc.search()[0] + self.assertAtcCase(case_id, """ + cups_id: ...M0F + date: '2021-11-11 15:13:39.998' + email_from: ...oop + id: {} + provincia: Barcelona + reclamante: '01' + resultat: '03' # <--------- THIS CHANGES subtipus_id: '003' time_tracking_id: Comercialitzadora total_cups: 1 @@ -182,14 +207,14 @@ def test_createAtcCase_unsolved(self): id: {} provincia: Barcelona reclamante: '01' - resultat: '03' # THIS CHANGES + resultat: '' # <--------- THIS CHANGES subtipus_id: '003' time_tracking_id: Comercialitzadora total_cups: 1 """.format(case_id)) def crm_base(self, **kwds): - return ns.loads("""\ + base = ns.loads("""\ date: '2021-11-11T15:13:39.998Z' phone: '' person: gabriel @@ -198,7 +223,9 @@ def crm_base(self, **kwds): contract: '0013117' user: RECLAMACIONS observations: adfasd - """).update(**kwds) + """) + base.update(**kwds) + return base def test_createCrmCase(self): case = self.crm_base() @@ -228,7 +255,7 @@ def test_createCrmCase_noContract(self): name: INCIDENCIA EN EQUIPOS DE MEDIDA partner_address_id: ...spí partner_id: ...osé - polissa_id: False # THIS CHANGES + polissa_id: False # <--------- THIS CHANGES section_id: Atenció al Client / RECLAMACIONS state: open user_id: false @@ -246,8 +273,8 @@ def test_createCrmCase_noPartner(self): id: {} name: INCIDENCIA EN EQUIPOS DE MEDIDA partner_address_id: False - partner_id: False # THIS CHANGES - polissa_id: False # THIS CHANGES + partner_id: False # <--------- THIS CHANGES + polissa_id: False # <--------- THIS CHANGES section_id: Atenció al Client / RECLAMACIONS state: open user_id: false From 51dc901019026546e23bc4c450413aa18069cd8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 15:07:25 +0100 Subject: [PATCH 012/120] replicated test from kalinfo.crmcases_test in claims --- TODO.md | 5 +++++ tomatic/claims_test.py | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/TODO.md b/TODO.md index 3cf661f31..34f43cd30 100644 --- a/TODO.md +++ b/TODO.md @@ -58,6 +58,7 @@ - [ ] moure scripts a una carpeta + - [ ] Antotacions UI: Radio button no resolt + tenia rao > no tenia rao - [ ] Anotacions: Unificar Api anotacio en un punt d'entrada - [ ] Anotacions: Unificar els fitxers d'anotacio i log de trucada @@ -70,9 +71,13 @@ - [x] create atc uses create crm - [ ] create crm: solved = True (lo comentamos cuando lo movimos a una funcion a parte) - [x] create atc: cover test cases +- [ ] empty kalinfo.crmcase and remove - [ ] create crm: renombrar user -> seccion/team (preguntar a AiS per terminologia de domini) - [ ] create crm: extract seccio del reason and remove the field - [ ] create crm: cas contracte no existeix +- [ ] create crm: te sentit loggejar el cups si tenim el contract id? +- [ ] Claims.get_claims -> claimCategories() +- [ ] repurpose claims diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index 94d90fea7..08df5bd02 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -7,6 +7,8 @@ from yamlns import namespace as ns from xmlrpc import client as xmlrpclib from .claims import Claims +from .kalinfo.crmcase import CrmCase + try: import dbconfig except ImportError: @@ -108,6 +110,12 @@ def test_getAllClaims(self): nombre_reclamacions = Reclamacio.count() self.assertEqual(len(reclamacions), nombre_reclamacions) + def test_getCrmCategories(self): + crm_case = CrmCase(self.erp) + crm_categories = crm_case.get_crm_categories() + categories = ns.load('b2bdata/categories_b2b.yaml') + self.assertEqual(crm_categories, categories) + def atc_base(self, **kwds): base = ns.loads(""" date: '2021-11-11T15:13:39.998Z' From b09fd5f4c3613af769a1727cc93671aab2c723f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 15:13:40 +0100 Subject: [PATCH 013/120] safe erp closing --- tomatic/claims_test.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index 08df5bd02..ebec8fdf3 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -31,18 +31,19 @@ def anonymize(result, attrib): class Claims_Test(unittest.TestCase): def setUp(self): + self.maxDiff = None + self.erp = None if not dbconfig: return if not dbconfig.erppeek: return self.erp = ClientWST(**dbconfig.erppeek) self.erp.begin() - self.maxDiff = None def tearDown(self): try: - self.erp.rollback() - self.erp.close() + self.erp and self.erp.rollback() + self.erp and self.erp.close() except xmlrpclib.Fault as e: if 'transaction block' not in e.faultCode: raise From 754865251f695173b63225179e94a44b7cdba771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 15:13:56 +0100 Subject: [PATCH 014/120] base case helpers joined --- tomatic/claims_test.py | 28 ++++++++++++++-------------- tomatic/kalinfo/crmcase_test.py | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index ebec8fdf3..93f2a2dea 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -134,6 +134,20 @@ def atc_base(self, **kwds): base.update(**kwds) return base + def crm_base(self, **kwds): + base = ns.loads("""\ + date: '2021-11-11T15:13:39.998Z' + phone: '' + person: gabriel + reason: '[RECLAMACIONS] 003. INCIDENCIA EN EQUIPOS DE MEDIDA' + partner: S001975 + contract: '0013117' + user: RECLAMACIONS + observations: adfasd + """) + base.update(**kwds) + return base + def test_createAtcCase_procedente(self): case = self.atc_base() @@ -222,20 +236,6 @@ def test_createAtcCase_unsolved(self): total_cups: 1 """.format(case_id)) - def crm_base(self, **kwds): - base = ns.loads("""\ - date: '2021-11-11T15:13:39.998Z' - phone: '' - person: gabriel - reason: '[RECLAMACIONS] 003. INCIDENCIA EN EQUIPOS DE MEDIDA' - partner: S001975 - contract: '0013117' - user: RECLAMACIONS - observations: adfasd - """) - base.update(**kwds) - return base - def test_createCrmCase(self): case = self.crm_base() claims = Claims(self.erp) diff --git a/tomatic/kalinfo/crmcase_test.py b/tomatic/kalinfo/crmcase_test.py index df6700b56..a87edcb43 100644 --- a/tomatic/kalinfo/crmcase_test.py +++ b/tomatic/kalinfo/crmcase_test.py @@ -45,4 +45,4 @@ def test_getCrmCategories(self): self.assertEqual(crm_categories, categories) -# vim: et ts=4 sw=4 \ No newline at end of file +# vim: et ts=4 sw=4 From 95e226ccd0782a4216d3a6b5dcc286b26fc47d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 15:29:19 +0100 Subject: [PATCH 015/120] crmcase methods moved to claims TODO: claims is not "claims", look for a better name --- tomatic/callregistry.py | 5 ++-- tomatic/claims.py | 15 +++++++++++ tomatic/claims_test.py | 7 +++-- tomatic/kalinfo/__init__.py | 0 tomatic/kalinfo/crmcase.py | 24 ----------------- tomatic/kalinfo/crmcase_test.py | 48 --------------------------------- 6 files changed, 20 insertions(+), 79 deletions(-) delete mode 100644 tomatic/kalinfo/__init__.py delete mode 100644 tomatic/kalinfo/crmcase.py delete mode 100644 tomatic/kalinfo/crmcase_test.py diff --git a/tomatic/callregistry.py b/tomatic/callregistry.py index 693bda1e6..6077a16cd 100644 --- a/tomatic/callregistry.py +++ b/tomatic/callregistry.py @@ -5,7 +5,6 @@ from yamlns import namespace as ns from consolemsg import error, step, warn, u from .claims import Claims -from .kalinfo.crmcase import CrmCase def fillConfigurationInfo(): return ns.load('config.yaml') @@ -91,8 +90,8 @@ def importClaimTypes(self, erp): ) def importCrmCategories(self, erp): - categories = CrmCase(erp) - erp_crm_categories = categories.get_crm_categories() + claims = Claims(erp) + erp_crm_categories = claims.crm_categories() Path(CONFIG.info_cases).write_text( '\n'.join([u(x) for x in erp_crm_categories]), diff --git a/tomatic/claims.py b/tomatic/claims.py index aa567005b..af595a2e3 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -102,6 +102,21 @@ def get_claims(self): return claims + def crm_categories(self): + crm_categories_model = self.erp.model('crm.case.categ') + all_crm_categories = crm_categories_model.search([]) + + crm_categories = [] + for category_id in all_crm_categories: + category = crm_categories_model.read( + category_id, ['name'] + ) + categ_name = category.get('name') + if categ_name.startswith('['): + crm_categories.append(categ_name) + + return crm_categories + def create_crm_case(self, case): '' partner_id = partnerId(self.erp, case.partner) diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index 93f2a2dea..cbf209369 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -7,7 +7,6 @@ from yamlns import namespace as ns from xmlrpc import client as xmlrpclib from .claims import Claims -from .kalinfo.crmcase import CrmCase try: import dbconfig @@ -111,9 +110,9 @@ def test_getAllClaims(self): nombre_reclamacions = Reclamacio.count() self.assertEqual(len(reclamacions), nombre_reclamacions) - def test_getCrmCategories(self): - crm_case = CrmCase(self.erp) - crm_categories = crm_case.get_crm_categories() + def test_crmCategories(self): + claims = Claims(self.erp) + crm_categories = claims.crm_categories() categories = ns.load('b2bdata/categories_b2b.yaml') self.assertEqual(crm_categories, categories) diff --git a/tomatic/kalinfo/__init__.py b/tomatic/kalinfo/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tomatic/kalinfo/crmcase.py b/tomatic/kalinfo/crmcase.py deleted file mode 100644 index d83aba4ad..000000000 --- a/tomatic/kalinfo/crmcase.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- encoding: utf-8 -*- - -class CrmCase(object): - - def __init__(self, erp): - self.erp = erp - - def get_crm_categories(self): - crm_categories_model = self.erp.model('crm.case.categ') - all_crm_categories = crm_categories_model.search([]) - - crm_categories = [] - for category_id in all_crm_categories: - category = crm_categories_model.read( - category_id, ['name'] - ) - categ_name = category.get('name') - if categ_name.startswith('['): - crm_categories.append(categ_name) - - return crm_categories - - -# vim: et ts=4 sw=4 \ No newline at end of file diff --git a/tomatic/kalinfo/crmcase_test.py b/tomatic/kalinfo/crmcase_test.py deleted file mode 100644 index a87edcb43..000000000 --- a/tomatic/kalinfo/crmcase_test.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- encoding: utf-8 -*- - -import unittest -import os -from unittest.case import skip -from erppeek_wst import ClientWST -from xmlrpc import client as xmlrpclib -from yamlns import namespace as ns -from .crmcase import CrmCase - -try: - import dbconfig -except ImportError: - dbconfig = None - -@unittest.skipIf(os.environ.get("TRAVIS"), - "Database not available in Travis") -@unittest.skipIf( - not dbconfig or not dbconfig.erppeek, - "Requires configuring dbconfig.erppeek" -) -class Claims_Test(unittest.TestCase): - - def setUp(self): - if not dbconfig: - return - if not dbconfig.erppeek: - return - self.erp = ClientWST(**dbconfig.erppeek) - self.erp.begin() - - def tearDown(self): - try: - self.erp.rollback() - self.erp.close() - except xmlrpclib.Fault as e: - if 'transaction block' not in e.faultCode: - raise - - @skip("there are nocategories in testing") - def test_getCrmCategories(self): - crm_case = CrmCase(self.erp) - crm_categories = crm_case.get_crm_categories() - categories = ns.load('b2bdata/categories_b2b.yaml') - self.assertEqual(crm_categories, categories) - - -# vim: et ts=4 sw=4 From e3900504f3b4894ec5504f467622e88a1c29bd94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 15:40:16 +0100 Subject: [PATCH 016/120] using search is easier if you just want the id --- TODO.md | 4 ++-- tomatic/claims.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/TODO.md b/TODO.md index 34f43cd30..9090f3b09 100644 --- a/TODO.md +++ b/TODO.md @@ -71,11 +71,11 @@ - [x] create atc uses create crm - [ ] create crm: solved = True (lo comentamos cuando lo movimos a una funcion a parte) - [x] create atc: cover test cases -- [ ] empty kalinfo.crmcase and remove +- [x] empty kalinfo.crmcase and remove - [ ] create crm: renombrar user -> seccion/team (preguntar a AiS per terminologia de domini) - [ ] create crm: extract seccio del reason and remove the field - [ ] create crm: cas contracte no existeix -- [ ] create crm: te sentit loggejar el cups si tenim el contract id? +- [ ] **create crm: te sentit loggejar el cups si tenim el contract id?** - [ ] Claims.get_claims -> claimCategories() - [ ] repurpose claims diff --git a/tomatic/claims.py b/tomatic/claims.py index af595a2e3..5e0d0174d 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -23,8 +23,8 @@ def partnerAddress(erp, partner_id): def contractId(erp, contract): if not contract: return None - contract_model = erp.GiscedataPolissa - return contract_model.browse([("name", "=", contract)])[0].id + contract_id = erp.GiscedataPolissa.search([("name", "=", contract)]) + if contract_id: return contract_id[0] def cupsId(erp, cups): @@ -173,7 +173,6 @@ def create_atc_case(self, case): claim_section_id = claimSectionID( self.erp, case.reason.split('.')[-1].strip() ) - crm_id = self.create_crm_case(case) data_atc = { From 9f957273eaa84c3cd4766e79fd00ddc90a9a3615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 15:52:14 +0100 Subject: [PATCH 017/120] no need to log cups, retrievable from contract --- tomatic/claims.py | 12 ++++-------- tomatic/claims_test.py | 1 - 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index 5e0d0174d..dfb8d0887 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -26,12 +26,6 @@ def contractId(erp, contract): contract_id = erp.GiscedataPolissa.search([("name", "=", contract)]) if contract_id: return contract_id[0] - -def cupsId(erp, cups): - cups_model = erp.GiscedataCupsPs - return cups_model.browse([('name', '=', cups)])[0].id - - def userId(erp, emails, person): email = emails[person] partner_address_model = erp.ResPartnerAddress @@ -162,7 +156,6 @@ def create_atc_case(self, case): improcedente: '' solved: '' user: section.name - cups: cups number observations: comments - ... ... @@ -174,11 +167,14 @@ def create_atc_case(self, case): self.erp, case.reason.split('.')[-1].strip() ) crm_id = self.create_crm_case(case) + contract_id = contractId(self.erp, case.contract) + contract = self.erp.GiscedataPolissa.read(contract_id, ['cups']) + cups_id = contract['cups'][0] if contract else None data_atc = { 'provincia': partner_address.get('state_id')[0] if partner_address else False, 'total_cups': 1, - 'cups_id': cupsId(self.erp, case.cups), + 'cups_id': cups_id, 'subtipus_id': claim_section_id, 'reclamante': RECLAMANTE, 'resultat': resultat(case), diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index cbf209369..db55fb7b5 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -127,7 +127,6 @@ def atc_base(self, **kwds): improcedente: '' solved: x user: RECLAMACIONS - cups: ES0031405524910014WM0F observations: adfasd """) base.update(**kwds) From e06f2a6c5e5f41dee94ceda89c44d030572c873d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 16:47:06 +0100 Subject: [PATCH 018/120] speed: single erp call to get all claim types --- TODO.md | 9 ++++++--- tomatic/claims.py | 9 ++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/TODO.md b/TODO.md index 9090f3b09..9f3a4c761 100644 --- a/TODO.md +++ b/TODO.md @@ -69,17 +69,20 @@ - [x] create crm: cas sense contracte - [x] create crm: cas sense partner - [x] create atc uses create crm -- [ ] create crm: solved = True (lo comentamos cuando lo movimos a una funcion a parte) +- [ ] !!! create crm: solved = True (lo comentamos cuando lo movimos a una funcion a parte) - [x] create atc: cover test cases - [x] empty kalinfo.crmcase and remove - [ ] create crm: renombrar user -> seccion/team (preguntar a AiS per terminologia de domini) - [ ] create crm: extract seccio del reason and remove the field - [ ] create crm: cas contracte no existeix -- [ ] **create crm: te sentit loggejar el cups si tenim el contract id?** +- [x] create crm: te sentit loggejar el cups si tenim el contract id? - [ ] Claims.get_claims -> claimCategories() - [ ] repurpose claims - +- [ ] callinfo log: remove cups +- [ ] callinfo log: unite resolution +- [ ] callinfo log: join infos and claims +- [ ] Check: sections id's unlikely to refer the same id (table) in atc and crm diff --git a/tomatic/claims.py b/tomatic/claims.py index dfb8d0887..506042bb5 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -76,11 +76,10 @@ def get_claims(self): claims = [] all_claim_ids = claims_model.search() - for claim_id in all_claim_ids: - claim = claims_model.read( - claim_id, - ['default_section', 'name', 'desc'] - ) + for claim in claims_model.read( + all_claim_ids, + ['default_section', 'name', 'desc'] + ): claim_section = claim.get("default_section") section = defaultSection From cc80c6613354ef2f77f8496e1f08e1601158b82f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 17:19:18 +0100 Subject: [PATCH 019/120] atc and crm categories b2b'd --- ...st.Claims_Test.test_atcCategories-expected | 93 +++++++++++++++++++ ...st.Claims_Test.test_crmCategories-expected | 1 + tomatic/claims_test.py | 15 ++- 3 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 b2bdata/tomatic.claims_test.Claims_Test.test_atcCategories-expected create mode 100644 b2bdata/tomatic.claims_test.Claims_Test.test_crmCategories-expected diff --git a/b2bdata/tomatic.claims_test.Claims_Test.test_atcCategories-expected b/b2bdata/tomatic.claims_test.Claims_Test.test_atcCategories-expected new file mode 100644 index 000000000..82a9cd3dd --- /dev/null +++ b/b2bdata/tomatic.claims_test.Claims_Test.test_atcCategories-expected @@ -0,0 +1,93 @@ +categories: +- '[RECLAMACIONS] 001. ATENCION INCORRECTA' +- '[RECLAMACIONS] 002. PRIVACIDAD DE LOS DATOS' +- '[RECLAMACIONS] 003. INCIDENCIA EN EQUIPOS DE MEDIDA' +- '[RECLAMACIONS] 004. DAÑOS ORIGINADOS POR EQUIPO DE MEDIDA' +- '[RECLAMACIONS] 005. CONTADOR EN FACTURA NO CORRESPONDE CON INSTALADO' +- '[FACTURA] 006. CONTRATOS ATR QUE NO SE FACTURAN' +- '[FACTURA] 007. CUPS NO PERTENECE A COMERCIALIZADORA O NO VIGENTE EN PERIODO DE + FACTURA' +- '[FACTURA] 008. DISCONFORMIDAD CON CONCEPTOS FACTURADOS' +- '[FACTURA] 009. DISCONFORMIDAD CON LECTURA FACTURADA' +- '[FACTURA] 010. DISCONFORMIDAD EN FACTURA ANOMALÍA / FRAUDE' +- '[FACTURA] 011. RECLAMACIÓN FACTURA PAGO DUPLICADO' +- '[FACTURA] 012. REFACTURACION NO RECIBIDA' +- '[CONTRACTES - C] 013. DISCONFORMIDAD CON CAMBIO DE SUMINISTRADOR' +- '[RECLAMACIONS] 014. REQUERIMIENTO DE FIANZA / DEPÓSITO DE GARANTÍA' +- '[CONTRACTES - B] 015. RETRASO CORTE DE SUMINISTRO' +- '[FACTURA] 018. DISCONFORMIDAD CON CRITERIOS ECONÓMICOS / COBROS' +- '[FACTURA] 019. DISCONFORMIDAD CON CRITERIOS TÉCNICOS / OBRA EJECUTADA' +- '[RECLAMACIONS] 020. CALIDAD DE ONDA' +- '[RECLAMACIONS] 021. CON PETICIÓN DE INDEMNIZACIÓN' +- '[RECLAMACIONS] 022. SIN PETICIÓN DE INDEMNIZACIÓN' +- '[RECLAMACIONS] 023. RETRASO EN PAGO INDEMNIZACION' +- '[RECLAMACIONS] 024. DAÑOS A TERCEROS POR INSTALACIONES' +- '[RECLAMACIONS] 025. IMPACTO AMBIENTAL INSTALACIONES' +- '[RECLAMACIONS] 026. RECLAMACIONES SOBRE INSTALACIONES' +- '[RECLAMACIONS] 027. DISCONFORMIDAD DESCUENTO SERVICIO INDIVIDUAL' +- '[RECLAMACIONS] 028. EJECUCIÓN INDEBIDA DE CORTE' +- '[ASSIGNAR USUARI] 029. RETRASO EN LA ATENCIÓN A RECLAMACIONES' +- '[CONTRACTES - A] 030. RETRASO PLAZO DE CONTESTACIÓN NUEVOS SUMINISTROS' +- '[CONTRACTES - A] 031. RETRASO PLAZO DE EJECUCIÓN NUEVO SUMINISTRO' +- '[CONTRACTES - B] 032. RETRASO REENGANCHE TRAS CORTE' +- '[CONTRACTES - C] 034. DISCONFORMIDAD CON CONCEPTOS DE CONTRATACIÓN ATR-PEAJE' +- '[ASSIGNAR USUARI] 035. DISCONFORMIDAD RECHAZO SOLICITUD ATR-PEAJE' +- '[FACTURA] 036. PETICIÓN DE REFACTURACIÓN APORTANDO LECTURA' +- '[FACTURA] 037. FICHERO XML INCORRECTO' +- '[RECLAMACIONS] 038. PRIVACIDAD DE LOS DATOS' +- '[RECLAMACIONS] 039. SOLICITUD DE CERTIFICADO / INFORME DE CALIDAD' +- '[FACTURA] 040. SOLICITUD DE DUPLICADO DE FACTURA' +- '[RECLAMACIONS] 041. SOLICITUD DE ACTUACIÓN SOBRE INSTALACIONES' +- '[RECLAMACIONS] 042. SOLICITUD DE DESCARGO' +- '[RECLAMACIONS] 043. PETICIÓN DE PRECINTADO / DESPRECINTADO DE EQUIPOS' +- '[RECLAMACIONS] 044. PETICIONES CON ORIGEN EN CAMPAÑAS DE TELEGESTIÓN' +- '[RECLAMACIONS] 045. ACTUALIZACION DIRECCIÓN PUNTO DE SUMINISTRO' +- '[FACTURA] 046. CERTIFICADO DE LECTURA' +- '[FACTURA] 047. SOLICITUD RECALCULO CCH SIN MODIFICACION CIERRE ATR' +- '[ASSIGNAR USUARI] 048. PETICIÓN INFORMACIÓN ADICIONAL RECHAZO' +- '[FACTURA] 049. FALTA FICHERO MEDIDA' +- '[FACTURA] 055. DISCONFORMIDAD SOBRE IMPORTE FACTURADO AUTOCONSUMO' +- '[FACTURA] 056. PETICIÓN DESGLOSE IMPORTE A FACTURAR AUTOCONSUMO' +- '[RECLAMACIONS] 057. DISCONFORMIDAD CON EXPEDIENTE DE ANOMALIA Y FRAUDE (sin factura + emitida)' +- '[CONTRACTES - C] 058. RETRASO EN PLAZO ACEPTACIÓN CAMBIO DE COMERCIALIZADOR' +- '[CONTRACTES - C] 059. RETRASO EN PLAZO ACTIVACIÓN CAMBIO DE COMERCIALIZADOR ' +- '[CONTRACTES - M] 060. RETRASO EN PLAZO ACEPTACIÓN MODIFICACIÓN CONTRACTUAL' +- '[CONTRACTES - M] 061. RETRASO EN PLAZO ACTIVACIÓN MODIFICACIÓN CONTRACTUAL' +- '[CONTRACTES - A] 062. RETRASO EN PLAZO ACEPTACIÓN ALTA DE UN NUEVO SUMINISTRO' +- '[CONTRACTES - A] 063. RETRASO EN PLAZO ACTIVACIÓN ALTA DE UN NUEVO SUMINISTRO' +- '[CONTRACTES - B] 064. RETRASO EN PLAZO ACEPTACIÓN DE UNA BAJA DE UN SUMINISTRO' +- '[CONTRACTES - B] 065. RETRASO EN PLAZO ACTIVACIÓN BAJA DE UN SUMINISTRO' +- '[ASSIGNAR USUARI] 066. INFORMACIÓN/VALIDACIÓN SOBRE DATOS DEL CONTRATO ATR/PEAJE' +- '[FACTURA] 067. VERIFICACIÓN DE CONTADOR' +- '[Atenció al Client] 068. RECLAMACIÓN POR APLICACIÓN DEL FACTOR DE CONVERSIÓN O + EL PCS' +- '[CONTRACTES - C] 100. INCIDENCIAS CONTRATACIÓN BONO SOCIAL' +- '[COBRAMENTS] 101. DATOS BANCARIOS/FORMA DE PAGO ERRÓNEA' +- '[COBRAMENTS] 102. ERRORES EN COBROS/ ABONOS' +- '[FACTURA] 103. DISCONFORMIDAD PRECIOS FACTURADOS O REPERCUTIDOS POR LA COMERCIALIZADORA' +- '[COBRAMENTS] 104. DISCONFORMIDAD FRACCIONAMIENTO O GASTOS ESPECIALES COBRADOS' +- '[COBRAMENTS] 105. DISCONFORMIDAD CON EL RECOBRO' +- '[RECLAMACIONS] 106. DISCONFORMIDAD CON PENALIZACIÓN POR PRONTA RESOLUCIÓN' +- '[CONTRACTES - C] 107. INSUFICIENTE INFORMACIÓN EN EL MOMENTO DE LA CONTRATACIÓN + (Condiciones contractuales, derecho de desistimiento)' +- '[CONTRACTES - C] 108. RECLAMACION RESPECTO AL DERECHO DE DESISTIMIENTO' +- '[FACTURA] 109. FACTURACION DE OTROS SERVICIOS TRAS LA CANCELACIÓN DEL SUMINISTRO' +- '[FACTURA] 110. FALTA DE CLARIDAD EN LAS FACTURAS ' +- '[RECLAMACIONS] 111. FALTA DE CLARIDAD CONDICIONES CONTRACTUALES' +- '[RECLAMACIONS] 112. DIFICULTAD EN LA CONTRATACIÓN DE LA TUR/PVPC CON EL CUR/COR' +- '[RECLAMACIONS] 113. RECLAMACIONES POR PRACTICAS COMERCIALES INCORRECTAS' +- '[FACTURA] 114. RETRASO EN FACTURACIÓN COMERCIALIZADOR' +- '[FACTURA] 069. COPIA F1 EN PDF' +- '[ASSIGNAR USUARI] 070. RETRASO EN LA ATENCIÓN A RECLAMACIONES NO SUJETAS A ATENCIÓN + REGLAMENTARIA' +- '[Atenció al Client] 033. POR URGENCIAS' +- '[Atenció al Client] 050. DESACUERDO FACTURACIÓN' +- '[Atenció al Client] 051. CONDUCTA INADECUADA' +- '[Atenció al Client] 052. DISCONFORMIDAD TRABAJOS REALIZADOS' +- '[Atenció al Client] 053. INCUMPLIMIENTO HORA' +- '[Atenció al Client] 054. DAÑOS INSPECCIÓN' +- '[ASSIGNAR USUARI] 071. RETRASO EN PLAZO DE ACEPTACIÓN DESISTIMIENTO' +- '[ASSIGNAR USUARI] 072. RETRASO EN PLAZO DE ACTIVACIÓN DESISTIMIENTO' +- '[ASSIGNAR USUARI] 073. PARÁMETROS DE COMUNICACIÓN' +- '[ASSIGNAR USUARI] 074. RETRASO EN PLAZO DE ACEPTACIÓN ANULACIÓN' diff --git a/b2bdata/tomatic.claims_test.Claims_Test.test_crmCategories-expected b/b2bdata/tomatic.claims_test.Claims_Test.test_crmCategories-expected new file mode 100644 index 000000000..dcf510c3f --- /dev/null +++ b/b2bdata/tomatic.claims_test.Claims_Test.test_crmCategories-expected @@ -0,0 +1 @@ +categories: [] diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index db55fb7b5..ce578b250 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- import unittest +import b2btest import os from consolemsg import error from erppeek_wst import ClientWST @@ -31,6 +32,7 @@ class Claims_Test(unittest.TestCase): def setUp(self): self.maxDiff = None + self.b2bdatapath = 'b2bdata' self.erp = None if not dbconfig: return @@ -103,18 +105,15 @@ def assertAtcCase(self, case_id, expected): self.assertNsEqual(ns(result), expected) - def test_getAllClaims(self): + def test_atcCategories(self): claims = Claims(self.erp) - reclamacions = claims.get_claims() - Reclamacio = self.erp.GiscedataSubtipusReclamacio - nombre_reclamacions = Reclamacio.count() - self.assertEqual(len(reclamacions), nombre_reclamacions) + categories = claims.get_claims() + self.assertB2BEqual(ns(categories=categories).dump()) def test_crmCategories(self): claims = Claims(self.erp) - crm_categories = claims.crm_categories() - categories = ns.load('b2bdata/categories_b2b.yaml') - self.assertEqual(crm_categories, categories) + categories = claims.crm_categories() + self.assertB2BEqual(ns(categories=categories).dump()) def atc_base(self, **kwds): base = ns.loads(""" From 0d5644376b431833b222e6e3a9a47fc27c3aa22c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 17:19:51 +0100 Subject: [PATCH 020/120] crm_categories: single erp call speed up --- tomatic/claims.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index 506042bb5..39de57bbd 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -96,19 +96,12 @@ def get_claims(self): return claims def crm_categories(self): - crm_categories_model = self.erp.model('crm.case.categ') - all_crm_categories = crm_categories_model.search([]) - - crm_categories = [] - for category_id in all_crm_categories: - category = crm_categories_model.read( - category_id, ['name'] - ) - categ_name = category.get('name') - if categ_name.startswith('['): - crm_categories.append(categ_name) - - return crm_categories + ids = self.erp.CrmCaseCateg.search([]) + return [ + category['name'] + for category in self.erp.CrmCaseCateg.read(ids,['name']) + if category['name'].startswith('[') + ] def create_crm_case(self, case): '' From 8cec21988d3be36bf40c2788bce6293d958ca9ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 17:20:29 +0100 Subject: [PATCH 021/120] rewrites --- tomatic/claims.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index 39de57bbd..f446b2a20 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -6,16 +6,17 @@ RECLAMANTE = '01' -def partnerId(erp, partner_id): - if not partner_id: return None - partner_model = erp.ResPartner - return partner_model.browse([('ref', '=', partner_id)])[0].id +def partnerId(erp, partner_nif): + if not partner_nif: return None + partner = erp.ResPartner.search([ + ('ref', '=', partner_nif) + ]) + return partner[0] if partner else None def partnerAddress(erp, partner_id): if not partner_id: return None - partner_address_model = erp.ResPartnerAddress - return partner_address_model.read( + return erp.ResPartnerAddress.read( [('partner_id', '=', partner_id)], ['id', 'state_id', 'email'] )[0] @@ -161,12 +162,11 @@ def create_atc_case(self, case): crm_id = self.create_crm_case(case) contract_id = contractId(self.erp, case.contract) contract = self.erp.GiscedataPolissa.read(contract_id, ['cups']) - cups_id = contract['cups'][0] if contract else None data_atc = { 'provincia': partner_address.get('state_id')[0] if partner_address else False, 'total_cups': 1, - 'cups_id': cups_id, + 'cups_id': contract['cups'][0] if contract else None, 'subtipus_id': claim_section_id, 'reclamante': RECLAMANTE, 'resultat': resultat(case), From 35445410a889ce659b2c053eb0e26e5f927d8c4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 18:10:23 +0100 Subject: [PATCH 022/120] b2b crmcategories from production --- TODO.md | 2 +- ...st.Claims_Test.test_crmCategories-expected | 28 ++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/TODO.md b/TODO.md index 9f3a4c761..571bb5663 100644 --- a/TODO.md +++ b/TODO.md @@ -79,7 +79,7 @@ - [ ] Claims.get_claims -> claimCategories() - [ ] repurpose claims -- [ ] callinfo log: remove cups +- [ ] callinfo log: do not dump cups - [ ] callinfo log: unite resolution - [ ] callinfo log: join infos and claims - [ ] Check: sections id's unlikely to refer the same id (table) in atc and crm diff --git a/b2bdata/tomatic.claims_test.Claims_Test.test_crmCategories-expected b/b2bdata/tomatic.claims_test.Claims_Test.test_crmCategories-expected index dcf510c3f..01d3afac9 100644 --- a/b2bdata/tomatic.claims_test.Claims_Test.test_crmCategories-expected +++ b/b2bdata/tomatic.claims_test.Claims_Test.test_crmCategories-expected @@ -1 +1,27 @@ -categories: [] +categories: +- '[INFO] Dubtes informació general (com fer-me soci, com omplir)' +- '[INFO] Sóc Soci i vull Convidar. No sóc Soci i vull contractar' +- '[INFO] Bo social (com es demana, qui hi pot tenir accés, etc)' +- '[INFO] No tinc llum, què faig?' +- '[OV] Demanen canvis que els hem de dirigir a l''OV ' +- '[OV] Problemes amb l''accés a OV (contrassenya, usuari, activació' +- '[OV] Consultes sobre les seccions (infoenergia i corbes, GkWh, i' +- '[FACTURA] Dubtes informació sobre les seves factures ' +- '[FACTURA]Dubtes informació sobre les lectures - vol donar lectur' +- '[FACTURA] Info comptadors, lloguer, canvi a telegestió, trifàsic' +- '[COBRAMENTS] Dubtes informació amb factures impagades i com fer ' +- '[COBRAMENTS] Informació sobre el tall de subministrament (com to' +- '[CONTRACTES] Tot tipus de consultes referents a altes d''un nou p' +- '[CONTRACTES] Tot tipus de consultes referents a baixes d’un punt' +- '[CONTRACTES] Informació procés contractació, endarreriments, reb' +- '[CONTRACTES] Informació sobre possible canvi de comer fraudulent' +- '[CONTRACTES] Quina potència necessito? Info sobre nova tarifa' +- '[CONTRACTES]Canvi de Titular - Canvi de Pagador. Com es fa, on, ' +- '[CONTRACTES] Tràmits sobre l’autoconsum (com es fa, documentació' +- '[ENTITATS I EMPRESES] Informació com contractar administradors d' +- '[ENTITATS I EMPRESES] Informació sobre tarifa 3.X TD i 6.X TD' +- '[PROJECTES - GENERACIÓ] Informació sobre les nostres plantes' +- '[AUTOPRODUCCIÓ] Informació sobre les compres col·lectives, com i' +- '[APORTACIONS - GKWH] Informació sobre les seves aportacions al G' +- '[COMUNICACIÓ] Comentar noticies blog, campanyes, newsletter, mai' +- '[GL - PARTICIPA - AULA POPULAR] Info sobre assemblea, sobre el P' From d51c54feda500610ce22e346d442b7650ce4f18c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 18:56:26 +0100 Subject: [PATCH 023/120] move together the entrypoints to be joined --- tomatic/api.py | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/tomatic/api.py b/tomatic/api.py index d7332e54e..58ef889b5 100644 --- a/tomatic/api.py +++ b/tomatic/api.py @@ -453,6 +453,22 @@ async def updateCallLog(user, request: Request): ) return yamlfy(info=ns(message='ok')) +@app.post('/api/infoCase') +async def postInfoCase(request: Request): + info = ns.loads(await request.body()) + CallRegistry().annotateInfoRequest(info) + return yamlfy(info=ns( + message="ok" + )) + +@app.post('/api/atrCase') +async def postAtrCase(request: Request): + atc_info = ns.loads(await request.body()) + CallRegistry().annotateClaim(atc_info) + return yamlfy(info=ns( + message="ok" + )) + @app.get('/api/updateClaims') def updateClaimTypes(): @@ -489,16 +505,6 @@ def getClaimTypes(): return yamlfy(info=result) -@app.post('/api/atrCase') -async def postAtrCase(request: Request): - - atc_info = ns.loads(await request.body()) - CallRegistry().annotateClaim(atc_info) - return yamlfy(info=ns( - message="ok" - )) - - @app.get('/api/updateCrmCategories') def updateCrmCategories(): with erp() as O: @@ -518,14 +524,6 @@ def getInfos(): )) -@app.post('/api/infoCase') -async def postInfoCase(request: Request): - info = ns.loads(await request.body()) - CallRegistry().annotateInfoRequest(info) - return yamlfy(info=ns( - message="ok" - )) - # vim: ts=4 sw=4 et From 9e14423447ea38ba50f239363b38cfc2b6e64eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 20:15:52 +0100 Subject: [PATCH 024/120] unified api for call annotation /api/call/annotate --- TODO.md | 1 + tomatic/api.py | 32 ++----- tomatic/callregistry.py | 31 +++++++ tomatic/static/components/questionnaire.js | 102 ++++++--------------- 4 files changed, 70 insertions(+), 96 deletions(-) diff --git a/TODO.md b/TODO.md index 571bb5663..bab9d11a0 100644 --- a/TODO.md +++ b/TODO.md @@ -83,6 +83,7 @@ - [ ] callinfo log: unite resolution - [ ] callinfo log: join infos and claims - [ ] Check: sections id's unlikely to refer the same id (table) in atc and crm +- [ ] On failing annotation, notify the user diff --git a/tomatic/api.py b/tomatic/api.py index 58ef889b5..414d61ebb 100644 --- a/tomatic/api.py +++ b/tomatic/api.py @@ -442,32 +442,20 @@ def getCallLog(extension): ) ) -@app.post('/api/updatelog/{user}') -async def updateCallLog(user, request: Request): - body = await request.body() - fields = ns.loads(body) - extension = persons.extension(user) - CallRegistry().updateCall(extension, fields=fields) - await asyncio.gather( - *backchannel.notifyCallLogChanged(user) - ) - return yamlfy(info=ns(message='ok')) - -@app.post('/api/infoCase') -async def postInfoCase(request: Request): - info = ns.loads(await request.body()) - CallRegistry().annotateInfoRequest(info) +@app.post('/api/call/annotate') +async def callAnnotate(request: Request): + annotation = ns.loads(await request.body()) + CallRegistry().annotateCall(annotation) + user = annotation.get('user', None) + if user: + await asyncio.gather( + *backchannel.notifyCallLogChanged(user) + ) return yamlfy(info=ns( message="ok" )) -@app.post('/api/atrCase') -async def postAtrCase(request: Request): - atc_info = ns.loads(await request.body()) - CallRegistry().annotateClaim(atc_info) - return yamlfy(info=ns( - message="ok" - )) + @app.get('/api/updateClaims') diff --git a/tomatic/callregistry.py b/tomatic/callregistry.py index 6077a16cd..ab87a0065 100644 --- a/tomatic/callregistry.py +++ b/tomatic/callregistry.py @@ -40,6 +40,37 @@ def updateCall(self, extension, fields): calls.dump(self.path) + def annotateCall(self, fields): + from . import persons + extension = persons.extension(fields.user) + self.updateCall(extension, ns( + data = fields.date, + telefon = fields.phone, + partner = fields.partner, + contracte = fields.contract, + motius = fields.reason, + )) + self.annotateInfoRequest(ns( + date = fields.date, + phone = fields.phone, + person = fields.user, + reason = fields.reason, + extra = fields.notes, + )) + if not fields.claimsection: return + self.annotateClaim(ns( + date = fields.date, + person = fields.user, + reason = fields.reason, + partner = fields.partner, + contract = fields.contract, + procedente = "x" if fields.resolution == 'fair' else '', + improcedente = "x" if fields.resolution == 'unfair' else '', + solved = "x" if fields.resolution != 'unsolved' else '', + user = fields.claimsection, + observations = fields.notes, + )) + def annotateInfoRequest(self, data): self._appendToExtensionDailyInfo('info_cases', data) diff --git a/tomatic/static/components/questionnaire.js b/tomatic/static/components/questionnaire.js index 9059bee0f..b26417299 100644 --- a/tomatic/static/components/questionnaire.js +++ b/tomatic/static/components/questionnaire.js @@ -27,31 +27,10 @@ var call_reasons = CallInfo.call_reasons; var reason_filter = ""; -var postClaim = function(claim) { - m.request({ - method: 'POST', - url: '/api/atrCase', - extract: deyamlize, - body: claim - }).then(function(response){ - console.debug("Info GET Response: ",response); - if (response.info.message !== "ok" ) { - console.debug("Error al fer el post del cas atr: ", response.info.message) - } - else{ - console.debug("ATC case saved") - CallInfo.savingAnnotation = false; - } - }, function(error) { - console.debug('Info GET apicall failed: ', error); - }); -}; - - var postAnnotation = function(annotation) { m.request({ method: 'POST', - url: '/api/infoCase', + url: '/api/call/annotate', extract: deyamlize, body: annotation }).then(function(response){ @@ -66,63 +45,38 @@ var postAnnotation = function(annotation) { reason_filter = ""; CallInfo.call.proc = false; CallInfo.call.improc = false; + CallInfo.call.date = ""; } }, function(error) { console.debug('Info POST apicall failed: ', error); }); } -var updateCall = function(data) { - m.request({ - method: 'POST', - url: '/api/updatelog/'+ Login.myName(), - body: data, - extract: deyamlize, - }).then(function(response){ - console.debug("Info POST Response: ",response); - if (response.info.message !== "ok") { - console.debug("Error al fer log dels motius: ", response.info.message) - } - CallInfo.call.date = ""; - }, function(error) { - console.debug('Info POST apicall failed: ', error); - }); -} - var saveLogCalls = function(phone, user, claim, contract, partner) { CallInfo.savingAnnotation = true; var partner_code = partner!==null ? partner.id_soci : ""; var contract_number = contract!==null ? contract.number : ""; - var contract_cups = contract!==null ? contract.cups : ""; var isodate = CallInfo.call.date || new Date().toISOString() - updateCall({ - "data": isodate, - "telefon": CallInfo.call.phone, - "partner": partner_code, - "contracte": contract_number, - "motius": CallInfo.call.reason, - }) - if (claim) { - postClaim({ - "date": isodate, - "person": user, - "reason": CallInfo.call.reason, - "partner": partner_code, - "contract": contract_number, - "procedente": (claim.proc ? "x" : ""), - "improcedente": (claim.improc ? "x" : ""), - "solved": (claim.solved ? "x" : ""), - "user": (claim.tag ? claim.tag : "INFO"), - "cups": contract_cups, - "observations": CallInfo.call.extra, - }); - } postAnnotation({ + "user": user, "date": isodate, "phone": CallInfo.call.phone, - "person": user, + "partner": partner_code, + "contract": contract_number, "reason": CallInfo.call.reason, - "extra": CallInfo.call.extra, + "notes": CallInfo.call.extra, + "claimsection": ( + !claim ? "" : ( + claim.tag ? claim.tag : ( + "INFO" + ))), + "resolution": ( + !claim ? "" : ( + !claim.solved ? "unsolved" : ( + !claim.proc ? "fair" : ( + !claim.improc ? "unfair" : ( + "unsolvable" + ))))), }); } @@ -289,28 +243,28 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { name: 'resolution', onChange: function(state) { reclamacio.solved = state.value != 'unsolved'; - reclamacio.proc = state.value == 'procedent'; - reclamacio.improc = state.value == 'improcedent'; + reclamacio.proc = state.value == 'fair'; + reclamacio.improc = state.value == 'unfair'; }, checkedValue: ( - !reclamacio.solved ? 'unsolved' : - reclamacio.proc ? 'procedent' : - reclamacio.improc ? 'improcedent' : - 'nogestionable' - ), + !reclamacio.solved ? 'unsolved' : ( + reclamacio.proc ? 'fair' : ( + reclamacio.improc ? 'unfair' : ( + 'unsolvable' + )))), buttons: [{ defaultChecked: true, label: "No resolt", value: 'unsolved', },{ label: "Resolt; tenia raó", - value: 'procedent', + value: 'fair', },{ label: "Resolt; no tenia raó", - value: 'improcedent', + value: 'unfair', },{ label: "Resolt; no es podia fer res", - value: 'nogestionable', + value: 'unsolvable', }], }), ]) From c46c04478cda381371b4915de09a0a371b7999fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 20:31:52 +0100 Subject: [PATCH 025/120] ui: using resolution tag instead multiple tags --- tomatic/static/components/questionnaire.js | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/tomatic/static/components/questionnaire.js b/tomatic/static/components/questionnaire.js index b26417299..4ac270b6f 100644 --- a/tomatic/static/components/questionnaire.js +++ b/tomatic/static/components/questionnaire.js @@ -70,13 +70,7 @@ var saveLogCalls = function(phone, user, claim, contract, partner) { claim.tag ? claim.tag : ( "INFO" ))), - "resolution": ( - !claim ? "" : ( - !claim.solved ? "unsolved" : ( - !claim.proc ? "fair" : ( - !claim.improc ? "unfair" : ( - "unsolvable" - ))))), + "resolution": claim ? claim.resolution:'', }); } @@ -242,16 +236,9 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { m(RadioGroup, { name: 'resolution', onChange: function(state) { - reclamacio.solved = state.value != 'unsolved'; - reclamacio.proc = state.value == 'fair'; - reclamacio.improc = state.value == 'unfair'; + reclamacio.resolution = state.value; }, - checkedValue: ( - !reclamacio.solved ? 'unsolved' : ( - reclamacio.proc ? 'fair' : ( - reclamacio.improc ? 'unfair' : ( - 'unsolvable' - )))), + checkedValue: reclamacio.resolution, buttons: [{ defaultChecked: true, label: "No resolt", @@ -299,6 +286,7 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { var emplenaReclamacio = function(tag) { var reclamacio = { + "resolution": "unsolved", "proc": false, "improc": false, "solved": false, From f6c3fcd184034023fe0bc2f1ec55979c2fd8736e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 21:02:35 +0100 Subject: [PATCH 026/120] todos --- TODO.md | 120 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 64 insertions(+), 56 deletions(-) diff --git a/TODO.md b/TODO.md index bab9d11a0..386004b60 100644 --- a/TODO.md +++ b/TODO.md @@ -1,34 +1,7 @@ -- [x] Call Info: download invoices and metering in a separate query to provide response earlier -- [x] Call Info: Relayout persons and contracts in one side, alarms invoices and meters on the other -- [x] Anotar trucada d'una persona encara no vinculada a som -- [x] Fix: CalRegistry: Selecting twice no longer deselects -- [x] Fix: Cambio de partner -> pestaña 0 de contratos -- [x] Spinner when loading additional -- [x] Search: Update the field whenever automatic search is done -- [x] Annotate: Dissable button if logged out -- [x] Search by contract -> persones vinculades -> contractes vinculats -- [x] Annotate: Context with person name and addresses -- [x] erp connection pool -- [x] Detecting user changed by other tab or cookie timeout -- [x] Call Registry: Codi titular -> Persona atesa -- [x] Change websocket lib to enable sharing http port and debug mode - - [x] Fast api spike - - [x] Migrate main api - - [x] Migrate sockets - - [x] Migrate planner api -- [x] Fix: annotations save date with miliseconds and duplicates existing entries -- [x] Menu for planner and scripts -- [x] As an agent i want to annotate about a partner having no contracts -- [x] "No estimable" flag is obsolete. No contract is estimable now. -- [x] Contract info: Contract aministrator role (besides TPNS) -- [x] Contract info: Add owner NIF -- [x] Contract info: Add provincia field -- [x] Contract info: Add Contract modification list -- [x] Call Registry: layout shorter and wider on small screens -- [x] Create Claim case -- [ ] Create Phone Call Case -- [ ] One endpoint for call registry in API -- [ ] One method for call registry in CallRegistry +# TODO's + +## Backlog + - [ ] `Claim.get_claims` -> Claim.get/update/retrieveClaimTypes - [ ] As an agent i want to be able to see ended contracts in callinfo (pe. for claims of unauthorized switching) - [ ] Call Info: Report diferently, search cleared from no search found @@ -45,50 +18,85 @@ - [ ] Translate log field names from catalan - [ ] api/info/ringring -> api/call/ringring (ext) - [ ] api/personlog/{ext} -> api/call/log/{user} -- [ ] api/updatelog/{user} -> api/call/log/update/{user} -- [ ] api/infoCase -> api/call/annotation -- [ ] api/atrCase -> api/call/annotation (joined) -- [ ] consider joining updatelog, infoCase and atrCase - [ ] api/updateClaims -> cron or init - [ ] api/updateClaimTypes -> cron or init - [ ] api/updateCrmCategories -> cron or init - [ ] api/getClaimTypes -> api/call/claim/types? - [ ] api/getInfos -> api/call/info/types? -- [ ] conisider joining getClaimTypes and getInfos +- [ ] consider joining getClaimTypes and getInfos +- [ ] moure scripts a una carpeta +## Trello https://trello.com/c/ljKRzvz5/4221-0-3-p7-centraleta-kalinfo-desar-els-casos-de-consultes-del-kalinfo-al-erp -- [ ] moure scripts a una carpeta +- [ ] Dubte AiS: cal pujar les anotacios que heu fet de proves +- [ ] Dubte AiS: renombrar user -> seccion/team (preguntar a AiS per terminologia de domini) +- [ ] Dubte AiS: tenim les llistes a produccio que fem amb elles (mostrarles perque hi ha brossa i textos que poden canviar) -- [ ] Antotacions UI: Radio button no resolt + tenia rao > no tenia rao -- [ ] Anotacions: Unificar Api anotacio en un punt d'entrada -- [ ] Anotacions: Unificar els fitxers d'anotacio i log de trucada +- [ ] callinfo log: unite resolution fields +- [ ] callinfo log: join infos and claims +- [ ] callinfo log: join infos/claims with log? (consider performance and usage) - [ ] Importar categories que falten de atc com a categorias de crmcases -- [ ] create crm: Inserir usuari correcte al CRM - [ ] anotate_case: sensitive to the case fields creates atc or not -- [x] create crm: cas amb tot -- [x] create crm: cas sense contracte -- [x] create crm: cas sense partner -- [x] create atc uses create crm - [ ] !!! create crm: solved = True (lo comentamos cuando lo movimos a una funcion a parte) -- [x] create atc: cover test cases -- [x] empty kalinfo.crmcase and remove -- [ ] create crm: renombrar user -> seccion/team (preguntar a AiS per terminologia de domini) +- [ ] !! create crm/atc: Check: we are using same ids for atc and crm sections and it is unlikely to be like that since they belong to different tables - [ ] create crm: extract seccio del reason and remove the field - [ ] create crm: cas contracte no existeix -- [x] create crm: te sentit loggejar el cups si tenim el contract id? -- [ ] Claims.get_claims -> claimCategories() -- [ ] repurpose claims +- [ ] Rename Claims to reflect its repurposing +- [ ] create crm: Inserir usuari correcte al CRM (es fa servir l'usuari loggejat a l'erp: Scriptlauncher i no veiem com canviar-ho) -- [ ] callinfo log: do not dump cups -- [ ] callinfo log: unite resolution -- [ ] callinfo log: join infos and claims -- [ ] Check: sections id's unlikely to refer the same id (table) in atc and crm -- [ ] On failing annotation, notify the user +- [ ] On failing annotation, ui notifies the user +## Dones +- [x] Call Info: download invoices and metering in a separate query to provide response earlier +- [x] Call Info: Relayout persons and contracts in one side, alarms invoices and meters on the other +- [x] Anotar trucada d'una persona encara no vinculada a som +- [x] Fix: CalRegistry: Selecting twice no longer deselects +- [x] Fix: Cambio de partner -> pestaña 0 de contratos +- [x] Spinner when loading additional +- [x] Search: Update the field whenever automatic search is done +- [x] Annotate: Dissable button if logged out +- [x] Search by contract -> persones vinculades -> contractes vinculats +- [x] Annotate: Context with person name and addresses +- [x] erp connection pool +- [x] Detecting user changed by other tab or cookie timeout +- [x] Call Registry: Codi titular -> Persona atesa +- [x] Change websocket lib to enable sharing http port and debug mode + - [x] Fast api spike + - [x] Migrate main api + - [x] Migrate sockets + - [x] Migrate planner api +- [x] Fix: annotations save date with miliseconds and duplicates existing entries +- [x] Menu for planner and scripts +- [x] As an agent i want to annotate about a partner having no contracts +- [x] "No estimable" flag is obsolete. No contract is estimable now. +- [x] Contract info: Contract aministrator role (besides TPNS) +- [x] Contract info: Add owner NIF +- [x] Contract info: Add provincia field +- [x] Contract info: Add Contract modification list +- [x] Call Registry: layout shorter and wider on small screens + +### 2021-11-12 +- [x] Create Claim case +- [x] One endpoint for call registry in API + - [x] joining updatelog, infoCase and atrCase + - [x] api/updatelog/{user} -> api/call/annotation (joined) + - [x] api/infoCase -> api/call/annotation (joined) + - [x] api/atrCase -> api/call/annotation (joined) +- [x] One method for call registry in CallRegistry +- [x] Bug: Antotacions UI: Radio button no resolt + tenia rao > no tenia rao +- [x] create crm: cas amb tot +- [x] create crm: cas sense contracte +- [x] create crm: cas sense partner +- [x] create crm: te sentit loggejar el cups si tenim el contract id? -> No, fet +- [x] create atc uses create crm +- [x] create atc: cover test cases +- [x] empty kalinfo.crmcase and remove +- [x] Claims.get_claims -> claimCategories() +- [x] callinfo log: do not dump cups From b78367dcabfbc19449cb51039e9a6fb27184e2f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 12 Nov 2021 21:09:11 +0100 Subject: [PATCH 027/120] ui: proc/improc/solved cleanup --- tomatic/static/components/callinfo.js | 2 -- tomatic/static/components/questionnaire.js | 5 ----- 2 files changed, 7 deletions(-) diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index b5c90097b..1b4cf570d 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -42,8 +42,6 @@ CallInfo.clear = function() { CallInfo.call.log_call_reasons = []; CallInfo.call.reason = ""; CallInfo.call.extra = ""; - CallInfo.call.proc = false; - CallInfo.call.improc = false; CallInfo.currentPerson = 0; CallInfo.currentContract = 0; CallInfo.savingAnnotation = false; diff --git a/tomatic/static/components/questionnaire.js b/tomatic/static/components/questionnaire.js index 4ac270b6f..d85ac9c14 100644 --- a/tomatic/static/components/questionnaire.js +++ b/tomatic/static/components/questionnaire.js @@ -43,8 +43,6 @@ var postAnnotation = function(annotation) { CallInfo.savingAnnotation = false; CallInfo.call.extra = ""; reason_filter = ""; - CallInfo.call.proc = false; - CallInfo.call.improc = false; CallInfo.call.date = ""; } }, function(error) { @@ -287,9 +285,6 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { var emplenaReclamacio = function(tag) { var reclamacio = { "resolution": "unsolved", - "proc": false, - "improc": false, - "solved": false, "tag": tag } Dialog.show(function() { From 36cfc1424b2e8e3b4c056a70b5f90346f0b8b781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Sat, 13 Nov 2021 13:15:21 +0100 Subject: [PATCH 028/120] Readme has a TOC --- TODO.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/TODO.md b/TODO.md index 386004b60..0af45ba81 100644 --- a/TODO.md +++ b/TODO.md @@ -26,6 +26,70 @@ - [ ] consider joining getClaimTypes and getInfos - [ ] moure scripts a una carpeta +## Copied from README to be reviewed for dups + +- GSpread docs say that moving the credential to `~/.config/gspread/service_account.json` avoids having to pass it around as parameter +- CallInfo + - [ ] /api/getInfos -> /api/call/infotypes + - [ ] Pujar infos a l'ERP + - [ ] Commit `info_cases/info_cases.yaml` + - [ ] Commit `claims_dict.yaml` + - [ ] /api/updateClaims -> /api/call/claimtypes/update + - [ ] /api/getClaims -> /api/call/claimtypes + - [ ] /api/updatelog/ -> /api/call/log/ + - [ ] /api/personlog without has no sense, remove it + - [ ] /api/personlog/ en els casos de fallada returnar una llista buida sense errors (no son de fallada, encara no hi ha logs i prou) + - [ ] /api/personlog/ /api/call/log/ + - [ ] components/call.js:getLog Deprecrated + - [ ] /api/claimReasons Deprecated (no ui code aparently) + - [ ] /api/infoReasons Deprecated (no ui code aparently) + - [ ] /api/callReasons Deprecated (no ui code aparently) + - [ ] Revisar handshaking dels websockets + - [ ] /api/info/ringring -> /api/call/ringring + - [ ] Fer la data ISO al call_log + - [ ] /api/info/all/ -> /api/info/by/any/ + - [ ] /api/info/xxxx/ -> /api/info/by/xxxx/ + - [ ] Refactoritzar codi comu dels getInfoPersonByXXXX + - [ ] Optimizar búsquedas callinfo + + +- Refactoring + - [x] use persons interface everywhere + - [x] api uses persons + - [x] persons() set attributes with ns() if not found + - [x] persons.update(person, **kwds) + - [x] tomatic_says use persons + - [ ] scheduler use persons + - [ ] shiftload uses persons + - [ ] tomatic_calls uses persons + - [x] use pbx backends instead of current pbx interface + - [x] remove use setScheduledQueue (mostly in tests) + - [x] unify backend interfaces + - [x] dbasterisk works with names not extensions + +- Hangout + - [x] Configurable token file path + - [x] Choose output channel by CLI + - [x] Choose token file by CLI + - [x] List channels when no channel has been configured yet +- Planner: + - [ ] Refactor as Single Page App + - [ ] Style it + - [ ] Show cutting reasons of best solutions + - [ ] Ask before deleting, killing, uploading... +- Scheduler: + - [ ] Join load computation into the script +- Person editor: + - [ ] Disable ok until all fields are valid + - [ ] Check extension not taken already + - [ ] Focus on first item + - [ ] Take person info from holidays manager +- Callinfo + - [ ] Simplify yaml structure + - [ ] Refactor tests + - Alerts: + - [ ] Unpaid invoices + ## Trello https://trello.com/c/ljKRzvz5/4221-0-3-p7-centraleta-kalinfo-desar-els-casos-de-consultes-del-kalinfo-al-erp - [ ] Dubte AiS: cal pujar les anotacios que heu fet de proves From 9661c474d33552a7c386250df27cd54196ecbca9 Mon Sep 17 00:00:00 2001 From: Roger Date: Tue, 16 Nov 2021 12:16:34 +0100 Subject: [PATCH 029/120] Unificate info_cases and atc_cases files. --- tomatic/callregistry.py | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/tomatic/callregistry.py b/tomatic/callregistry.py index ab87a0065..e6d5c87b6 100644 --- a/tomatic/callregistry.py +++ b/tomatic/callregistry.py @@ -50,32 +50,7 @@ def annotateCall(self, fields): contracte = fields.contract, motius = fields.reason, )) - self.annotateInfoRequest(ns( - date = fields.date, - phone = fields.phone, - person = fields.user, - reason = fields.reason, - extra = fields.notes, - )) - if not fields.claimsection: return - self.annotateClaim(ns( - date = fields.date, - person = fields.user, - reason = fields.reason, - partner = fields.partner, - contract = fields.contract, - procedente = "x" if fields.resolution == 'fair' else '', - improcedente = "x" if fields.resolution == 'unfair' else '', - solved = "x" if fields.resolution != 'unsolved' else '', - user = fields.claimsection, - observations = fields.notes, - )) - - def annotateInfoRequest(self, data): - self._appendToExtensionDailyInfo('info_cases', data) - - def annotateClaim(self, data): - self._appendToExtensionDailyInfo('atc_cases', data) + self._appendToExtensionDailyInfo('cases', fields) def _appendToExtensionDailyInfo(self, prefix, info, date=datetime.today()): path = self.path.parent / prefix / '{:%Y%m%d}.yaml'.format(date) From 09f0c9b356d6a480d14aed500ee2ce04103db1e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Wed, 17 Nov 2021 17:53:09 +0100 Subject: [PATCH 030/120] todo and changelog --- CHANGES.md | 8 ++++++-- TODO.md | 30 ++++++++++++------------------ 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4ecda6fed..593ee105f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,14 +23,18 @@ ## 4.2.5 2021-11-23 -- Callinfo: New autoconsumption alert +- Call annotations are uploaded by a cron to the ERP as CRM and ATC Cases +- Call logging and annotation simplified in a single log (Breaks backward compatibility) +- persons.yaml is created the first time Tomatic is run + +- Callinfo: New autoconsumption contract alert - Fix: person color sliders set to initial values - Persons have a new field: ERP User - Accessibility: White for text in person boxes with dark colors ## 4.2.4 2021-11-22 -- Documentation: Added a user guide +- Documentation: Added user guide with screenshots and videos - Documentation: README splitted and reorganized - CI/CD Migrated from Travis to Github Actions - Scripts moved to a folder and used thought PATH diff --git a/TODO.md b/TODO.md index 0af45ba81..0937a3c6c 100644 --- a/TODO.md +++ b/TODO.md @@ -3,20 +3,21 @@ ## Backlog - [ ] `Claim.get_claims` -> Claim.get/update/retrieveClaimTypes -- [ ] As an agent i want to be able to see ended contracts in callinfo (pe. for claims of unauthorized switching) +- [ ] As an agent i want to be able to see cancelled contracts in callinfo (pe. for claims of unauthorized switching) - [ ] Call Info: Report diferently, search cleared from no search found - [ ] Call Info: Intercept backend connection errors and behave - [ ] Call info: List previous calls from same person/contract - [ ] Google login - [ ] API tests in fastapi - [ ] Accept fragile erp tests -- [ ] Fix: Person color picker does not pick initial color +- [ ] Fix: Person color picker sliders are not valued with the initial color - [ ] Strip spaces in the search - [ ] Edit previous annotations - [ ] Use contrast text color for person boxes - [ ] Sandwich pbx ext from ringring, and use users all along - [ ] Translate log field names from catalan - [ ] api/info/ringring -> api/call/ringring (ext) +- [ ] /api/personlog/ en els casos de fallada returnar una llista buida sense errors (no son de fallada, encara no hi ha logs i prou) - [ ] api/personlog/{ext} -> api/call/log/{user} - [ ] api/updateClaims -> cron or init - [ ] api/updateClaimTypes -> cron or init @@ -24,33 +25,18 @@ - [ ] api/getClaimTypes -> api/call/claim/types? - [ ] api/getInfos -> api/call/info/types? - [ ] consider joining getClaimTypes and getInfos -- [ ] moure scripts a una carpeta +- [ ] move scripts to a folder ## Copied from README to be reviewed for dups - GSpread docs say that moving the credential to `~/.config/gspread/service_account.json` avoids having to pass it around as parameter - CallInfo - - [ ] /api/getInfos -> /api/call/infotypes - [ ] Pujar infos a l'ERP - - [ ] Commit `info_cases/info_cases.yaml` - - [ ] Commit `claims_dict.yaml` - - [ ] /api/updateClaims -> /api/call/claimtypes/update - - [ ] /api/getClaims -> /api/call/claimtypes - - [ ] /api/updatelog/ -> /api/call/log/ - - [ ] /api/personlog without has no sense, remove it - - [ ] /api/personlog/ en els casos de fallada returnar una llista buida sense errors (no son de fallada, encara no hi ha logs i prou) - - [ ] /api/personlog/ /api/call/log/ - [ ] components/call.js:getLog Deprecrated - - [ ] /api/claimReasons Deprecated (no ui code aparently) - - [ ] /api/infoReasons Deprecated (no ui code aparently) - - [ ] /api/callReasons Deprecated (no ui code aparently) - [ ] Revisar handshaking dels websockets - - [ ] /api/info/ringring -> /api/call/ringring - [ ] Fer la data ISO al call_log - [ ] /api/info/all/ -> /api/info/by/any/ - [ ] /api/info/xxxx/ -> /api/info/by/xxxx/ - - [ ] Refactoritzar codi comu dels getInfoPersonByXXXX - - [ ] Optimizar búsquedas callinfo - Refactoring @@ -114,6 +100,14 @@ ## Dones +- [x] Refactoritzar codi comu dels getInfoPersonByXXXX +- [x] Optimizar búsquedas callinfo +- [x] Commit `info_cases/info_cases.yaml` +- [x] Commit `claims_dict.yaml` +- [x] /api/claimReasons Deprecated (no ui code aparently) +- [x] /api/infoReasons Deprecated (no ui code aparently) +- [x] /api/callReasons Deprecated (no ui code aparently) +- [x] Translate case field names from catalan - [x] Call Info: download invoices and metering in a separate query to provide response earlier - [x] Call Info: Relayout persons and contracts in one side, alarms invoices and meters on the other - [x] Anotar trucada d'una persona encara no vinculada a som From 6397af116a5a59e929cddc7063b8ce4aef7eb215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Wed, 17 Nov 2021 18:13:50 +0100 Subject: [PATCH 031/120] callregistry: tests on the call log side --- tomatic/callregistry_test.py | 151 +++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 tomatic/callregistry_test.py diff --git a/tomatic/callregistry_test.py b/tomatic/callregistry_test.py new file mode 100644 index 000000000..1b0759b43 --- /dev/null +++ b/tomatic/callregistry_test.py @@ -0,0 +1,151 @@ +import unittest +from pathlib import Path +from .callregistry import CallRegistry +from yamlns import namespace as ns + +def removeTree(path): + path = Path(path) + if not path.exists(): return + if path.is_file(): + path.unlink() + return + for child in path.glob('*'): + removeTree(child) + path.rmdir() + +class CallRegistry_Test(unittest.TestCase): + def setUp(self): + self.dir = Path('test_callregistry') + removeTree(self.dir) + self.dir.mkdir() + self.dailycalls = self.dir / 'dailycalls.yaml' + + def tearDown(self): + removeTree(self.dir) + + from yamlns.testutils import assertNsEqual + + def test_updateCall_behavesAtStartUp(self): + reg = CallRegistry(self.dailycalls) + assert not (self.dir/'dailycalls.yaml').exists() + self.assertEquals(reg.callsByExtension('alice'), []) + + def test_updateCall_updatesAfterWrite(self): + reg = CallRegistry(self.dailycalls) + reg.updateCall('alice', ns( + attribute="value", + tag="content", + )) + self.assertNsEqual(ns.load(self.dailycalls), """\ + alice: + - attribute: value + tag: content + """) + self.assertNsEqual( + ns(calls = reg.callsByExtension('alice')), """\ + calls: + - attribute: value + tag: content + """) + + def test_updateCall_sameTimeExtension_updates(self): + reg = CallRegistry(self.dailycalls) + reg.updateCall('alice', ns( + data="2021-02-01T20:21:22.555Z", + attribute="value", + tag="content", + )) + reg.updateCall('alice', ns( + data="2021-02-01T20:21:22.555Z", + attribute="second value", + tag="second content", + )) + self.assertNsEqual(ns.load(self.dailycalls), """\ + alice: + - data: "2021-02-01T20:21:22.555Z" + attribute: second value + tag: second content + """) + self.assertNsEqual( + ns(calls = reg.callsByExtension('alice')), """\ + calls: + - data: "2021-02-01T20:21:22.555Z" + attribute: second value + tag: second content + """) + + def test_updateCall_differentTime_appends(self): + reg = CallRegistry(self.dailycalls) + reg.updateCall('alice', ns( + data="2021-02-01T20:21:22.555Z", + attribute="value", + tag="content", + )) + reg.updateCall('alice', ns( + data="2021-02-02T20:21:22.555Z", + attribute="second value", + tag="second content", + )) + self.assertNsEqual(ns.load(self.dailycalls), """\ + alice: + - attribute: value + tag: content + data: "2021-02-01T20:21:22.555Z" + - attribute: second value + tag: second content + data: "2021-02-02T20:21:22.555Z" + """) + self.assertNsEqual( + ns(calls = reg.callsByExtension('alice')), """\ + calls: + - data: "2021-02-01T20:21:22.555Z" + attribute: value + tag: content + - data: "2021-02-02T20:21:22.555Z" + attribute: second value + tag: second content + """) + + def test_updateCall_differentExtension_splits(self): + reg = CallRegistry(self.dailycalls) + + reg.updateCall('alice', ns( + data="2021-02-02T20:21:22.555Z", + attribute="value", + tag="content", + )) + reg.updateCall('barbara', ns( + data="2021-02-02T20:21:22.555Z", + attribute="second value", + tag="second content", + )) + self.assertNsEqual(ns.load(self.dailycalls), """\ + alice: + - data: "2021-02-02T20:21:22.555Z" + attribute: value + tag: content + barbara: + - data: "2021-02-02T20:21:22.555Z" + attribute: second value + tag: second content + """) + + self.assertNsEqual( + ns(calls = reg.callsByExtension('alice')), """\ + calls: + - data: "2021-02-02T20:21:22.555Z" + attribute: value + tag: content + """) + self.assertNsEqual( + ns(calls = reg.callsByExtension('barbara')), """\ + calls: + - data: "2021-02-02T20:21:22.555Z" + attribute: second value + tag: second content + """) + + + + + From dd5a55f8af7334a0d703a019536a51245204922f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Wed, 17 Nov 2021 22:05:23 +0100 Subject: [PATCH 032/120] persons default after explicit --- tomatic/persons.py | 3 +++ tomatic/persons_test.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tomatic/persons.py b/tomatic/persons.py index bb0f08437..191cb951b 100644 --- a/tomatic/persons.py +++ b/tomatic/persons.py @@ -49,6 +49,9 @@ def reload(): persons.path = Path(path) return reload() + if not persons.path.exists(): + return reload() + if persons.mtime != persons.path.stat().st_mtime: return reload() diff --git a/tomatic/persons_test.py b/tomatic/persons_test.py index cd4757c53..1dd96a0aa 100644 --- a/tomatic/persons_test.py +++ b/tomatic/persons_test.py @@ -339,7 +339,10 @@ def test_update_getSaved(self): groups: {} """) - + def test_persons_explicit(self): + persons.persons('p.yaml') + p = persons.persons() + self.assertNsEqual(p, ns()) From ed3fd074ae9c0643b254fe4f56994d59c56594a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Wed, 17 Nov 2021 22:20:55 +0100 Subject: [PATCH 033/120] testing callregistry --- tomatic/callregistry.py | 4 +- tomatic/callregistry_test.py | 75 +++++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/tomatic/callregistry.py b/tomatic/callregistry.py index e6d5c87b6..fa9ac0ea3 100644 --- a/tomatic/callregistry.py +++ b/tomatic/callregistry.py @@ -42,7 +42,7 @@ def updateCall(self, extension, fields): def annotateCall(self, fields): from . import persons - extension = persons.extension(fields.user) + extension = persons.extension(fields.user) or fields.user self.updateCall(extension, ns( data = fields.date, telefon = fields.phone, @@ -58,7 +58,7 @@ def _appendToExtensionDailyInfo(self, prefix, info, date=datetime.today()): dailyInfo = ns() if path.exists(): dailyInfo = ns.load(str(path)) - dailyInfo.setdefault(info.person, []).append(info) + dailyInfo.setdefault(info.user, []).append(info) path.parent.mkdir(parents=True, exist_ok=True) dailyInfo.dump(str(path)) diff --git a/tomatic/callregistry_test.py b/tomatic/callregistry_test.py index 1b0759b43..4a7cd55e0 100644 --- a/tomatic/callregistry_test.py +++ b/tomatic/callregistry_test.py @@ -1,7 +1,9 @@ import unittest from pathlib import Path -from .callregistry import CallRegistry from yamlns import namespace as ns +from datetime import date +from .callregistry import CallRegistry +from .persons import persons def removeTree(path): path = Path(path) @@ -28,7 +30,7 @@ def tearDown(self): def test_updateCall_behavesAtStartUp(self): reg = CallRegistry(self.dailycalls) assert not (self.dir/'dailycalls.yaml').exists() - self.assertEquals(reg.callsByExtension('alice'), []) + self.assertEqual(reg.callsByExtension('alice'), []) def test_updateCall_updatesAfterWrite(self): reg = CallRegistry(self.dailycalls) @@ -145,7 +147,76 @@ def test_updateCall_differentExtension_splits(self): tag: second content """) + def assertDirContent(self, expected): + self.assertEqual( + [str(x) for x in sorted(self.dir.glob('**/*'))], + sorted(expected) + ) + + def test_appendDaily(self): + reg = CallRegistry(self.dailycalls) + + reg._appendToExtensionDailyInfo('prefix', ns( + user="alice", + date="2021-02-01T20:21:22.555Z", + phone="555444333", + partner="S00000", + contract="100000", + reason="CODE", + ), date=date(2021,2,1)) + self.assertDirContent([ + 'test_callregistry/prefix', + 'test_callregistry/prefix/20210201.yaml', + ]) + self.assertNsEqual(ns.load('test_callregistry/prefix/20210201.yaml'), + """ + alice: + - date: "2021-02-01T20:21:22.555Z" + phone: '555444333' + partner: 'S00000' + contract: '100000' + reason: CODE + user: alice + """) + + + def test_annotateCall_writesCallLog(self): + reg = CallRegistry(self.dailycalls) + reg.annotateCall(ns( + user="alice", + date="2021-02-01T20:21:22.555Z", + phone="555444333", + partner="S00000", + contract="100000", + reason="CODE", + )) + content = ns.load(self.dailycalls) + self.assertNsEqual(content, """\ + alice: + - data: "2021-02-01T20:21:22.555Z" + telefon: '555444333' + partner: 'S00000' + contracte: '100000' + motius: CODE + """) + def test_annotateCall_writesFiles(self): + reg = CallRegistry(self.dailycalls) + persons(self.dir/'persons.yaml') + reg.annotateCall(ns( + user="alice", + date="2021-02-01T20:21:22.555Z", + phone="555444333", + partner="S00000", + contract="100000", + reason="CODE", + )) + self.assertDirContent([ + 'test_callregistry/cases', + 'test_callregistry/cases/{:%Y%m%d}.yaml'.format( + date.today()), + str(self.dailycalls), + ]) From 8b34082072e8bca115ffd9d3f504405b51491145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Wed, 17 Nov 2021 22:45:12 +0100 Subject: [PATCH 034/120] case parameter person -> user to match frontend --- tomatic/claims.py | 7 ++++--- tomatic/claims_test.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index f446b2a20..ad7eadb78 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -104,6 +104,7 @@ def crm_categories(self): if category['name'].startswith('[') ] + def create_crm_case(self, case): '' partner_id = partnerId(self.erp, case.partner) @@ -140,8 +141,8 @@ def create_atc_case(self, case): namespace( person: - - date: D-M-YYYY H:M:S - person: person + - date: D-M-YYYY H:M:S + user: person reason: '[´section.name´] ´claim.name´. ´claim.desc´' partner: partner number contract: contract number @@ -174,7 +175,7 @@ def create_atc_case(self, case): 'email_from': partner_address.get('email') if partner_address else False, 'time_tracking_id': COMERCIALIZADORA } - # user_id = userId(self.erp, self.emails, case.person) + # user_id = userId(self.erp, self.emails, case.user) # if user_id: # data_crm['create_uid'] = user_id data_atc['crm_id'] = crm_id diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index ce578b250..0af4683ec 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -118,7 +118,7 @@ def test_crmCategories(self): def atc_base(self, **kwds): base = ns.loads(""" date: '2021-11-11T15:13:39.998Z' - person: gabriel + user: gabriel reason: '[RECLAMACIONS] 003. INCIDENCIA EN EQUIPOS DE MEDIDA' partner: S001975 contract: '0013117' @@ -135,7 +135,7 @@ def crm_base(self, **kwds): base = ns.loads("""\ date: '2021-11-11T15:13:39.998Z' phone: '' - person: gabriel + user: gabriel reason: '[RECLAMACIONS] 003. INCIDENCIA EN EQUIPOS DE MEDIDA' partner: S001975 contract: '0013117' From 0040c62570e3d57ebf64ede0459bcf4b14832e1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 19 Nov 2021 16:45:19 +0100 Subject: [PATCH 035/120] gha: TRAVIS var defined to true like in travis-ci --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 38cf8a51b..7b83deaf5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,7 +18,7 @@ jobs: # The type of runner that the job will run on runs-on: ubuntu-latest env: - TRAVIS: 1 # Skip tests requiring data + TRAVIS: 'true' # Skip tests requiring data strategy: matrix: python-version: @@ -56,6 +56,7 @@ jobs: - uses: actions/upload-artifact@master if: failure() && hashFiles('b2bdata/*result*') with: + # TODO: use GITHUB_REF_NAME GITHUB_SHA name: b2b-results path: | b2bdata/*result* From acdca21c240f16316539a2e586510cdb69298d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Sun, 21 Nov 2021 16:12:32 +0100 Subject: [PATCH 036/120] user id taken from persons or email --- tomatic/claims.py | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index ad7eadb78..40b57b7e9 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -27,22 +27,26 @@ def contractId(erp, contract): contract_id = erp.GiscedataPolissa.search([("name", "=", contract)]) if contract_id: return contract_id[0] -def userId(erp, emails, person): - email = emails[person] - partner_address_model = erp.ResPartnerAddress - address_id = partner_address_model.read( - [('email', '=', email)], - ['id'] - ) - users_model = erp.ResUsers - try: - user_id = users_model.read( - [('address_id', '=', address_id)], - ['login'] - )[0].get("id") - return user_id - except IndexError: - return None +def erpUser(erp, person): + # Try with explicit erpuser in persons.yaml + erplogin = persons().get('erpusers',{}).get(person,None) + if erplogin: + user_ids = erp.ResUsers.search([ + ('login', '=', erplogin) + ]) + if user_ids: return user_ids[0] + # if not found try with email + email = persons().get('emails',{}).get(person,None) + if email: + address_ids = erp.ResPartnerAddress.search([ + ('email', '=', email), + ]) + user_ids = erp.ResUsers.search([ + ('address_id', 'in', address_ids), + ]) + if user_ids: return user_ids[0] + # No match found + return None def resultat(case): if not case.solved: return '' @@ -175,10 +179,9 @@ def create_atc_case(self, case): 'email_from': partner_address.get('email') if partner_address else False, 'time_tracking_id': COMERCIALIZADORA } - # user_id = userId(self.erp, self.emails, case.user) - # if user_id: - # data_crm['create_uid'] = user_id - data_atc['crm_id'] = crm_id + #user_id = erpUser(self.erp, case.user) + #if user_id: + # data_crm['create_uid'] = user_id case = self.erp.GiscedataAtc.create(data_atc) return case.id From ba1ccd0db54206ff92537ffa8e7c89d408babc67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Sun, 21 Nov 2021 17:29:13 +0100 Subject: [PATCH 037/120] fix: crm_id need to be set --- tomatic/claims.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index 40b57b7e9..04d4e50a4 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -177,7 +177,8 @@ def create_atc_case(self, case): 'resultat': resultat(case), 'date': case.date, 'email_from': partner_address.get('email') if partner_address else False, - 'time_tracking_id': COMERCIALIZADORA + 'time_tracking_id': COMERCIALIZADORA, + 'crm_id': crm_id, } #user_id = erpUser(self.erp, case.user) #if user_id: From 3017b42284e49cfc206830c6debfaac258005426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Sun, 21 Nov 2021 18:16:15 +0100 Subject: [PATCH 038/120] resolution field used directly --- tomatic/claims.py | 16 ++++++++-------- tomatic/claims_test.py | 16 ++++------------ tomatic/static/components/questionnaire.js | 2 +- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index 04d4e50a4..b1f13fca2 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -49,11 +49,12 @@ def erpUser(erp, person): return None def resultat(case): - if not case.solved: return '' - if case.procedente: return '01' - if case.improcedente: return '02' - return '03' # cannot be solved - + return dict( + unsolved = '', + fair = '01', + unfair = '02', + irresolvable = '03', + ).get(case.resolution, 'bad') def sectionName(erp, section_id): claims_model = erp.GiscedataSubtipusReclamacio @@ -150,11 +151,10 @@ def create_atc_case(self, case): reason: '[´section.name´] ´claim.name´. ´claim.desc´' partner: partner number contract: contract number - procedente: '' - improcedente: '' - solved: '' user: section.name observations: comments + # maybe unsolved, fair, unfair, irresolvable or empty + resolution: fair - ... ... ) diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index 0af4683ec..69c00ffca 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -122,11 +122,9 @@ def atc_base(self, **kwds): reason: '[RECLAMACIONS] 003. INCIDENCIA EN EQUIPOS DE MEDIDA' partner: S001975 contract: '0013117' - procedente: x - improcedente: '' - solved: x user: RECLAMACIONS observations: adfasd + resolution: fair """) base.update(**kwds) return base @@ -166,9 +164,7 @@ def test_createAtcCase_procedente(self): def test_createAtcCase_improcedente(self): case = self.atc_base( - procedente='', - improcedente='x', - solved='x', + resolution='unfair', ) claims = Claims(self.erp) @@ -189,9 +185,7 @@ def test_createAtcCase_improcedente(self): def test_createAtcCase_noSolution(self): case = self.atc_base( - procedente='', - improcedente='', - solved='x', + resolution='irresolvable', ) claims = Claims(self.erp) @@ -212,9 +206,7 @@ def test_createAtcCase_noSolution(self): def test_createAtcCase_unsolved(self): case = self.atc_base( - procedente='', - improcedente='', - solved='', + resolution='unsolved', ) claims = Claims(self.erp) diff --git a/tomatic/static/components/questionnaire.js b/tomatic/static/components/questionnaire.js index d85ac9c14..da586ab25 100644 --- a/tomatic/static/components/questionnaire.js +++ b/tomatic/static/components/questionnaire.js @@ -249,7 +249,7 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { value: 'unfair', },{ label: "Resolt; no es podia fer res", - value: 'unsolvable', + value: 'irresolvable', }], }), ]) From cac7c8cc5c769e5045669ace21c07d7987533160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Sun, 21 Nov 2021 19:01:58 +0100 Subject: [PATCH 039/120] rename case fields like frontend object --- tomatic/claims.py | 34 ++++++++++++++++++++++++++++------ tomatic/claims_test.py | 15 ++++++++------- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index b1f13fca2..aa662a8f8 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -1,5 +1,26 @@ # -*- encoding: utf-8 -*- from yamlns import namespace as ns +from pydantic import BaseModel +from typing import Optional +from enum import Enum +from .persons import persons + +class Resolution(str, Enum): + unsolved = 'unsolved' + fair = 'fair' + unfair = 'unfair' + irresolvable = 'irresolvable' + +class CallAnnotation(BaseModel): + user: str + date: str + phone: str + partner: str + contract: str + reason: str + notes: str + claimsection: Optional[str] + resolution: Optional[Resolution] PHONE = 2 COMERCIALIZADORA = 1 @@ -114,7 +135,7 @@ def create_crm_case(self, case): '' partner_id = partnerId(self.erp, case.partner) partner_address = partnerAddress(self.erp, partner_id) - crm_section_id = crmSectionID(self.erp, case.user) + crm_section_id = crmSectionID(self.erp, case.claimsection) claim_section_id = claimSectionID( self.erp, case.reason.split('.')[-1].strip() ) @@ -127,19 +148,18 @@ def create_crm_case(self, case): 'partner_id': partner_id, 'partner_address_id': partner_address.get('id') if partner_address else False, 'state': 'open', # TODO: 'done' if case.solved else 'open', - 'user_id': '', + 'user_id': erpUser(self.erp, case.user), } crm_id = self.erp.CrmCase.create(data_crm).id data_history = { 'case_id': crm_id, - 'description': case.observations, + 'description': case.notes, } crm_history_id = self.erp.CrmCaseHistory.create(data_history).id return crm_id - def create_atc_case(self, case): ''' Expected case: @@ -151,14 +171,16 @@ def create_atc_case(self, case): reason: '[´section.name´] ´claim.name´. ´claim.desc´' partner: partner number contract: contract number - user: section.name - observations: comments # maybe unsolved, fair, unfair, irresolvable or empty resolution: fair + claimsection: section.name + notes: comments - ... ... ) ''' + CallAnnotation(**case) + partner_id = partnerId(self.erp, case.partner) partner_address = partnerAddress(self.erp, partner_id) claim_section_id = claimSectionID( diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index 69c00ffca..100c7a808 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -118,13 +118,14 @@ def test_crmCategories(self): def atc_base(self, **kwds): base = ns.loads(""" date: '2021-11-11T15:13:39.998Z' - user: gabriel + phone: '555444333' + user: albert reason: '[RECLAMACIONS] 003. INCIDENCIA EN EQUIPOS DE MEDIDA' partner: S001975 contract: '0013117' - user: RECLAMACIONS - observations: adfasd resolution: fair + claimsection: RECLAMACIONS + notes: adfasd """) base.update(**kwds) return base @@ -132,13 +133,13 @@ def atc_base(self, **kwds): def crm_base(self, **kwds): base = ns.loads("""\ date: '2021-11-11T15:13:39.998Z' - phone: '' - user: gabriel + phone: '555444333' + user: albert reason: '[RECLAMACIONS] 003. INCIDENCIA EN EQUIPOS DE MEDIDA' partner: S001975 contract: '0013117' - user: RECLAMACIONS - observations: adfasd + claimsection: RECLAMACIONS + notes: adfasd """) base.update(**kwds) return base From 330c72d284ef4c796de241b64563e2bf449ca62f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Sun, 21 Nov 2021 19:02:34 +0100 Subject: [PATCH 040/120] test_createCrmCase_erpuserInPersons --- tomatic/claims.py | 3 --- tomatic/claims_test.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index aa662a8f8..0f256c3b7 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -202,9 +202,6 @@ def create_atc_case(self, case): 'time_tracking_id': COMERCIALIZADORA, 'crm_id': crm_id, } - #user_id = erpUser(self.erp, case.user) - #if user_id: - # data_crm['create_uid'] = user_id case = self.erp.GiscedataAtc.create(data_atc) return case.id diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index 100c7a808..65e74d8c7 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -8,6 +8,7 @@ from yamlns import namespace as ns from xmlrpc import client as xmlrpclib from .claims import Claims +from .persons import persons try: import dbconfig @@ -34,6 +35,15 @@ def setUp(self): self.maxDiff = None self.b2bdatapath = 'b2bdata' self.erp = None + + self.old_persons = None + if hasattr(persons, 'path'): + self.old_persons = getattr(persons, 'path') + persons.path.write_text("""\ + erpusers: + marc: Marc + """, encoding='utf8') + if not dbconfig: return if not dbconfig.erppeek: @@ -42,6 +52,9 @@ def setUp(self): self.erp.begin() def tearDown(self): + if self.old_persons: + persons.path.unlink() + persons(self.old_persons) try: self.erp and self.erp.rollback() self.erp and self.erp.close() @@ -75,6 +88,7 @@ def assertCrmCase(self, case_id, expected): fkname(result, "user_id") anonymize(result, 'partner_id') anonymize(result, 'partner_address_id') + anonymize(result, 'user_id') self.assertNsEqual(ns(result), expected) @@ -242,6 +256,24 @@ def test_createCrmCase(self): user_id: false """.format(case_id)) + def test_createCrmCase_erpuserInPersons(self): + case = self.crm_base( + user='marc', + ) + claims = Claims(self.erp) + case_id = claims.create_crm_case(case) + self.assertCrmCase(case_id, """\ + canal_id: Teléfono + id: {} + name: INCIDENCIA EN EQUIPOS DE MEDIDA + partner_address_id: ...spí + partner_id: ...osé + polissa_id: '0013117' + section_id: Atenció al Client / RECLAMACIONS + state: open + user_id: ...lló + """.format(case_id)) + def test_createCrmCase_noContract(self): case = self.crm_base( contract = '', From c5e6fee6533e6d95bc5f8bdbcfe629d4b7d1cc6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Sun, 21 Nov 2021 19:11:18 +0100 Subject: [PATCH 041/120] check state in atc cases test --- tomatic/claims_test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index 65e74d8c7..06b25bf76 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -108,6 +108,7 @@ def assertAtcCase(self, case_id, expected): 'date', 'email_from', 'time_tracking_id', + 'state', ])) fkname(result, "cups_id") @@ -174,6 +175,7 @@ def test_createAtcCase_procedente(self): resultat: '01' subtipus_id: '003' time_tracking_id: Comercialitzadora + state: open total_cups: 1 """.format(case_id)) @@ -195,6 +197,7 @@ def test_createAtcCase_improcedente(self): resultat: '02' # <--------- THIS CHANGES subtipus_id: '003' time_tracking_id: Comercialitzadora + state: open total_cups: 1 """.format(case_id)) @@ -216,6 +219,7 @@ def test_createAtcCase_noSolution(self): resultat: '03' # <--------- THIS CHANGES subtipus_id: '003' time_tracking_id: Comercialitzadora + state: open total_cups: 1 """.format(case_id)) @@ -237,6 +241,7 @@ def test_createAtcCase_unsolved(self): resultat: '' # <--------- THIS CHANGES subtipus_id: '003' time_tracking_id: Comercialitzadora + state: open total_cups: 1 """.format(case_id)) From 696b6f6e7350cb919fb9a457c7308f354b9efa92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Sun, 21 Nov 2021 19:40:48 +0100 Subject: [PATCH 042/120] properly tearing down custom persons.yaml --- tomatic/claims_test.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index 06b25bf76..29744bc8d 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -39,10 +39,11 @@ def setUp(self): self.old_persons = None if hasattr(persons, 'path'): self.old_persons = getattr(persons, 'path') - persons.path.write_text("""\ - erpusers: - marc: Marc - """, encoding='utf8') + persons('testpersons.yaml') + persons.path.write_text("""\ + erpusers: + marc: Marc + """, encoding='utf8') if not dbconfig: return @@ -52,9 +53,8 @@ def setUp(self): self.erp.begin() def tearDown(self): - if self.old_persons: - persons.path.unlink() - persons(self.old_persons) + persons.path.unlink() + persons(self.old_persons or False) try: self.erp and self.erp.rollback() self.erp and self.erp.close() From 5bfe49d6330ff3d01405cb2afea791ccfe374642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Sun, 21 Nov 2021 20:09:32 +0100 Subject: [PATCH 043/120] changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 593ee105f..282ef7255 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -27,7 +27,7 @@ - Call logging and annotation simplified in a single log (Breaks backward compatibility) - persons.yaml is created the first time Tomatic is run -- Callinfo: New autoconsumption contract alert +- Call Info: New alert for self-comsumption contracts - Fix: person color sliders set to initial values - Persons have a new field: ERP User - Accessibility: White for text in person boxes with dark colors From 2d99fc31c4eac286dfb5493d729d00527098e580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Mon, 22 Nov 2021 11:51:05 +0100 Subject: [PATCH 044/120] todo updated --- TODO.md | 61 +++++++++++++++++++++++---------------------------------- 1 file changed, 25 insertions(+), 36 deletions(-) diff --git a/TODO.md b/TODO.md index 0937a3c6c..87aaf885b 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,10 @@ ## Backlog +- [ ] Remove config.yaml from git (backup the file to use it in production) +- [ ] Configurable timetable directory ('graelles') +- [ ] Configurable execution directory ('executions') +- [ ] Move shiftload generated files to a configurable dir (maybe same as timetables dir?) - [ ] `Claim.get_claims` -> Claim.get/update/retrieveClaimTypes - [ ] As an agent i want to be able to see cancelled contracts in callinfo (pe. for claims of unauthorized switching) - [ ] Call Info: Report diferently, search cleared from no search found @@ -14,50 +18,21 @@ - [ ] Strip spaces in the search - [ ] Edit previous annotations - [ ] Use contrast text color for person boxes -- [ ] Sandwich pbx ext from ringring, and use users all along +- [ ] Sandwich pbx ext from ringring, and use tomatic users inside - [ ] Translate log field names from catalan - [ ] api/info/ringring -> api/call/ringring (ext) - [ ] /api/personlog/ en els casos de fallada returnar una llista buida sense errors (no son de fallada, encara no hi ha logs i prou) - [ ] api/personlog/{ext} -> api/call/log/{user} -- [ ] api/updateClaims -> cron or init -- [ ] api/updateClaimTypes -> cron or init -- [ ] api/updateCrmCategories -> cron or init +- [ ] api/updateClaims -> called by cron or init +- [ ] api/updateClaimTypes -> called by cron or init +- [ ] api/updateCrmCategories -> called by cron or init - [ ] api/getClaimTypes -> api/call/claim/types? - [ ] api/getInfos -> api/call/info/types? - [ ] consider joining getClaimTypes and getInfos - [ ] move scripts to a folder +- [ ] GSpread docs say that moving the credential to `~/.config/gspread/service_account.json` avoids having to pass it around as parameter +- [ ] `tomatic_calls` should use persons module instead referring persons.yaml directly -## Copied from README to be reviewed for dups - -- GSpread docs say that moving the credential to `~/.config/gspread/service_account.json` avoids having to pass it around as parameter -- CallInfo - - [ ] Pujar infos a l'ERP - - [ ] components/call.js:getLog Deprecrated - - [ ] Revisar handshaking dels websockets - - [ ] Fer la data ISO al call_log - - [ ] /api/info/all/ -> /api/info/by/any/ - - [ ] /api/info/xxxx/ -> /api/info/by/xxxx/ - - -- Refactoring - - [x] use persons interface everywhere - - [x] api uses persons - - [x] persons() set attributes with ns() if not found - - [x] persons.update(person, **kwds) - - [x] tomatic_says use persons - - [ ] scheduler use persons - - [ ] shiftload uses persons - - [ ] tomatic_calls uses persons - - [x] use pbx backends instead of current pbx interface - - [x] remove use setScheduledQueue (mostly in tests) - - [x] unify backend interfaces - - [x] dbasterisk works with names not extensions - -- Hangout - - [x] Configurable token file path - - [x] Choose output channel by CLI - - [x] Choose token file by CLI - - [x] List channels when no channel has been configured yet - Planner: - [ ] Refactor as Single Page App - [ ] Style it @@ -70,6 +45,7 @@ - [ ] Check extension not taken already - [ ] Focus on first item - [ ] Take person info from holidays manager + - [ ] List/admin mode - Callinfo - [ ] Simplify yaml structure - [ ] Refactor tests @@ -93,13 +69,26 @@ - [ ] create crm: cas contracte no existeix - [ ] Rename Claims to reflect its repurposing - [ ] create crm: Inserir usuari correcte al CRM (es fa servir l'usuari loggejat a l'erp: Scriptlauncher i no veiem com canviar-ho) - - [ ] On failing annotation, ui notifies the user ## Dones +- [x] persons interface: api uses persons +- [x] persons interface: persons() set attributes with ns() if not found +- [x] persons interface: persons.update(person, **kwds) +- [x] persons interface: tomatic_says use persons +- [x] persons interface: scheduler use persons +- [x] persons interface: shiftload uses persons +- [x] pbx interface: use pbx backends instead of current pbx interface +- [x] pbx interface: remove use setScheduledQueue (mostly in tests) +- [x] pbx interface: unify backend interfaces +- [x] pbx interface: dbasterisk works with names not extensions +- [x] Hangouts: Configurable token file path +- [x] Hangouts: Choose output channel by CLI +- [x] Hangouts: Choose token file by CLI +- [x] Hangouts: List channels when no channel has been configured yet - [x] Refactoritzar codi comu dels getInfoPersonByXXXX - [x] Optimizar búsquedas callinfo - [x] Commit `info_cases/info_cases.yaml` From c4d1bd9808b61aa2304980bb46c2b52c7795aea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Mon, 22 Nov 2021 13:08:20 +0100 Subject: [PATCH 045/120] todos added --- TODO.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO.md b/TODO.md index 87aaf885b..572fd8074 100644 --- a/TODO.md +++ b/TODO.md @@ -11,6 +11,8 @@ - [ ] Call Info: Report diferently, search cleared from no search found - [ ] Call Info: Intercept backend connection errors and behave - [ ] Call info: List previous calls from same person/contract +- [ ] Unify call log into the case log +- [ ] Editable erpuser in PersonEditor - [ ] Google login - [ ] API tests in fastapi - [ ] Accept fragile erp tests From a8c5f3b6eddd8ae16431b4d0045e92ed4df4bdf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 23 Nov 2021 18:10:56 +0100 Subject: [PATCH 046/120] todos --- TODO.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/TODO.md b/TODO.md index 572fd8074..466eb4cd6 100644 --- a/TODO.md +++ b/TODO.md @@ -11,17 +11,14 @@ - [ ] Call Info: Report diferently, search cleared from no search found - [ ] Call Info: Intercept backend connection errors and behave - [ ] Call info: List previous calls from same person/contract -- [ ] Unify call log into the case log -- [ ] Editable erpuser in PersonEditor +- [ ] Unify call log also into the case log - [ ] Google login - [ ] API tests in fastapi - [ ] Accept fragile erp tests -- [ ] Fix: Person color picker sliders are not valued with the initial color - [ ] Strip spaces in the search - [ ] Edit previous annotations -- [ ] Use contrast text color for person boxes -- [ ] Sandwich pbx ext from ringring, and use tomatic users inside -- [ ] Translate log field names from catalan +- [ ] On ringing sandwich pbx ext, and use tomatic users inside (do not log by ext but by user) +- [ ] Translate call log field names from catalan - [ ] api/info/ringring -> api/call/ringring (ext) - [ ] /api/personlog/ en els casos de fallada returnar una llista buida sense errors (no son de fallada, encara no hi ha logs i prou) - [ ] api/personlog/{ext} -> api/call/log/{user} @@ -31,7 +28,6 @@ - [ ] api/getClaimTypes -> api/call/claim/types? - [ ] api/getInfos -> api/call/info/types? - [ ] consider joining getClaimTypes and getInfos -- [ ] move scripts to a folder - [ ] GSpread docs say that moving the credential to `~/.config/gspread/service_account.json` avoids having to pass it around as parameter - [ ] `tomatic_calls` should use persons module instead referring persons.yaml directly @@ -41,11 +37,12 @@ - [ ] Show cutting reasons of best solutions - [ ] Ask before deleting, killing, uploading... - Scheduler: - - [ ] Join load computation into the script + - [ ] Join load computation into the scheduler script - Person editor: - [ ] Disable ok until all fields are valid - [ ] Check extension not taken already - - [ ] Focus on first item + - [ ] Check erp user exists + - [ ] Focus on first dialog field on open - [ ] Take person info from holidays manager - [ ] List/admin mode - Callinfo @@ -66,17 +63,20 @@ - [ ] Importar categories que falten de atc com a categorias de crmcases - [ ] anotate_case: sensitive to the case fields creates atc or not - [ ] !!! create crm: solved = True (lo comentamos cuando lo movimos a una funcion a parte) -- [ ] !! create crm/atc: Check: we are using same ids for atc and crm sections and it is unlikely to be like that since they belong to different tables - [ ] create crm: extract seccio del reason and remove the field - [ ] create crm: cas contracte no existeix -- [ ] Rename Claims to reflect its repurposing -- [ ] create crm: Inserir usuari correcte al CRM (es fa servir l'usuari loggejat a l'erp: Scriptlauncher i no veiem com canviar-ho) -- [ ] On failing annotation, ui notifies the user +- [ ] callreg: Rename Claims to reflect its repurposing +- [ ] callreg: create crm: Inserir usuari correcte al CRM (es fa servir l'usuari loggejat a l'erp: Scriptlauncher i no veiem com canviar-ho) +- [ ] callreg: On failing annotation, ui notifies the user ## Dones +- [x] Use contrast text color for person boxes +- [x] Editable erpuser in PersonEditor +- [x] move scripts to a folder +- [x] Fix: Person color picker sliders are not valued with the initial color - [x] persons interface: api uses persons - [x] persons interface: persons() set attributes with ns() if not found - [x] persons interface: persons.update(person, **kwds) From c8efd2ed9a43b49cbcad14a4ebd10eb5deb9230d Mon Sep 17 00:00:00 2001 From: Marta Date: Wed, 29 Dec 2021 09:12:02 +0100 Subject: [PATCH 047/120] create crm case for info calls * create crm case in ERP for every call * create atc case in ERP when its a claim call * changed INFO -> CONSULTA --- scripts/tomatic_uploadcases.py | 7 ++- tomatic/claims.py | 60 +++++++++++++--------- tomatic/static/components/questionnaire.js | 2 +- 3 files changed, 42 insertions(+), 27 deletions(-) diff --git a/scripts/tomatic_uploadcases.py b/scripts/tomatic_uploadcases.py index b44c19744..ef9dc8889 100755 --- a/scripts/tomatic_uploadcases.py +++ b/scripts/tomatic_uploadcases.py @@ -29,8 +29,11 @@ def main(yaml_directory, current_date): for person in atc_yaml: for case in atc_yaml[person]: try: - case_id = claims.create_atc_case(case) - logging.info(" Case {} created.".format(case_id)) + crm_case_id = claims.create_crm_case(case) + logging.info(" CRM case {} created.".format(crm_case_id)) + if claims.is_atc_case(case): + atc_case_id = claims.create_atc_case(case, crm_case_id) + logging.info(" ATC case {} created.".format(atc_case_id)) except Exception as e: logging.error(" Something went wrong in {}: {}".format( atc_yaml_file, diff --git a/tomatic/claims.py b/tomatic/claims.py index 0f256c3b7..a27716aad 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -10,6 +10,7 @@ class Resolution(str, Enum): fair = 'fair' unfair = 'unfair' irresolvable = 'irresolvable' + not_resolution = '' class CallAnnotation(BaseModel): user: str @@ -25,6 +26,16 @@ class CallAnnotation(BaseModel): PHONE = 2 COMERCIALIZADORA = 1 RECLAMANTE = '01' +CRM_CASE_SECTION_NAME = 'CONSULTA' +UNKNOWN_STATE = None +defaultSection = 'ASSIGNAR USUARI' + + +def descStateId(erp): + global UNKNOWN_STATE + if UNKNOWN_STATE is None: + UNKNOWN_STATE = erp.model('res.country.state').search([('code', '=', '00')])[0] + return UNKNOWN_STATE def partnerId(erp, partner_nif): @@ -91,7 +102,6 @@ def crmSectionID(erp, section): sections_model = erp.CrmCaseSection return sections_model.search([('name', 'ilike', section)])[0] -defaultSection = 'ASSIGNAR USUARI' class Claims(object): @@ -125,42 +135,41 @@ def get_claims(self): def crm_categories(self): ids = self.erp.CrmCaseCateg.search([]) return [ - category['name'] + f"[{CRM_CASE_SECTION_NAME}] {category['name']}" for category in self.erp.CrmCaseCateg.read(ids,['name']) if category['name'].startswith('[') ] - def create_crm_case(self, case): - '' - partner_id = partnerId(self.erp, case.partner) + def create_crm_case(self, crm_case): + CallAnnotation(**crm_case) + partner_id = partnerId(self.erp, crm_case.partner) partner_address = partnerAddress(self.erp, partner_id) - crm_section_id = crmSectionID(self.erp, case.claimsection) - claim_section_id = claimSectionID( - self.erp, case.reason.split('.')[-1].strip() - ) + + crm_section_id = crmSectionID(self.erp, crm_case.claimsection) data_crm = { 'section_id': crm_section_id, - 'name': sectionName(self.erp, claim_section_id), + 'name': crm_case.reason.split('.')[-1].strip(), 'canal_id': PHONE, - 'polissa_id': contractId(self.erp, case.contract), + 'polissa_id': contractId(self.erp, crm_case.contract), 'partner_id': partner_id, 'partner_address_id': partner_address.get('id') if partner_address else False, 'state': 'open', # TODO: 'done' if case.solved else 'open', - 'user_id': erpUser(self.erp, case.user), + 'user_id': erpUser(self.erp, crm_case.user), } crm_id = self.erp.CrmCase.create(data_crm).id data_history = { 'case_id': crm_id, - 'description': case.notes, + 'description': crm_case.notes, } crm_history_id = self.erp.CrmCaseHistory.create(data_history).id return crm_id - def create_atc_case(self, case): + + def create_atc_case(self, atr_case_data, crm_case_id): ''' Expected case: @@ -179,31 +188,34 @@ def create_atc_case(self, case): ... ) ''' - CallAnnotation(**case) + CallAnnotation(**atr_case_data) - partner_id = partnerId(self.erp, case.partner) + partner_id = partnerId(self.erp, atr_case_data.partner) partner_address = partnerAddress(self.erp, partner_id) claim_section_id = claimSectionID( - self.erp, case.reason.split('.')[-1].strip() + self.erp, atr_case_data.reason.split('.')[-1].strip() ) - crm_id = self.create_crm_case(case) - contract_id = contractId(self.erp, case.contract) + contract_id = contractId(self.erp, atr_case_data.contract) contract = self.erp.GiscedataPolissa.read(contract_id, ['cups']) - + state_id = partner_address.get('state_id')[0] if partner_address else descStateId(self.erp) data_atc = { - 'provincia': partner_address.get('state_id')[0] if partner_address else False, + 'provincia': state_id, 'total_cups': 1, 'cups_id': contract['cups'][0] if contract else None, 'subtipus_id': claim_section_id, 'reclamante': RECLAMANTE, - 'resultat': resultat(case), - 'date': case.date, + 'resultat': resultat(atr_case_data), + 'date': atr_case_data.date, 'email_from': partner_address.get('email') if partner_address else False, 'time_tracking_id': COMERCIALIZADORA, - 'crm_id': crm_id, + 'crm_id': crm_case_id, } case = self.erp.GiscedataAtc.create(data_atc) return case.id + def is_atc_case(self, case_data): + return case_data.get('claimsection', '') != '' + + # vim: et ts=4 sw=4 diff --git a/tomatic/static/components/questionnaire.js b/tomatic/static/components/questionnaire.js index da586ab25..be9c29cef 100644 --- a/tomatic/static/components/questionnaire.js +++ b/tomatic/static/components/questionnaire.js @@ -186,7 +186,7 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { } var esReclamacio = function(type) { - const info = "INFO"; + const info = "CONSULTA"; return (type != info); } From 632115bde6d3c8259aea07e6ff68da5137f3513e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 21 Jan 2022 03:07:57 +0100 Subject: [PATCH 048/120] create_atc_case also creates crm_case, tests pass --- scripts/tomatic_uploadcases.py | 7 ++++--- tomatic/claims.py | 4 +++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/tomatic_uploadcases.py b/scripts/tomatic_uploadcases.py index ef9dc8889..143d60036 100755 --- a/scripts/tomatic_uploadcases.py +++ b/scripts/tomatic_uploadcases.py @@ -29,11 +29,12 @@ def main(yaml_directory, current_date): for person in atc_yaml: for case in atc_yaml[person]: try: - crm_case_id = claims.create_crm_case(case) - logging.info(" CRM case {} created.".format(crm_case_id)) if claims.is_atc_case(case): atc_case_id = claims.create_atc_case(case, crm_case_id) - logging.info(" ATC case {} created.".format(atc_case_id)) + logging.info(f" ATC case {atc_case_id} created.") + else: + crm_case_id = claims.create_crm_case(case) + logging.info(f" CRM case {crm_case_id} created.") except Exception as e: logging.error(" Something went wrong in {}: {}".format( atc_yaml_file, diff --git a/tomatic/claims.py b/tomatic/claims.py index a27716aad..a1c6bfca1 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -169,7 +169,7 @@ def create_crm_case(self, crm_case): return crm_id - def create_atc_case(self, atr_case_data, crm_case_id): + def create_atc_case(self, atr_case_data): ''' Expected case: @@ -190,6 +190,8 @@ def create_atc_case(self, atr_case_data, crm_case_id): ''' CallAnnotation(**atr_case_data) + crm_case_id = self.create_crm_case(atr_case_data) + partner_id = partnerId(self.erp, atr_case_data.partner) partner_address = partnerAddress(self.erp, partner_id) claim_section_id = claimSectionID( From a137d4159b0cdfbe193d4f727bf13f5be85e9f09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 21 Jan 2022 03:14:10 +0100 Subject: [PATCH 049/120] cached unknown state without a global --- tomatic/claims.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index a1c6bfca1..604672a74 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -27,16 +27,16 @@ class CallAnnotation(BaseModel): COMERCIALIZADORA = 1 RECLAMANTE = '01' CRM_CASE_SECTION_NAME = 'CONSULTA' -UNKNOWN_STATE = None defaultSection = 'ASSIGNAR USUARI' -def descStateId(erp): - global UNKNOWN_STATE - if UNKNOWN_STATE is None: - UNKNOWN_STATE = erp.model('res.country.state').search([('code', '=', '00')])[0] - return UNKNOWN_STATE - +def unknownState(erp): + if hasattr(unknownState,'cached'): + return unknownState.cached + unknownState.cached = erp.ResCountryState.search([ + ('code', '=', '00') + ])[0] + return unknownState.cached def partnerId(erp, partner_nif): if not partner_nif: return None @@ -199,7 +199,7 @@ def create_atc_case(self, atr_case_data): ) contract_id = contractId(self.erp, atr_case_data.contract) contract = self.erp.GiscedataPolissa.read(contract_id, ['cups']) - state_id = partner_address.get('state_id')[0] if partner_address else descStateId(self.erp) + state_id = partner_address.get('state_id')[0] if partner_address else unknownState(self.erp) data_atc = { 'provincia': state_id, 'total_cups': 1, From 08313052c63d6e447be857573eafe573615f935d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 21 Jan 2022 03:31:26 +0100 Subject: [PATCH 050/120] constants renaming --- tomatic/claims.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index 604672a74..0c53d1435 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -23,11 +23,11 @@ class CallAnnotation(BaseModel): claimsection: Optional[str] resolution: Optional[Resolution] -PHONE = 2 -COMERCIALIZADORA = 1 -RECLAMANTE = '01' +PHONE_CHANNEL = 2 +TIME_TRACKER_COMERCIALIZADORA = 1 +CLAIMANT = '01' # Titular de PS/ Usuario efectivo (Tabla 83) CRM_CASE_SECTION_NAME = 'CONSULTA' -defaultSection = 'ASSIGNAR USUARI' +DEFAULT_SECTION = 'ASSIGNAR USUARI' def unknownState(erp): @@ -119,7 +119,7 @@ def get_claims(self): ): claim_section = claim.get("default_section") - section = defaultSection + section = DEFAULT_SECTION if claim_section: section = claim_section[1].split("/")[-1].strip() @@ -151,7 +151,7 @@ def create_crm_case(self, crm_case): data_crm = { 'section_id': crm_section_id, 'name': crm_case.reason.split('.')[-1].strip(), - 'canal_id': PHONE, + 'canal_id': PHONE_CHANNEL, 'polissa_id': contractId(self.erp, crm_case.contract), 'partner_id': partner_id, 'partner_address_id': partner_address.get('id') if partner_address else False, @@ -205,11 +205,11 @@ def create_atc_case(self, atr_case_data): 'total_cups': 1, 'cups_id': contract['cups'][0] if contract else None, 'subtipus_id': claim_section_id, - 'reclamante': RECLAMANTE, + 'reclamante': CLAIMANT, 'resultat': resultat(atr_case_data), 'date': atr_case_data.date, 'email_from': partner_address.get('email') if partner_address else False, - 'time_tracking_id': COMERCIALIZADORA, + 'time_tracking_id': TIME_TRACKER_COMERCIALIZADORA, 'crm_id': crm_case_id, } case = self.erp.GiscedataAtc.create(data_atc) From 7969c4de0bb243510537c73937a34fadedbb888b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 21 Jan 2022 04:35:03 +0100 Subject: [PATCH 051/120] assert and case build sepearated --- tomatic/claims_test.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index 29744bc8d..4ea06adb1 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -64,11 +64,7 @@ def tearDown(self): from yamlns.testutils import assertNsEqual - def assertCrmCase(self, case_id, expected): - if not expected: - self.assertFalse(case_id) - return - self.assertTrue(case_id) + def crmCase(self, case_id): result = ns(self.erp.CrmCase.read(case_id, [ 'section_id', 'name', @@ -90,14 +86,18 @@ def assertCrmCase(self, case_id, expected): anonymize(result, 'partner_address_id') anonymize(result, 'user_id') - self.assertNsEqual(ns(result), expected) - + return result - def assertAtcCase(self, case_id, expected): + def assertCrmCase(self, case_id, expected): if not expected: self.assertFalse(case_id) return self.assertTrue(case_id) + result = self.crmCase(case_id) + self.assertNsEqual(result, expected) + + + def atcCase(self, case_id): result = ns(self.erp.GiscedataAtc.read(case_id, [ 'provincia', 'total_cups', @@ -118,7 +118,15 @@ def assertAtcCase(self, case_id, expected): anonymize(result, "cups_id") anonymize(result, "email_from") - self.assertNsEqual(ns(result), expected) + return result + + def assertAtcCase(self, case_id, expected): + if not expected: + self.assertFalse(case_id) + return + self.assertTrue(case_id) + result = self.atcCase(case_id) + self.assertNsEqual(result, expected) def test_atcCategories(self): claims = Claims(self.erp) From c940cf7c8cf32995d35a98b9e96050d40d60501b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 21 Jan 2022 08:55:06 +0100 Subject: [PATCH 052/120] reorder claims_test --- tomatic/claims_test.py | 45 ++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index 4ea06adb1..251aa44d0 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -65,6 +65,7 @@ def tearDown(self): from yamlns.testutils import assertNsEqual def crmCase(self, case_id): + """Retrieves the data of a CrmCase to check its fields""" result = ns(self.erp.CrmCase.read(case_id, [ 'section_id', 'name', @@ -88,16 +89,8 @@ def crmCase(self, case_id): return result - def assertCrmCase(self, case_id, expected): - if not expected: - self.assertFalse(case_id) - return - self.assertTrue(case_id) - result = self.crmCase(case_id) - self.assertNsEqual(result, expected) - - def atcCase(self, case_id): + """Retrieves the data of a AtcCase to check its fields""" result = ns(self.erp.GiscedataAtc.read(case_id, [ 'provincia', 'total_cups', @@ -120,7 +113,19 @@ def atcCase(self, case_id): return result + def assertCrmCase(self, case_id, expected): + """Asserts that the CrmCase fields to be checked have + the proper values""" + if not expected: + self.assertFalse(case_id) + return + self.assertTrue(case_id) + result = self.crmCase(case_id) + self.assertNsEqual(result, expected) + def assertAtcCase(self, case_id, expected): + """Asserts that the AtcCase fields to be checked have + the proper values""" if not expected: self.assertFalse(case_id) return @@ -128,17 +133,8 @@ def assertAtcCase(self, case_id, expected): result = self.atcCase(case_id) self.assertNsEqual(result, expected) - def test_atcCategories(self): - claims = Claims(self.erp) - categories = claims.get_claims() - self.assertB2BEqual(ns(categories=categories).dump()) - - def test_crmCategories(self): - claims = Claims(self.erp) - categories = claims.crm_categories() - self.assertB2BEqual(ns(categories=categories).dump()) - def atc_base(self, **kwds): + """Provides a base for input atc cases to be feed""" base = ns.loads(""" date: '2021-11-11T15:13:39.998Z' phone: '555444333' @@ -167,6 +163,17 @@ def crm_base(self, **kwds): base.update(**kwds) return base + + def test_atcCategories(self): + claims = Claims(self.erp) + categories = claims.get_claims() + self.assertB2BEqual(ns(categories=categories).dump()) + + def test_crmCategories(self): + claims = Claims(self.erp) + categories = claims.crm_categories() + self.assertB2BEqual(ns(categories=categories).dump()) + def test_createAtcCase_procedente(self): case = self.atc_base() From d356bac7e68317077609d37e3601d77c5524f225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 21 Jan 2022 12:51:25 +0100 Subject: [PATCH 053/120] resultat -> resolutionCode --- tomatic/claims.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index 0c53d1435..c63dfa1f3 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -80,7 +80,7 @@ def erpUser(erp, person): # No match found return None -def resultat(case): +def resolutionCode(case): return dict( unsolved = '', fair = '01', @@ -206,7 +206,7 @@ def create_atc_case(self, atr_case_data): 'cups_id': contract['cups'][0] if contract else None, 'subtipus_id': claim_section_id, 'reclamante': CLAIMANT, - 'resultat': resultat(atr_case_data), + 'resultat': resolutionCode(atr_case_data), 'date': atr_case_data.date, 'email_from': partner_address.get('email') if partner_address else False, 'time_tracking_id': TIME_TRACKER_COMERCIALIZADORA, From 4f63d087673b35b536a85b495a1186280fbe3d98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 21 Jan 2022 12:52:18 +0100 Subject: [PATCH 054/120] param crm_case -> case (case is agnostic of type) --- tomatic/claims.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index c63dfa1f3..b572d6a75 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -141,28 +141,28 @@ def crm_categories(self): ] - def create_crm_case(self, crm_case): - CallAnnotation(**crm_case) - partner_id = partnerId(self.erp, crm_case.partner) + def create_crm_case(self, case): + CallAnnotation(**case) + partner_id = partnerId(self.erp, case.partner) partner_address = partnerAddress(self.erp, partner_id) - crm_section_id = crmSectionID(self.erp, crm_case.claimsection) + crm_section_id = crmSectionID(self.erp, case.claimsection) data_crm = { 'section_id': crm_section_id, - 'name': crm_case.reason.split('.')[-1].strip(), + 'name': case.reason.split('.')[-1].strip(), 'canal_id': PHONE_CHANNEL, - 'polissa_id': contractId(self.erp, crm_case.contract), + 'polissa_id': contractId(self.erp, case.contract), 'partner_id': partner_id, 'partner_address_id': partner_address.get('id') if partner_address else False, 'state': 'open', # TODO: 'done' if case.solved else 'open', - 'user_id': erpUser(self.erp, crm_case.user), + 'user_id': erpUser(self.erp, case.user), } crm_id = self.erp.CrmCase.create(data_crm).id data_history = { 'case_id': crm_id, - 'description': crm_case.notes, + 'description': case.notes, } crm_history_id = self.erp.CrmCaseHistory.create(data_history).id From f49aea062627c0a2bca3bb9d8ff9fa1eaa60836d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 21 Jan 2022 12:54:03 +0100 Subject: [PATCH 055/120] atr_case_data -> case (case is agnostic of type) --- tomatic/claims.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index b572d6a75..8110e9831 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -168,8 +168,7 @@ def create_crm_case(self, case): return crm_id - - def create_atc_case(self, atr_case_data): + def create_atc_case(self, case): ''' Expected case: @@ -188,16 +187,16 @@ def create_atc_case(self, atr_case_data): ... ) ''' - CallAnnotation(**atr_case_data) + CallAnnotation(**case) - crm_case_id = self.create_crm_case(atr_case_data) + crm_case_id = self.create_crm_case(case) - partner_id = partnerId(self.erp, atr_case_data.partner) + partner_id = partnerId(self.erp, case.partner) partner_address = partnerAddress(self.erp, partner_id) claim_section_id = claimSectionID( - self.erp, atr_case_data.reason.split('.')[-1].strip() + self.erp, case.reason.split('.')[-1].strip() ) - contract_id = contractId(self.erp, atr_case_data.contract) + contract_id = contractId(self.erp, case.contract) contract = self.erp.GiscedataPolissa.read(contract_id, ['cups']) state_id = partner_address.get('state_id')[0] if partner_address else unknownState(self.erp) data_atc = { @@ -206,8 +205,8 @@ def create_atc_case(self, atr_case_data): 'cups_id': contract['cups'][0] if contract else None, 'subtipus_id': claim_section_id, 'reclamante': CLAIMANT, - 'resultat': resolutionCode(atr_case_data), - 'date': atr_case_data.date, + 'resultat': resolutionCode(case), + 'date': case.date, 'email_from': partner_address.get('email') if partner_address else False, 'time_tracking_id': TIME_TRACKER_COMERCIALIZADORA, 'crm_id': crm_case_id, From 626e1e72d6bc8c6f99f128a7d04685934a926483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 21 Jan 2022 13:45:51 +0100 Subject: [PATCH 056/120] case_data -> case (input struct always case) --- tomatic/claims.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index 8110e9831..ae5b0cc49 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -215,8 +215,8 @@ def create_atc_case(self, case): return case.id - def is_atc_case(self, case_data): - return case_data.get('claimsection', '') != '' + def is_atc_case(self, case): + return case.get('claimsection', '') != '' # vim: et ts=4 sw=4 From 11b96fbf79d3d52e4ecc13b33c13428d06805746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 21 Jan 2022 20:31:13 +0100 Subject: [PATCH 057/120] callinfo: ensure just the first dot is splitted --- tomatic/claims.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index ae5b0cc49..d9a419165 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -150,7 +150,7 @@ def create_crm_case(self, case): data_crm = { 'section_id': crm_section_id, - 'name': case.reason.split('.')[-1].strip(), + 'name': case.reason.split('.',1)[-1].strip(), 'canal_id': PHONE_CHANNEL, 'polissa_id': contractId(self.erp, case.contract), 'partner_id': partner_id, @@ -194,7 +194,7 @@ def create_atc_case(self, case): partner_id = partnerId(self.erp, case.partner) partner_address = partnerAddress(self.erp, partner_id) claim_section_id = claimSectionID( - self.erp, case.reason.split('.')[-1].strip() + self.erp, case.reason.split('.',1)[-1].strip() ) contract_id = contractId(self.erp, case.contract) contract = self.erp.GiscedataPolissa.read(contract_id, ['cups']) From bd0d47dcf076d784c859b674f0dfc638bf1def73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Fri, 21 Jan 2022 20:31:59 +0100 Subject: [PATCH 058/120] nicer text color in kumato --- tomatic/static/components/callinfo_style.styl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tomatic/static/components/callinfo_style.styl b/tomatic/static/components/callinfo_style.styl index 3045d52d8..4d98f7296 100644 --- a/tomatic/static/components/callinfo_style.styl +++ b/tomatic/static/components/callinfo_style.styl @@ -430,7 +430,7 @@ body #tomatic > .pe-dark-tone nobackground: #3c1509 background: #373721 - color: #d9bbbb + color: #d9dfdf min-height: 100% From 0819a9552611b98e81e08ae2161c57d08a1fb71c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Sun, 23 Jan 2022 23:10:00 +0100 Subject: [PATCH 059/120] removed no_resolution state --- tomatic/claims.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index d9a419165..52b4fac8b 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -10,7 +10,6 @@ class Resolution(str, Enum): fair = 'fair' unfair = 'unfair' irresolvable = 'irresolvable' - not_resolution = '' class CallAnnotation(BaseModel): user: str From c53c181fce157833b64a1964011ed5b075839307 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Sun, 23 Jan 2022 23:11:00 +0100 Subject: [PATCH 060/120] DEFAULT_SECTION -> SECTION_TO_BE_SPECIFIED --- tomatic/claims.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index 52b4fac8b..2546166ec 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -26,7 +26,7 @@ class CallAnnotation(BaseModel): TIME_TRACKER_COMERCIALIZADORA = 1 CLAIMANT = '01' # Titular de PS/ Usuario efectivo (Tabla 83) CRM_CASE_SECTION_NAME = 'CONSULTA' -DEFAULT_SECTION = 'ASSIGNAR USUARI' +SECTION_TO_BE_SPECIFIED = 'ASSIGNAR USUARI' def unknownState(erp): @@ -118,9 +118,10 @@ def get_claims(self): ): claim_section = claim.get("default_section") - section = DEFAULT_SECTION - if claim_section: - section = claim_section[1].split("/")[-1].strip() + section = ( + claim_section[1].split("/")[-1].strip() + if claim_section else SECTION_TO_BE_SPECIFIED + ) message = u"[{}] {}. {}".format( section, From 0681c0c74ed170ef9564976bd6fb23228b84fc28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Mon, 24 Jan 2022 13:27:18 +0100 Subject: [PATCH 061/120] removed an slow unused last case search in tests --- tomatic/claims_test.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index 251aa44d0..6e0bf32ac 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -179,7 +179,6 @@ def test_createAtcCase_procedente(self): claims = Claims(self.erp) case_id = claims.create_atc_case(case) - last_case_id = self.erp.GiscedataAtc.search()[0] self.assertAtcCase(case_id, """ cups_id: ...M0F date: '2021-11-11 15:13:39.998' @@ -201,7 +200,6 @@ def test_createAtcCase_improcedente(self): claims = Claims(self.erp) case_id = claims.create_atc_case(case) - last_case_id = self.erp.GiscedataAtc.search()[0] self.assertAtcCase(case_id, """ cups_id: ...M0F date: '2021-11-11 15:13:39.998' @@ -223,7 +221,6 @@ def test_createAtcCase_noSolution(self): claims = Claims(self.erp) case_id = claims.create_atc_case(case) - last_case_id = self.erp.GiscedataAtc.search()[0] self.assertAtcCase(case_id, """ cups_id: ...M0F date: '2021-11-11 15:13:39.998' @@ -245,7 +242,6 @@ def test_createAtcCase_unsolved(self): claims = Claims(self.erp) case_id = claims.create_atc_case(case) - last_case_id = self.erp.GiscedataAtc.search()[0] self.assertAtcCase(case_id, """ cups_id: ...M0F date: '2021-11-11 15:13:39.998' From 9f67c654e02dcefb0e7a50a4a6e48ebc1714097f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Mon, 24 Jan 2022 18:52:47 +0100 Subject: [PATCH 062/120] migration script subtypes-as-categories --- .../202201-subtypes-as-categories.py | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100755 scripts/migrations/202201-subtypes-as-categories.py diff --git a/scripts/migrations/202201-subtypes-as-categories.py b/scripts/migrations/202201-subtypes-as-categories.py new file mode 100755 index 000000000..cb7dcbe77 --- /dev/null +++ b/scripts/migrations/202201-subtypes-as-categories.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python + +from erppeek import Client +import dbconfig +from yamlns import namespace as ns +from erppeek_wst import ClientWST +erp = ClientWST(**dbconfig.erppeek) + +def loadData(erp, context): + category_ids = erp.CrmCaseCateg.search([]) + context.categories = [ + ns(c) for c in sorted( + erp.CrmCaseCateg.read(category_ids, []), + key=lambda x: x['name'] + ) + ] + + subtypes_ids = erp.GiscedataSubtipusReclamacio.search([]) + context.subtypes = [ + ns(t) for t in sorted( + erp.GiscedataSubtipusReclamacio.read(subtypes_ids, []), + key=lambda x: x['name'] + ) + ] + +def apply(): + # Fix double '][' in INFOENERGIA name + erp.CrmCaseCateg.write(91, dict( + name='[INFOENERGIA] Consulta sobre els seus informes', + )) + + # Add category codes to existing categories + for id, code in ns.loads("""\ + 83: 'IN01' # [INFO] Dubtes informació general (com fer-me soci, com omplir) + 85: 'IN02' # [INFO] Sóc Soci i vull Convidar. No sóc Soci i vull contractar + 86: 'IN03' # [INFO] Bo social (com es demana, qui hi pot tenir accés, etc) + 87: 'IN04' # [INFO] No tinc llum, què faig? + 88: 'OV01' # [OV] Demanen canvis que els hem de dirigir a l'OV + 89: 'OV02' # [OV] Problemes amb l'accés a OV (contrassenya, usuari, activació + 90: 'OV03' # [OV] Consultes sobre les seccions (infoenergia i corbes, GkWh, i + 91: 'IE01' # [INFOENERGIA] Consulta sobre els seus informes + 92: 'FA01' # [FACTURA] Dubtes informació sobre les seves factures + 93: 'FA02' # [FACTURA]Dubtes informació sobre les lectures - vol donar lectur + 94: 'FA03' # [FACTURA] Info comptadors, lloguer, canvi a telegestió, trifàsic + 95: 'CB01' # [COBRAMENTS] Dubtes informació amb factures impagades i com fer + 96: 'CB02' # [COBRAMENTS] Informació sobre el tall de subministrament (com to + 97: 'CT01' # [CONTRACTES] Tot tipus de consultes referents a altes d'un nou p + 98: 'CT02' # [CONTRACTES] Tot tipus de consultes referents a baixes d’un punt + 99: 'CT03' # [CONTRACTES] Informació procés contractació, endarreriments, reb + 100: 'CT04' # [CONTRACTES] Informació sobre possible canvi de comer fraudulent + 101: 'CT05' # [CONTRACTES] Quina potència necessito? Info sobre nova tarifa + 102: 'CT06' # [CONTRACTES]Canvi de Titular - Canvi de Pagador. Com es fa, on, + 103: 'CT07' # [CONTRACTES] Tràmits sobre l’autoconsum (com es fa, documentació + 104: 'EE01' # [ENTITATS I EMPRESES] Informació com contractar administradors d + 105: 'EE02' # [ENTITATS I EMPRESES] Informació sobre tarifa 3.X TD i 6.X TD + 106: 'PR01' # [PROJECTES - GENERACIÓ] Informació sobre les nostres plantes + 107: 'AU01' # [AUTOPRODUCCIÓ] Informació sobre les compres col·lectives, com i + 108: 'AP01' # [APORTACIONS - GKWH] Informació sobre les seves aportacions al G + 109: 'CO01' # [COMUNICACIÓ] Comentar noticies blog, campanyes, newsletter, mai + 110: 'PA01' # [GL - PARTICIPA - AULA POPULAR] Info sobre assemblea, sobre el P + """).items(): + print("updating {id}: code {code}".format(id=id, code=code)) + erp.CrmCaseCateg.write(id, dict( + categ_code=code, + )) + + # Create a new category for each claim subtypes + for sub in context.subtypes: + erp.CrmCaseCateg.create(dict( + name = sub.desc, + section_id = sub.default_section[0] if sub.default_section else False, + categ_code = "R"+sub.name, + )) + + +try: + erp.begin() + context = ns() + loadData(erp, context) + context.dump('content-before.yaml') + apply() + loadData(erp, context) + context.dump('content-after.yaml') +except: + erp.rollback() + raise +finally: + erp.rollback() + + + + From 40979d6461220912a9c51920c503882e5e9c4410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Mon, 24 Jan 2022 18:53:32 +0100 Subject: [PATCH 063/120] subtypes-as-categories: commented out commit --- scripts/migrations/202201-subtypes-as-categories.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/migrations/202201-subtypes-as-categories.py b/scripts/migrations/202201-subtypes-as-categories.py index cb7dcbe77..eed37bbaa 100755 --- a/scripts/migrations/202201-subtypes-as-categories.py +++ b/scripts/migrations/202201-subtypes-as-categories.py @@ -86,6 +86,7 @@ def apply(): raise finally: erp.rollback() + #erp.commit() From 9889ad7b3da89e37b4097665af8ab47a8a76f028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Mon, 24 Jan 2022 19:49:29 +0100 Subject: [PATCH 064/120] questionnaire: listamotius: model-view split --- tomatic/static/components/questionnaire.js | 32 ++++++++++------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/tomatic/static/components/questionnaire.js b/tomatic/static/components/questionnaire.js index be9c29cef..c8a3b20aa 100644 --- a/tomatic/static/components/questionnaire.js +++ b/tomatic/static/components/questionnaire.js @@ -72,32 +72,28 @@ var saveLogCalls = function(phone, user, claim, contract, partner) { }); } - -var llistaMotius = function() { - const all=true; +var filteredCaseCategories = function(categoriesFilter) { + var call_reasons = CallInfo.call_reasons; function contains(value) { - var contains = value.toLowerCase().includes(reason_filter.toLowerCase()); + var contains = value.toLowerCase().includes(categoriesFilter.toLowerCase()); return contains; } var list_reasons = [].concat( call_reasons.infos, - all ? call_reasons.general : [] + call_reasons.general, ); - if (reason_filter !== "") { - var filtered_regular = list_reasons.filter(contains); - if (all) { - var filtered_extras = call_reasons.extras.filter(contains); - var extras = CallInfo.getExtras(filtered_extras); - var filtered = filtered_regular.concat(extras); - } - else { - var filtered = filtered_regular - } - } - else { - var filtered = list_reasons; + if (reason_filter === "") { + return list_reasons } + var filtered_regular = list_reasons.filter(contains); + var filtered_extras = call_reasons.extras.filter(contains); + var extras = CallInfo.getExtras(filtered_extras); + return filtered_regular.concat(extras); +} + +var llistaMotius = function() { + var filtered = filteredCaseCategories(reason_filter) var disabled = (CallInfo.savingAnnotation || CallInfo.call.date === "" ); From 214b2063460e0bc65bbec6a6f024c0bfd0230c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Mon, 24 Jan 2022 20:18:27 +0100 Subject: [PATCH 065/120] claims documentation for further reference --- doc/claims.md | 104 +++++++++++++++++++++++++++++++++++++++++++++++++ doc/claims.png | Bin 0 -> 47538 bytes doc/claims.py | 55 ++++++++++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 doc/claims.md create mode 100644 doc/claims.png create mode 100644 doc/claims.py diff --git a/doc/claims.md b/doc/claims.md new file mode 100644 index 000000000..d41229bce --- /dev/null +++ b/doc/claims.md @@ -0,0 +1,104 @@ +Hi ha quatre aspectes que cal quadrar perque estan col·lissionant tota l'estona: + +- El model de negoci que volem implementar +- El model que implementa, de fet l'ERP +- El contingut de les taules mestres de l'ERP (Categories, Seccions, Subtipus...) +- El que tenim implementat al Callinfo + +El model de l'ERP: + +![](claims.png) + +CrmCase: La base del cas, sigui o no reclamació. Tambe es fa servir per casos ATR. + section_id -> CrmCaseSection.id + name -> case.reason.split('.',1)[-1] + categ_id -> CrmCaseCateg + categ_ids -> CrmCaseCateg + +GiscedataAtc: se añade a CrmCase si es una reclamacion + subtipus_id -> GiscedataSubtipusReclamacio desc ~= case.reason.split('.',1)[-1] + +GiscedataSubtipusReclamacio + default_section -> CrmCaseSection + name '003' + desc -> case.reason.split('.',1)[-1] + +CrmCaseCateg + complete_name: "grandparent / parent / child" + name: "[ENTITATS I EMPRESES] Informació com contractar administradors d" (el [tag] no te a veure amb seccions, hauria?) + categ_code (buit) + section_id -> CrmCaseSection + - Les que comencen per `[` tenen section "Helpdesk" + - Hi ha una que comença per `][` + Se usan para listar, pero despues no se enlazan! + +CrmCaseSection + name ~= case.claimSection + code: + Uppercase name, code starts with ATC, parent hierarchy from "Atencio al Client" + ATC: Atenció al Client + ATCI: INFO + ATCF: FACTURES + ATCCB: COBRAMENTS + ATCR: RECLAMACIONS + ATCC: CONTRACTES + ATCCA: CONTRACTES - A + ATCCB: CONTRACTES - B + ATCCC: CONTRACTES - C + ATCCM: CONTRACTES - M + +A revisar: + +- Si replicamos los Subtipus como categories, la lista de categoria tambien incluira los subtipus +- Si no tiene la misma estructura la lista de info que de claims + Info: [CONSULTA] [ENTITATS I EMPRESES] Informació com contractar administradors d + Claim: [FACTURA] 008. DISCONFORMIDAD CON CONCEPTOS FACTURADOS +- Porque el nombre de la `default_section` se parte por la '/' para generar la lista + + +Proposta + +- Omplir el `categ_code` amb un codi, que coincidira amb el name dels subtipus quan son reclamacions, i sera un codi nou en els infos. +- El callinfo fara les cerques de categories y subtipus per aquest codi, no pas per la descripció +- Quines son les categories i subtipus bons (produccío? testing?) + + + + +Copy GiscedataSubtipusReclamacio -> CmrmCaseCateg + default_section -> section_id + na + + + +select + cat.name, + cat.categ_code, + sec.name, + sec.code +from crm_case_categ as cat +left join crm_case_section as sec +on sec.id = cat.section_id ; + + +select + sub.type, + sub.name, + sub.desc, + sub.default_section, + sec.code, + sec.name +from giscedata_subtipus_reclamacio as sub +left join crm_case_section as sec +on sec.id = sub.default_section +where sec.active +; + + + + + + + + + diff --git a/doc/claims.png b/doc/claims.png new file mode 100644 index 0000000000000000000000000000000000000000..c6ad287f8a8fadfad2c61052ed1c6ef8391812d5 GIT binary patch literal 47538 zcmce;by(Hix;8oi0V!!vkWOhyX~ap1lynP7mvomXOj^3TLAqN+x?5W5?#?rCt#|Fc z*IDmA=lcHm@RB*N`5SZec%J*Y?`IGoFDrq8N`wl5Kro~vp>H7&_+s$C{UJQ~B&7JX z3%nrdOG`i@clZC&nsOr{5ORnV^rez>()OIO7T(%4(!Lu(%;#)hPVDx;j*oB_?_NT& zX|VC6Ge`pz`T_!&%cwhS^}`Tl^f`wO&KfC+V0rHlh|`d8+i|ea0!IkG)E_6GAy|rS z?)ETkpWLnZ@~^n;s;iy$a18dbG4{t^N4BOV!lD=R1!3+p=aWy?uPAPk7yedb_O1eh#9TMr$KL zAXs`z-msr)Exg2gQ9oHP@;7^H0~1cZ;tQ!1m6?MHv3-o6mbc)z5dEd?hg7n;W-oE& z3E~WD0}=#6@2>z?@5&V}*&vsy(Gr^dKt|EH$7MT=zrt+9JG4X8!B6k;+MHXVDv2v; zv!`u~W4(sxlC11#jaXAo#3$kAi?ou@#WBTVHF_0iKbBRh;)Z?}2jnT)&v+FLN;a^p z^1(CvIyOmM?AFpoNLtDtuP^t6Av;+cGvS#$QRjDOo7@|_nS5*aNPRd&`$(v=hMU0b zVWkWo=Ht8ZR{kj&*Gw=G%A$B_S=5y5?A%r+{DV~47Tv#XpvRMEc8}scZd=xW^koq< zJ{`+aZMW35?=$NryUS{9Eb|!ZrP8gFyE$7!8#vL%y`;amSQ&3xJiBx)3Z&29tP9u1 zRan>0eI}e!e~oo1r(vXjOrhI^7Nq6J5<>q~O&ja-^U)77-H_{N+X~{cm%^6zHTj8h`S3J|!mQ z$vB$$F?0Wu_-7wK6i3Xn)THc@l(W7jZMr%moG2^E_4QS8XtNCHnrQXWt@|-DQf|wX z=IYzwuwz3m>|ShoVlJaOW2JO>wt(5AI~`19*&n2}wbF2MMvPKt2pu!_(7GHPSTU#i z_;Dm)?L1u~aTBReB6wXYV&-KlEAcQt7`P4$p3_3jE#o>ajxJ>Wm8NDxv9hbHtMc9I zfiu+1)pfk=rHo97BxSxpyY<|s;)=be>bz~)HC1)#F+^=_=%Y3VGL&Vt`rf)T4r@D! z{>`N>t{-+`vPiB3)tSa6n88JA>QM;^GSs|h0YX&SqcvXG3hwUinPJ2bhz1oDj_8GP zaTDL15cdApnCJ*i>3a4?p)G^D9lqb4m`OP$C>%*1zow}a(mxrha}N&&zzhpnp1%uz znsc}It37U+N#V^9pVR4e&w*p{%pwf-V09G6gyEAe3PKkO6l zW(sZ-hK2iPV#!_Xlx=1QvRj0>Qz{pac9R^nPuHR!K96@}Nm!d5hx!j==x`7v z;tU8j%62oFP}O!5Y%pmyhNSV5p1Gx#ZA;nW+^dC%%e^&OXlvlcDFd!XyAZ?PVIn6A zmzYP_J&J{erOO?!*`s^6!J7Nz+g`;=>+^12vGxnoG791Cu=f{x4d~p|lq2>FrIWdK zo4fI3mS`?tqO{e%Tr6*v2rV_srPLY*P+2jVyDm|=C1#?uF`i5=L+LsyXwN|t+Z=#{p6=& z^%#7y+ZV=4tc(T}sU-$ROWPF}>*X(%i-ZlNNB5FuIgKsuNOwX$5s7A2&mqaGHPXGN z&bJbYz;1F1SG|+%5N-Agl|r6TXr5saC|L=9Y3|SX5CSoK_j9wN-~RgipgWRInibB( zkXANH5()In-md6Q&1255j^#6-V2agyq6u@th>~UKJS3kXb ze_*t-K(YH}#(4}u$|4TD@ zDz1Hr&vJQpD)keXaBp1p`BR7YoH$1(aUO;+H5_lQvHz|ZZ}1Frk5JBuV~HvSX52(d z=^5&>cHE5e;H<%35Z^?h9B*rRiBpKYyi-KfPXgIpFNxNOu-$^3dGhTEHjX z;xS%I#<`*q?%&N)F-ZPz?#Cp+tgaoai*>VR&WUE3&F1ae^u!^OrN_a5AMJ~KPp3io zL;~?=@Oo2F2j;p(2J;Z7iLt>RpT~+%AuB2j_BY-{X*;geki4yytdA~G%|t;+LeAs0 zymCbbal@*IiM1uu7?w$*Jt^C5p|4YU_7yWsu=&U6z-wJaa?Z`4UW=QfPM(fMaht|$ zjFruSzB&1-7V9=yrPWAPGW8e-2MsK5e-IjeA1@4mPJ3LAT{>vFDPJuOd`;j?%Pf;m z)_dIHayGxI?4ROak~&+GTpTl08##JxsZf$n{X5KBXx%)$Fx!0C^5?eT^E#QmgxWhP zQ!A=YHi4mAdnHYXDf1K?}($GayC(= z8t>NAp3fIPwXy2C;V_?l6OeR`qaI`EYy7b5{qZH*LHvbF#<{exZ0Tyg)7_=(%adCQ zj!IA{!kjbF{&4Bh=KIz1(>`I-I$o(WxuVBLq6r=`605N#tRV|4`XU0-XZ>piqqcSt zyT~QBRa`t2IBzhlYE5Dcr5}Y+Wt90dO1o6FZmT}`SqLQ>X{&EVBD=)k!6lk131uiS z@P)B5!@A54`a4t&^#m~$IUX*FXupE2!sVDAD+cIIEXwxI~4A~lnj@0F5tE#o4{z7 zq`xE(UUV&0u}~HNfeZ)r?a#I;WofgUkJ!kACa{r_7)0yPoZ_el6rjMlRyf ztakYDOO>UpvXc7s>(>$K<}6-s@ms_UI*C+RabJWzp~(6e89P|xcyGS1 z7eO=BFp9N{ui7PbDi+OeAK1r)f(?ZU+$2xDoDA%3MzQM>5CNK%P1<+ zo@5Yg+AL8b4U@>q$yL2s$;-(Rrw&(2S+ssV`Sdnx^PHKTgM%g_D+`ttGk!T>pW5)Q zGUvtnEmq6`80pSD(Q0E?jd`0tIs{^E>WYUI7r%jvpOd{ZQUCX7VQuL-`jKPBf8m1q zuAjU~lJE30q?PD#R;2b~Vqzwisq(hVCXhHdI1DZ2d%p47?Tl}n26!7+eT|yc7>WLU zqE1%m9lE@pFq*1F(ju0hZweK#vx`K+9DTo;k#Dp;ms+Dk&(K_Y(i!|Dh_>e`Du4lr zU~?ojHok#n45ai&Xq-FGT?uS`#*Qc#E`h?jXYGas){1HQ-2GsAX=%Uu2|kHZDA?FM z#~yB4;qJ>@U=K*@2;<2fNmQjo&E^SN7}G7fY3zDSX>0E^Nf(HU(VwrAY`fBvnM3yS z^EA<$7EeLhRiwh_bE|jN1uaI$m`A5HlJi>1PiyQpO2R_uiq5r86^R4a6P8ky+O3D< zIZ6s+@js{5QPVQ3vewj#CuKd#R>c}KVY_IJ-<#W>PT)R8{dhO#g7~s-46Qc_ z{lZc^mb4ZpHbGTs|8hlG&y5hH;@1;&9l~T)W91XPzVbS^S=bmYvWhj;rRn4F)8H?( zPW>lx47?_;5)K`GhBEs%SxK{%wWWcK4?PQQ5MLJUy}3x=a~h@8f1GUNuRB@RJ;P>+ z1xZ7S&w%v49dNX?2X+t415l z?o4mDfs+>4ncgtn|16$Nl55p`xKC$H`MYI+Tg~Q|(uU?9N)FP5g?#T&ed<{O@QelS zNIti@6b_XR)<;;8t|jDoZxbB1e$|Xh99OgB9FB9@_t!r#+VSMQD7>&+TTZN--4qQt#w>40pEW{&Z| zfqrhf@~HZh3_Tbi7P#2Hj>Z38>I{=h5m0vx7-_iEKAYb{C75$`)XN_urA;#|C()fM{J(ktM7*dx zu7iij_<~;$uF5=xd(Q$zGZf>xgF6SfK+R6G=s9h|GVk z(?01FJZ;W1Xu6ETWx8^mgz>2`!cHc6)VMumLgJ4x#zBO?ANm0YQPji!vC;l%l+kWX zv?MhXV>tbi5{!1TW7rLV=b}&snU;xrH0MM(sXQhdZ;k-*R*{L8#R5%z(|jKcZy|m> zFSdki&pk|FLR48%=9Jug_-&kUC9H}Of9_Owi({lXs4i5bH%)s_p_#upfFU(J_J4pg zH`)IKaQ5kcfHUZ3fa^GhS)x7h;*yiLk{zsheYiWC40CsWK#hKgz1;EqUGYNxRY~uA zx0!^unsYZ3ccfrOIYW*SP!bN`iwX_SI>_udsq!w)MqL?C)W+lRUeJ^?^&$;oed%65 zAzb7*I)Wa=dM3T=SKPZjamerN{oFT1YkMeNJZv%29mby*8Q(k2ahJ8SNm5*4Q&XF$+DV!)=~=cUl~wpG(Ygc! zp~M$GrLgWcJC&_R#h$2R~t8f@f2&Lf1b9iuXT5M zf7MxE#?PJ9m7gm^$?)Q)Kj}_z@^E^!#r=&FE{S$f3Yoi?#mz%*qqbOKAqvVK{)?TF zQl=&;{!kCWkq)TDWJYf1<{k5P;=(aio_yxZrs^ia$(>ZA6sD$rVZGRhsg#svs)i@# zD3$%LZdUUrVl|hE_ZzGC^>TwkB>D}Q_%2aDdMxb6^pr%)yH?~?L;Is&7Pru*v9VpA zc44NPYc#zcov0>`zP>s0MM}UxeNC6L3BrlSjkkJUmUC z7;pR@?abeIM$^|aIeFo{qv{FQbOK@BM6c7<9uJOfFD!5Hvi3MNdCRnVEu%Mov9Co1 zGh)s0iv1W3Fq1(!51sYWp*$U5)xjwj5*G^T&GQ`&FO|&M>b`7(6@&heyWu%F0;{Rl4hQpV4D3J--S}EjMb0 zs8Xh0+Dl&SvbPAU=trlDrvF7Y2nB_g*}Vw=_5&K5Cr{lIInT_@T+}TgKV?vaVPs^~ zT=&Fane9dxnYDx3I6(j7L|w(2YG{q!908qqNyvmqOI4FUIJq)(Z7A?`tgN^hc@@SL zSp-5lXF`LndY`hk2N|*T+0V2%2kQz09;H%HGSJgIS0JX#ODbt;y@A?W*I{sM*1Vz2 zJy?>-KVFT7iEhX&@tmld3$vC-!Jf{|odjfvzRIR0$wan!FXJG`eE`BXZsv@-@Tb;X%3* zxl^^8p@3ZOp>g%Zx3U(q?gt9>wYB6&3g@RZ4SUT~qOAPK896mIY*_$Cc58^1v-JVnYXFvV}e+wEeJn&?^w@0mO zZ7Xi-#g&Cny@kA(@hNdi8B=e$Seb-}zr9svk>R@)Ho$o`78r44oUt5kE_ADPO%6TZ zc*o!4H1m0F#2Ed@|LVrM@-qqEV0@iQi%EA7-c!!qyH>hDhrf|~rB=;(Gtez#*KxWx zJTI9?!D%*Q`Rh}=TLv`3)wRCH1r`wV`>K%Ayd`dbY87r|uciO6#k{cSx965CX1Zr> z+&xb5i{|t9`UkyIaj1qO_QLHA#dZ*Lj*%0NMq!-~ZK%CdIJb;L(SLP98 z=3Wc@T#vM)c`J>>qT?sRn7TL8*eTVq$Pd@&2s0>oHYMt3$<>=*+HO=c)YKYpL}PW-7UdTbr`Z91fd* z+$*S;K0D^*i@shbcB&=ufjWtKlU!w%ItZ#EuhYds?wKF&jDI75h90L2@u71#yP-}x zfamm2Gww_Mr5}Lf`*L)Qv3UIKMbrn=x#im(jLJ_PBo=}M$b#Xn5V`#I>N}0pa7XPv zhX)!+JFa1eb2c*6`9Z*4EVO~6&d8}I{wT})LAFSg=A;cjphv&7?i83K@va74hnl~jGd)9eH8t54v z8S8mA)VULIJSUvUwY$)HD+{(#$o!7s%Q4g6CyNEi5wf1MFO&PUCQ>fwm;u1P&Bt$h zBUv$c*mYfB=&{Z-`^Myb3tz$M8baCEzjJ_QzF(e9LuEa?t6DG#CfaCs(7lpUbicS7 zeD<9#)y@!a{QM1DAoL;Fhb|f3zk`1ju>SgPFb_|eZ{7Oq$)7Oa>Dj{sBgyMB{|a+X z{p0TV9tooIyRVy4P|KFawi7RIeI)Q&t=O>1Ve^KX?>N__@ZEv<*l-`N4 zhR#G3!~&*Wb1Bms#Q7%=u;p3)fYUAx)imQ4&+k26XkWj+35bY@U?6nnv-qE)=yaNOmz(We9C8jd(d0Ukxn`HF$aHwP^xyNKXSMp}E=f1PucNgEAn? zj9cV1nTpZ4_PCmxXY8%PV?RAUmS!nxf*^CwjNDvRZS;S}$L$3>&H8~P&_g+)*~O2hTTSwqai|WIm`i);ck(^?~cdYCq89dj74rvmX`7NYSFKs z{PiEn|AtDB-g*WHZ)b-bIKG=V4@YXd2|yz>JN znp)##e&LxmV58EUT4jh#rK(c?c)wd`l47TdOCt0Nsw6K@?$jW=)S38pqn{D`^N93c z9!P1`h;@K{y^1>q20QN%lo*(OTxrFrh~+@L?a+=Sn*JrPwl)s7K5t8GG-e&o?gYA> zrZg{uCr`Y-`Bml_9k*5UIlZ=))|U%<_p69Grxh#JQI9sA)Pej4W6w(F<(F?A^ zr~9q^EB#?7Z>FpI6SNC=5)NYH)P9LrjDnxvJliU2h%f9+k@IOzi5I8LkNUG>jBxx$aSx+$INqcE=!zd{mw3UZd@-)BD8N?2H@pdHxyd$B|=xI-dR~Gc6KsROx<0 zd6qRkxCDCvI8p^Q^nrq#{8@Iymdpr@{`vgYYfUFYSTYYyeuW@pd;fz?SIe(B>kK1H z6>wo^oV;*{WOD;0loIvf!(a8l683VC`xmGHXs9SW zvTAxQF=;s&IjddNAOY;OQSPTq05l;(?(YNP#lmEZubn@fId+Kp#DrJ1A=}-^&UtC8 zG^OCr{p8wv?k6&nb>w}ZOVSw0JO@bxlEUygT(V3hVgk~R#kO{CiNJ>wF*q8F zCtYm2$VUN`1R7C;U5cqVQ0ToMFj{wc`s`~F2?j>G-c|FiUA2Ep0X@_#_7zI(gv{>3 zyoj{PZ0m%9%}}IkinLOGC@GqL80$f0#mFQcV50v-EuZd%fuV9K;V)?lVO084U(^YL zZ->U$mPXn?yF7dDqszY;0R+CDaVa|C-85Mv7W2XD$e6@Pdw^B+XtSn_UEX`PBXz$V zjasii@;lCiDS9 zXW<9E{P4i1yWX$!S#0jInv_KfS8ubNo+n`^=~lHrw}u-o)uyb*Ham{n5>^P4wC)Mm z-jva~;nVV_ABrFcUVV(b7v-ZuW|2#{d6Rq1ttS;lO)%{8Ynb%iRGqsaR9>F%;HmJj ziDHxQ1q@qI%9fx0oCaI$0a73gsjh)olFRhmRPx{iaVO!em>B>6o9Nu0DoHIhLt|uA z%*e{h%CT3bVCUrYH~Es7sB=E$;yn~VBCkwn-UaD$+eeh4qBr9~Y&s?>GQ;bi{Vc zB3X|};T6eFR!$D~L0`Yjd){IsS3tAUFyI9oX2pW2f}xr5{!CFxcnOibpV;_XL zg(lTKH<>fZA>VxS)130ZM+Dl7|B49y8!dVFPg)X(lJeie35^yD#%W%=Rfm)EjD3#S zA6YA#E3|8IuZsSPC+rGhvvpfY&mPr-Gi}0BKBxN2XTiAF*C4_!Fz{$p|CQ%YvhKqL zrzaMY(5#JSy%M>#u{jRmX@W!YXAPy~N8RJP9vvR30ye!k5)PCjL=Ad_$DxQVtVy-I zeZI@HACqE-C4^HOg{BPVMCnMe75+mUQBdU9<4E9KB6vg2%OiIT{6lBzt~B?_&x50u zzV{x&o0%MXH>=^|yl`kj)3(1ATy?B^+A7R*c4`Yp_fx>~^hmotXbD%v%39#9GadMO zD>zMz!u3hqvH>Hh)LAY{@@><9w%jZp9U>8)TQ!I&H+)3q2Z*i&+9bJOQC+EvHU6FE z8!%Ak{!oyh)c-Nx$GR%BIDcVaqTgvI`$r%nN@jC;)O0)G;FyTnUS4)}?y|nyQjrK- zto5czaojoPJlF7>xM@^PZaz|6;dpo~jA6Q8gfQ?M6&~VE^f;!dSz_Q|sgq6)1o;or zqT~O`tbNda2=Ole7UB<}$6pCHiYxRVz9;4@mW-d(v0wrR%UhY`kH?sDhSC=ZrXf1F z=Z|6b{-dx?|Bs}ZLc{4_pq0e?^qPaim0ri4gyR<$fNv^}p=WbBTwX0fNSktYbFmG<9|J$UkW**gEXzx0cQfst{k{=sf^ zRMZDBD#)0`&5jr@ovg(zpGInJ?F*sQg?vT2Vzl3CdU|^H_V$y-+WmcfwZ)%d>m4D) z2*{6??{k)D4#n|G%gYZ>Pge_~CYD!vW6yTyMnMWkb@l3jlIyydqN1XN#M*R~IT8}m zT!T|hW##+Fer>KukDk6T86tlKSP*$`^xkr<$)Rbem>4fVe{56~8+wKLB#W-DZZYHH z&qCD_KBwKUxw*07;lp)yYwbaVcU!u5I1e9a-Dg=r;~3LE45kPk?eEhwG8)`d@8gB4 zgoK24+IXEz%*?_!$BHsC{m7W4wG9n}slqKDSEr!Lfu5eldQM;>@#ORR`rfFjPR`EG z9-{a%_2%T{kiPify;WaZV=+};Sy>56@BS>KqM-P_aCdtaO2R)~V=e4;d!3aN$74Gs*9NZ}Y{3$Ckk5f%OD=ZEB9^~`rr=!%J&np#Mx`MRj9yL+m| z>kd=+`b&8DbJLM5d`1=fo$2T;BA}*VizDLs8cxM?7>h%pW3`Tt5D=7`FIE$mmzUf8 zF=JcCMn@At%YcdoU9;Vpt~#4{v%Wmu08_qHEeV0(e^P*JS1Z$RzdYHZ!RekUWMh*z zJl&r9{rh)WS=r{MwO0LG4UNsECA;JGVZ+`S%b%q_g@@n(;ev+K($c;S4wg(j6?y#l z%i)~ULX-2s;`(qVS4=9OL(js3SL;W_E^ylVF%?AMz_buBsmU+h*=LtAHP|ePqkpqs zAJW?#%Qv4azUPHB9^>J08ummD3g69wWQ4Y9&MPJ8nq6g~`lO=t7<4qH&l;gM`<;mh?*a0cQM6Is4p5t#uU9NaVI$TwFZenK>JlAlsR# z3F}qt-4fN{wzjqg8v_NGR+2hG*Xst~6hv-wt-5N6ZYP`gz2zz@Dq4giiyS9_wY$6P^XEqrfc-tNE+1)VX%8>lfuSiz zGo=v^BPGGXu>~W`SInKLjRYv2{=q-KKQc0cvsR67^0QRWcp!<_?R2}7%qtiefwT|j z9dAp2{D8i}ldnyGu{h%VGdH&@ zG(r}rghWF_!`jMfqYh5?aw`B)z!`J7q(Vb zyWk8*Mn?WA^ZC9Eu$E9b*-GmLApwELtgIjH?KIDx1^k&@94MUFYbmMcJUnalsQ8A4 zh7LQ^T%=)Zi;IVwb z=;^e0YOATi=)W;@a8#6*vXPLG7#pwU%B9-gPcs^rX0{dF2_`0{S{CSdd_sawy}gvK zuGjhgZy@ab2Gu|e{Xv;$&z?bn9W{KXuWx&CXb9^1-R3Xsc^!A(*C--j2f$eNFWe0b z42sdU(?EGP*ZnzH$bqp<7tO%Y-}%_c9W{As)B-g zZ>|75L57G({dSYq`*7inGcq~Z;(VYVMk**C{MZJp(fyF+#len>OiBu-Py19^RRxUA z9B@Q>ZGK`B5_Vuf-XmaEAu7+ z*jGqaTt7dWn3&w#n(d!uWbXT|;N-X8@9C$CYxUkL8P{+Rz#M^tpYR+#bDtXw0e_yh zF~m?EBshEXfhznYsYk}g@lij6GCj;{rSCw(w3aHI3>H3qmB*E1aUsO}5*#m3jQ#oZ z-}QkL)}TNntqjPhHX#Bjbu|8U@PXkeDf2lp#Go($@gOUwT?C|yc@v-l-*OKF(>{yh z$p;@F+xOvvk7FK!k4HvFLqbC8is1_++4r7a%{$GCBvyCu!TW?tU0GH8ihERkSh^i* z(Z#3lbg<0sY>c-}7?tI*TR0%Lcp`53D~7OtmKAVfps8;t@HCWCyfyZcg#IpTh?y!E z3(ibE{hls7+v{gbMWkfcMmY`1qh403R`E`HR;q$_VNFTyR0aP`qx+S%)pAM(qllQD z_4-AD+=Sp=4i?@U_?x7pr%<>R`40x4ul$7t*6-|=S5jB&&n&I;veQk6s-;_Og6`aS z-{%)499RD%oes+DheGshOu}Ce$yzSF4o>RF?JM=Y7Hg^F{cpF4)+7zHI|w4uzVvC3 zjg5^zE6e`wZi@R)3SD-K`@~97qLB%v+UbdjOg>`0RYS^hPRhy7P%yr2@%r`cUB2AR z%}fg)wjh7wp6#$Zz{Cm&m zzok}i|As%}2is4f&wFbA;!skqT^{C9WZmL8dzJ-J<%bzR!o!JLvu~v=;@7Y~%KA-np3n*M? zlhLUijWu|AG^N$A{*(XadX)W}1R!hOTh2FZ?#8b#(1vwXg~IE5t@B3`$0j^)kabwW z@UD~LLP)`yq<^yK!p>YIgiZQ#-O)~6#<+wG7jc5RqjRioO*DS?82=W z^x)tCDn_Zm5}p$f5PEC{A|31MNRPFyo+m6#Mg166V}PWO3k|Zy0WjCk%-UI^`^3Ya zbmh{`RCO}<dOz0{ahNZ#JAOKP45)Czr)IT z%o!V+(-4M_+OFDOZ<)2+A|QLm+(#+E>IlSPsjzPM4{-kW!xr(&wXMF9)Z_S^R!@ zd0D<`U%jVm3yzqyCR3VIqo@XCuaCM~c0L)AJ#`rX=_ffUhZ}vWd&_kv+c&D}h>O9U zs*Z!}6c#*FE&5j4Q%eitMA(D2eXB<{8M8^t$i~f173Ss)f|mR3tSr;eRZ?Ls814#T z0LS9@Yk`?U?2W-Xc?aisH;E&uyEX*Y!x5_36NSC1Q#}JmcaLwce%|@hRW<3N2p=26 zDh0yck5BzEDQ}1$$a?~N;$2mBk|UE?TbyxXZh`ksJ%$;7PN>{$Rn33E=u*So0mJgu z5z8sdd}@i{hYM4kc3L0BY->@bIHIod>bN2&EcE zsg{!&_(4ySkg9Wd!Jy($MHNIr9MzJ3*nC4*p%1)EZ-ga1xvyd>D1>~orp{a zmFBs*t&{APyK7CG$H}P-sV~#+Odt0ComTSQvyB6g%hmL!d3#?5EGw(fzdp^`ZPd0A zIN3HaGE#9!Lf~$BzHv8YT#6BgofbaeFR3J`w{+2-46iv-0cCx=uZ^wZwoR>Mg%-KRp3_pF^sYm8yhCL$m> zT}$<{)t1~+-J-fk6Z^LC1Y+oi`{1yrr{{Z~NQLP!$3qeIC}CfKuAgLhH-ZT0x$1p) zSnfMKbZ`7cpL|;Xvr01fzCk3!pyyEDD;2K)zsScx!$5D6)Yk$X9o` z)H*lw1PxI`eBu#V-wD6Nox>Ash3)BP{G+7;q|;Z%8&RyLM^h^`8Vs--kn(KFe8Pn& zI=b5?eDhpXsaZg;SH#aoI8F|)Y$(Is>EzippH@o5H&tYwQbNeEC`S_|oohC#u>)EOXZEJjm6 z8G{*Ped;TJckEv*L>9}!(kC5&ay_jO8@%SHT9TSBv%DOu)zn#maSH9QE8uyS%AL1x zbMt%jOv@`FJn`guu+8q$BvSlLEY@z*Dp%|NiX7+Zht|X*(?lmCPCKvVioQniYL!d| zn^&F6VF@(R{|izvm^RNiG{$Q5^@9`9V`CsE3`V6-ES2uIkds>op9`*fFm2Q%$g_8u zk@yC~kg)#i0#9##W_YNlo4;v8sQX;ziwW!7P>J&B=#sNx)|LzOa_+I1qAD*uN3AHQ z!_b{ghWo(76!ISU| z#)o@auiy0fGvaVzENh{oWSvXJ#MS=F9ITcx-?e?7--#mNv8EGG&>^B|dg*5G)jt&R z6VF%zah;or%H0W~ubcX>I>((ucXuzL3hC**wZU_~<b7jjHp>0t6o7OgXTsaI=H2@RdvJta-beB= zkHDq`Y@3FL2H*yO%*zi9J{}<d?@Lo{|X5V|)`MK-5{7@{p=HtniSo0;wYpQyiE9deAO22{>ne zc!O*(I%#mBA>*H3sytGX8YkevC=$4vct3J&DJhhHI#=LQr|#7t5k_x%@`lAlebM`l z6M&98KE7uiQ~$@a>Tr@s#;v|2nhljzJw^hCb1O_4&;(KsG`mcAUz;h0v zlB0G#`ZIt(?-W3XG~hs!(J?VN;Ry*ulb^vSUH7e_xBZcOyc>a4YcFS-r1|T?zq+CG z=N$pYfA8A=-_P!c@TVJpJoTS$a3U?dXK!%anf?bQ*x%n@S1d;JEBH*IA9u92{sy)5YvDt>Q0x$wKQkc~_zteQ zxw;-+Tq~Cip8S;ypfOp_L`*`G92GUzrwr$vbU(L>lW8A2!pL;NcAC`S;;It9JgTl_!<$v>%F8020i>#^%}GjhCjn`V^pWkB*KK>lh$k0A|{L01tO>)b9)0 zCGuFOBqk;%CntaYEOvpMjvCg<$ii}aI%8c|fB{KBbs~<^zR>-akwFR2h>4W};F&p$ z-i%9^sU?A1LLh%EC4~hi05HK`mm9f26w~#*$WH0S0DR>yE8lXr!)2*h7VV78OhDDu z6(fq&0qy2ZGT-?0G#?vV=_~(7_oVVkD+>G+fRbq}fUU2wnqzqO%;v*~e=>u4n4rht z_-qbmOUD8=1<WlOxVzU=^;ZPN4rL|#tMK{AjfDm zI){S>eqn$HKsMUo8WpHTd~G1vdaj*@_+^>B+>h0U)izW)szr#vEC!Bd zu}%}8)okr^gAV%cF`oxSY8Q!(zmO)|IWgUxm4W)A6-AD!6({YwT6y+)2OAg7?ZpQD=fxDPD{CDN@2+#T2=_bGjoi)TSLEk?Ixt zs~JTzmRL!cq0~t59p?D8;78PhlanU21neEj}h z6tKM}mOZ(TfNbyP;=-(1{f099gEqE^Ifxes)8*CV<>fCnhovr^*{V2beF)U~hqQ$2 z?=;K|Vyw0^Fn7AuB;hSL1-zU2B&Uu$kcQn%uOaNiv@!_gWw{%S8=FCa8`8VT0Wbm$ zr8k@LAR7zd?#1lR@adUhthZOqoW}f3425dyO}!NLm7$k4bj%bErP=K-$Kw2-jm;at zk?4v#s$@MQkF;7|lAsJ0`Iz(ozSZmQY5|DkR~Hv9uC7ML#ypL3G7g^92Km-2*R=&c zH|l)&(n1Z_Dz6Zay@I+XA~+G>PbUlAghN+)iSMSOQa(KvZ8;xtTg}QpFftD*F7OfN za`@&?xY_aXDUfS_t*@)y?qN-Q)blSkS{j-9F2GIu#bL}aWKG4|?a)9aSuM6|TiDb> z7RSvD(OlFbb;ZJS1?lCex=@&dcH6vLd!XIam9ZL+VocJ{ABU^PrO>asw*5G-A5}oN zf3}TFf6S?H_j9;NX*vt+moa3$C?gqHSHwJ3*dT7O-|fBJHq?DzrY<+Nf}ploLA*rl zFa$EX44vvqe;Ca1RFs)ooHBCnYdT6|#=w$^X4jo?(pW&w0S7q4Wo0hs`+AX%7VA_O zr^cI8NT39+P7}2!hV&0duhisbe~*M#BSB-#LA|Pgon}~nenYaYwHJ4(pvjIV-G#>x zUa;ow(q@^zBy*SBl92C1x;#L^R%m^a;1S13C;!c9tdQAsLw@l2vDa3>!jC|P=_hh@ zD)J@OSq86J&`cI`qZ|{OUmSPD#8+1yDVC4W-Go-^Q=q)2hvsy4ttDq=>t*o-gnGzu z(`HS^2uu!h+NmeN=d}X`I5RU72zJPio&pVh?3`rlMv_}99c?F3j3h*|MNno&O!q8J zO?di1vSZ>{C}Y9BYg)cz;#OhS=64$!{{3+Qmr(H$#lyM6CR~Rn7M@YWf?FuAukY&g z+hF9c>1?mROiS(v>OTno^zmZ=j1pc7%Gs5Sp2f{}9Tixj*pQ>7G(4cj9;LA^;P(ki zi119+OmEOr>BL>l2R|N%hK4IU_jP!B6reRjjr7#V#uFVM`IE_wv!y!!s_42vzp9X};bQyCk_5y|m` zUt3$m1O$#(r#rVdSHkY+rRn`!7m_&GVG>X!CN6Dm#==uaGbYwMcV!Pl?!yXo8o{RK z2_i-18xa1S?~C4>GO$fy?Td%Md(_9wE!*{%QaJ6Xiil+p74S~)w&$GG@Fu4r`%W?1 z1TpgsAM@7n#o*A=;P78QFn9?RAwy!lTPqrzLF0$O8S|hwSHT+~;yWeetCu z*btE^+(+jxr(ELdP*RLU)v>8lAgTSos!)=^Kqsod!NZ#2vR(E&5lbDBb|xsz@{Bvg zofA-Kv}((62}t2RSbrWVtZgG$|K$FE7<A5gk?s!Zlm-!`OG=RL zZUJc!>F(~766pqMknVmbuDze*dEaC27{*u6M8)7NmF~lS{Z?qdpO_*}eV(CUv7$UvZ4# zYMs9MrFW+z_c6oW8Ok%Jpzrs4Q*XdKD>Wm9p?4HrmQ2&isAUT!t{<&PoNu=Vh_o)Y zdJsc^o*a>~VSNF;+W~gxyF3n0cfHWDzKBr+big(BF~wI--)+0#Y{ngHF=*%LX?1y| zg?YtXG&H%Q$JPz)iXduAncH9ouk2FsYNi2S1L>258+%I`EdAJ+b1{z{ ziAtAOgl57xTH>t2g74dsEMjC0x!YSIuQO=I%9GT6B z;}5%v4ojL{Scn9HehrT)s`q1L)Z62Khz1x2t7-1MURK{sP}nC?_^RlS_3QumB^=a;jLpNMmwhB6#^(RywMxPh@8i zG{QjgTmej9Bx&4i0Sv+bjvO2>QW+8jb&9)(2RMsVRa64e$sewc-#2!NNISm+-~LG* zLiv@$YNp)ZAJ)OaK}xFcwdu%DQZ!LEq<{ZPHa)#X*~F)y{QByONw2ZGu~GYu6XBc{ ztk!s-_|V4{mQI*dAm#SFy8=T;CzljW_1k0x;avLZ0l=Jldf;L>jgO0$_XaTfE-o&> zX7TiPC>`MK0OR5GKr+v-zI;ZwD0!OYQ9L0{&35Jt3uQR%T|o)7}&a_|~qD*GfxE zxt$=Cpg-P7Nx?&oxS;*l2UB?*K!ghT0bn^}V_|Kh!Bg@+^+^)wu{^+`!NS6NhC*=E z0Z&dM=$>?7hmnm6W{`Rc()#A+FH!{2{9(Y~dQL?JqNqm04(L=~Z#Sl=Z;+9Zk&u9U z>i0a0JhW7P*Aoy1g41+#WF!}~w2>$mS|#(_H@^Zf=)-+|bhNaa04joR0qno0iQ0(s z^gvNj7+7agv<+#g?9Sorz!08oHt zXJ^Mqkwr;={gLh~0{vQ6rl^dgHx4(FE&z^m9qnYv#i7|MQ$T~_(^qtJy8(aa(~l~C zSD^{Mq$?2h8y|*0Z_Y-{NY0D+194?#Wm(yc`2?BR#E*o3grnMH!Nl0oFGGcl;=;v2Bc#SgJDPR{hd zd3pHjAmF~fT^|{ec{z=EP7_wm|oN_%m<6;>i@)xXqsJZm>Lx8lprw9gRIv_^!eMoyB&+=cp{!Eb5s z?m)^Ps(zy>S`0fHZaLDJyNC}`k(MDz2Ne`BWQ-=R2E``ylpQCXj63uh9Oz#kzu$s- zW?@yE?9&sC80OXJxQCCvZ!_n(CGl~<;7hjTVfPLtsVZJw?^+jmM&t~(!|4`9R8~uM zJnk3vh+DLb%HLjGRBg4&7)-KP?t+x=tya3U(Mq5DWI7{R6m$2O@0g9hPh+TBo6vBG z4Jtcu-n-tZhK&TPn5D8+jWS4}j%ST~aW{Ox`L=twa;UgDPO5~zcNYnkH?N$W97Om- z9&&X2N_)wGy7LP!{ROG4SzfqP3Cp|T#`oiF#E69w>H7!^$+{+&=PVg#*2n(XUt)?{ zlk=&|g@-+sg?eW3VccBa`8zVW-khO&2YmTr6xWklQN@4W#D=Cdzx9DiiU>-Ljz%Dd zFZSisHK|V&K`a^|%RTd>tQv0DrK=uEY`78!s}j^FEGL@N!5&H97pYz&Bi~=pYbkxU)&V=FmCOX^Cv{<7 zO{N6Ew;a;Pt@q#~6*lRQ)@Xx;^B;acC7*MABfw#_SR8}LOF}E596wkFPyCkD!{dF` zsGTZ@Nu&ZfT^nh|Vb<X zV1O7JAMjfqM&LJmH<`7!{DYf%X-}+iPRc2;V)nZ!J9Z&RYU={}=h2n%SiGX5KtB^B zpfNZlV+v=OxV+f22(u9zUp<}8IX5qWW2 zZ1Ug>mH{`Mn=jvpwqY0Zm-*Kg7MA2#3F=<`)>5!n?u~1k;@#lU3@jc4 zxON8as@XszBZ^7$+g@|%uI~5qU*c1LXNS|R?&(^ioF^+t4x*e^87e8!2Ku|*!OPtaelqk*wq&07r_*%v@^L4MuS_;YeWLOqQRh#xJhR-qAx!2gL*U5@fAgkT=*JEHvmXmv$EjmwmaUE+}&z z+F`1nxhqy}dvm)M9EjLFIt7aNYooRt;nz91co2;u^67iR#lYOIEeG8TW3C51JWfSq z&GbGbI1A^6m@&qdyhxPl$=v?&?E=(E7Z1bmf9e*_+<(1dsZy{5_jL;q)=CZOu6@V*Ag+ploj{%ZUoU9s!;ZN zGcz{%r95ly7!S?yi%3bMXYyZW}bgG$oQznI9_wroKYd^j^R zU$qN3`biLDV`Fv(3axSdAX!^jAcIn=tk^+UMV8Ft6I8tF)@@XSU0FFOUD6aj^h1Bf zUO>$rkG!(7Q!rT{zRzaP5C5YyP#>>(bLI_s#bSaNZ<5>Rt2|ZL8e}8|Yd+(nYy9_` z6n@|oqF~{wn{~QueN2=?prl^4jET$X4iB1J++NLMC=Z$H?EleZkW^ch7p|v!!-bla zwkG}?bvE~#kai0T(}9WLpdQgsA+VGqYo-`Kdp{@$x9ygt}_x6vM(p12c`&%Avb-PYd5`uajc zG7KJHB)JkNzmyYfb$+Pt0RAmMkZ{0h(-Zm$0$88;v3pB7SKl6MZZ4&NUI`}eN^)Z3 z+oH)A=xD{H=^?bY-Bkpxuxxs?wxB}*^aWjwQ1MF5uAv9=#?7tJHwAoFlP?a~>LoIZ z&z{{t?uwa8+bcD0&4hpAV}JP+Gw*dx3|YA@n=fl(Lf@u)pc-HGLmz^Cx;)IQwX{U+ z6$@KL@U@&@P`$Ys&ONNNO|5<}mOJHb|FE1USATOZoPW)?{^$L1Qyw9y-XiT`cF{c( zsH0hzB0_{!RjUS;txM#Mmuq|rjM(nty80e1wI0movm-*zzeZnq;_#qo)0j_J@37OC z&eitL23Gm!fKVY>zs0S^<5r{m(xflHoq~^+I!)jdkT32&zqnNnr%X}Zw!WD>7*Oye zF12J9bl>8sOI1Xm-04s_cL)TMmtVD)w+H=`QeWW~q~4|Q_k#_{TF)g18d8pm$^n(M zWUB2dskF3I@hdWCa{4+n!lxuJ@0rrbMgoG()EVp#PS&3g`q2UN1EJ8mE{?>h@)V{^q>om9d8_d%`QDxst&*RP{Z6xE`jE!A^j_FpW<(StyYQ4f~ZFx zl$Tz-cmY?2IDl68`0IX8lAh+Fe*I_Bhk`dIyGvzWpR-)<2@$MbQES8G|$T>8!QSGZ?JDkWCoExAU}G$7NtL>K9BEP zP%yiT$(FD-`i8aWB5FRma5TvpP-(gB0X*Q!%>VAA)fCDZwP{xt^8}`Sqgc!#X$IcZ zXPd*ska~wL1^hbXScUT+k=h$MJ#5bRH%BECe_}ykVVj{RRKGAAoW$KM}$UnK5dV zV5-yF?)TnFzH^n3xu^=;>obS4oavQ1kGR{g9Wc81+Iasv&IQh-xf#FCGfgP^{VsOw z%YA9-{cy6MS4T%Fu?tD+Vo6iPj|udF$@sT)W-wjGziM9;EC8(0=k>7MjsEPy#uXka zj*Xy>@=iul*?>bLZi~4+ze8-i?{hloHxB$C_B*h3f(ooMWVM z>_{(*!BLR9aH<>KT5#c=j{n&cR*M-h3&nNXN7Rjm_M8E;Celc`PidyT#2w<%o9m z(|+nB3-Q(+GT~@vE~kmMW$4-+;9p(l8qcxod5BtPJtlAcB#x3PfhwgU$ITDG;TKh-kr59-3ABhiN zJilx1_8ZKL*#6e_j-hb(yw)Ch2J^hJr=}}X3Lptw20G|o&D}zJ%jDVH19=PS@7=xa zvI12f3rh+bpND<3-q~*2pIR1bg4hW*(klgKe71eWR}IqHrmsqRLzK&T0L54O{cYI( zH%Z(?d}8P%1Ud5r1k?furN5d(Tx6Nfin*D2G{Nu_h-GC)84%4x~#nTnxUXkO&7}Rtz#!BGp2Cr9GErnKY<)Q zk4pkAYcCAyV*HpRYU01j)h3G=e2(f#85Od1o|YE*)haP^A2IPIzJ^JP<%)*VHB3)W zKEZ&ld4aR?06p$I{^7nJzjLNf;d5E7y0%kaJr@U9XLU>a+R9OxE|;yEwkQ_+I!+bK z7S1u{kiDk^^FsEMoim=%5HN}cS?-Zi zo-k1=0KB9MWe$YAAb0d0pNp|%oGW`^ZBhf$k|67tm)CuQF0WGDlu?Lk_69DRBqr|i zk|G978Sb~j$wR}#plmPif+HPhvQJoYf4sryLsM3&e-B^X7g2@TD9_i-d$p%Gy5WPx z--}M^Z#S2zc7CvU3(}R2Qa;A>x7dR)=*zM!Oj{dmjV>PxMlX+dZG?)y=Foe}zCuD% zm`HZ!{ziQ`RyJ7Mf_~+kP`t$BjS>3_%vM!!8XOlwuzKt|DvFvGk@dtMK&+I_wgRxdQFh@K1;)%sn@IW{T*GJa?NJ8tg*7GS~w_HT)(wnf6?eCoCecY zku$Kc&^cDLn0u*HAFt3N0yAmPr8OAUG1(mHHw^OYMpsO%qkw3xv#GeDrEKrN+E#yZ% z{ZYi{3XL|icAqB|4dw*h@b&xm4WP$ttRG`(z3S}f5OL@67mu|O)w;cU=O0~c?wa$S zk;3Hu=KKXcegEL#_}JJ^r6ef{$%oue3&t0Ca!U406?&j9P_ z5U?r0=n|>_D@o>aU>MCxx=pM9rXOiE3$O7n@SE^HSL7ucdPcRtb)ZwHL`oJxXnL9&bP}jHs)htst)qsF~Y%s z_~%%c$B3%4(}y7K=GkmJ9if86=-=j`l5m&j=hIVB`5g%7Y=Z=xM8wip2^W$R^vj$x znslD~*qK{qQq$bRoDVw=&jn0coai`cEEnmh$vU@A5m!%-GpM9RuXLeyh}q3BAi(r+ z8K{XJ`+Sw^`VxcEZ|nlXWWWNC3^ z(1zfIkPDq=w9@WVnSd1icYlkjo=|=FztazdKu7HmC(3!FSiWzW|3hPno%9c0`5s!L zLES3oDdqUS7!~hr6UiFvf~2Z0qQZ^deVvo6v<5V(n38N$o{u+)QnAQ1)^&?e$ffFP z>t^RfyO{N-y-h?u!-M~mM9~=A6Y3SZdRezI9Wb*p|Lym6LydzJI_u! zAh49Ulf3Q4Z(LJ>keQWI^c9>`1q90fmc}5ei%Knn2G7hE&xJcUe16?wxX>GH%F`o3 zsQ940Rt*0}OrtOU&4f)8%*cPwM{rR3NAFg9tg-vN`(3gB=8b~=YLisfa$^Lc8Cv@V zY9jxY#vLjDl=nC5I>A~|X(;{m=TvXj{LbI1IJj=mXhn&(eN=-<%Ow7NL0!Ph2?q%u zN;BPWIeuPO&BSLD(c*h5SekTCt%nssgE-OZA#C-E!AW($^pu~LRezo%$H1ln<|ktF zSUt71AFB7jFHL2g5Lz34$1?;hfmGOwkb1FPPwc@g)jRawW(InOD=v$3X%j=#FPHew~S&Mi!S^W_cLFSQgL2s2+ST7mlbnZ>4lf8QJ4q7d9Ou=_tK!Z?Xb8qW>&3a z2aD&7J6SVQl-O7Z;F{e=AB3w3pMBcC!lVW}!9}$BjJT!er!E90Ar3#(pOe@0yU?Hr zaOab$ibBH8N=HY>^;Yubp=40tM<^10es(xK3+C|R=Gy=Ui1+mBFJh6Kh@yBas~fRN zKX}C+HDbI3WRyPi7DwVl!box!23M8q_2o1lJraZA2Ip8RW|h5m`^Bk!S*Rt%uzz!6LEznX0hL&= z;eq9EY>E#}zq40_$e@(u?!JF#?+0XOyJTZV70=0l-GYn~^W#Y$QM|X{@)Eo^;JPEu zSsS#AP7x7o!yHvr)+=>v;K9)E>=YoCPESwUDu)EG%**sR*{a-)J!;>7rZ#gYmv^E{ z2?^s0$_-wM@l7epuWmAT9WPj%Xr*NKc-|+rMJh5JEMQCoID+gCS^4=0?OUf%PM<-| z5eX>Yot&JszVh_{D|%$GaXK$rj7z)Y)ScFLzZ!9{;mN=d{pFVi32qmQr4O~xD#^;C zg^b_IsSoZcx_<5pwXSP{Le>J@OPF?6_6eBU^mHO&oxUCb zG4LDnbAKiox~h+Yi1I|c4@`#O{_bFmD7Sn{E+AtZXRj0-^yI!nIW-x(OZL|p<7Z86 zbPtgg5>Us3gD7cCS%nG(b(wS93hG-+H0+PU+a(L?kr%GnN+crV$=|;77cHZCHTiqa z+NJ&6lIr;qS7~uQrhNoR=Zr5*LP!Mo*&#+OdW;rK(Vs7E3WZI6``Km`g|U}#P3fZ; z?Vu!=8(PlfhNx*-b)Vm8>MX_ysOCAviHptk_am$<&i9fNR{mb=5lSs9S)_C0cf3Q0 zWhJW!LfJi@6I_MJ7>{KGH~+|e-$3DCdYQTpwmxQ-h$ic!s~(NYFSsQ7MC0 z-2V>A#3Yz3_ju?CVfw!#CR%_o?i1T+!e&i(FTxa;aO00p^cQZN9)u$c6r~$s_iOo9 z8f)S^>QPq}*+N4dRLZ91gO&`qNEV!9u6A50WdQVqLYA zg$z!ji+f2@d;N`H91}pX*V59`MMMlMz4f}`^1|ztQ++@rTUd$B#Hjs6WBFEdqtimt zqL0ng2VN39*~#`FJzn!6>S#^{*^3E@0N^mHD8PnSOsFeQu~%I#rl|c=vey6Iu`w=r zHnv)hqkir-Uk~>u*iDb-UDG~V{eX6EY+yme=k`d;ok{4FQ_}% z8I}~mgkQJe)O`8aj`obgyi4*~tw{3DX#+0cNzXxl}(b&S6AHJ|TO0 zjxN*e$Wya`e#7ag+jNWs6yS#X`eu*Y+A4A(Ib}6z>HcD^D&89032bRgrZhp1xxPO5 zQD%gJ+yp7!5!`JiPEJl72?6)3=zn0ZG9Y*`0e5AfmjxaT*`Truin32EiLq!(F)|@f zITxJDoj)jV==aVQx6#lk5m%)p7ol2n>akG?+15evI-3t^mVJ%sAv(MfhGZb058%r* zGho94{1Y}dHhzaa2fvUVuo|#uXJ*viz8(Al*eJ2qlX<&kH7|yngk4w>&gsDpl?_;A zFBlkB0bHf-_!o8zV+p~CeF@rv88<+}Zl?6{Bio|SnQ-ErI)la82vg&lumF0b0JRr* z1zl%n=O+qGruG9_X-^dI93u1$`|6dr9 z^0jFY$2~$@o@8+f$1l;Kt{nQ%kJlV^-&Qj(7-V@`K(zcd5KHIZ#jXUG%@9JNPASpF z&Ht&Xb!z-#uc>r6*ZF*Pky42_EREJjw(Gg=N+=qotff_uljFGFA2VVJP3;ySw{O|U z6r^x0L~xp$nX|H-KS}k)hr``cTP)`2+UjRYJr5&uTeW|Rvvpk^j~~xUQIpB=iyb~I zxAw7a7YUjM^r(@2xXjh#foz71bhl_NwbZn$J$5Q@!NCI!POEr(8yj0%g7A5QW2U@% z`n3TA%hYpW^Zwl=mDRDS7KmNTPkhM9e<@g*(PR9pB6WWM9|~qY`)%nWl@-+K{$*5! z3iDhF*aWm>^QAfKIQXrfac)ZHlYBPim*wa`+yxUOetqYO#}t^g7kJcn82YJ4OYu2p z{T>WA9P;N!>PUf!`?NhQ_Xi(Z>ohN%|5-Ft8(&#v|pX-pc>rEykLS9YY3Iz#^DPZ;0s&NA^KqJZ~7ct z8O~U{QnJs{&8MA9v_e?_FHCj4qiNb9ff*t_I$2X7Y~L^E1Ua8IC6*#^y`0yqHDsi4 zz6&Np{HNaYT5JiqI>pQGx$GxK@Av3VUjG2AaWJatbP{Z=OE? z!czfX{*9+n-Wu7K={`OJ4-lyK-lcT)wr(JZET7sgFV{8X_T2tHuj#XTb?oPzfrIy; zVU{{*`{DHDu*^jXN2UG0)K&bw&S;-S5p-Z0AUTyL$tH$ENEQ|+0a|OSKf`CD`rG=k ztSns!JUY6(4{r^gq{h;Hr<$}H_l1(8KR{AdRaLSI3V@T8%ZC90z7}DARj7pV^HK31siert-!|kwf*tt&-l$4q-9-+?~C$O3)Z9-Ol-QBr&uwjLhV^aB88)iZ&rN@msl37Zv&WJ6d zflzd0ZcbZEOY4It6-GAgss=kcO5RI@-@gr?;k(jK)m~kO6cqE zuRC`|+3nAcenNs<91HU8wGI}W*!weO{WfFkgmLgBV|k|GWIBkn^zFd=K@qRPKL+?i zvzXWH-^!br9y8|Kt`)BZ9X1tTkOs8kpU#fh2Qut7dpTTpz7=@fCc;;M?Q7kj3^PtJA*sj()x5>(_pI$tH@x^}!`GrRFPI zP>%{n`N!jBXQMA-dQeTwEIJyoM^1oL>a6ptKkfT7XxV2aMNJXj8Hu0luB$OU9J!e! z)LWJ~ZJ_wF_3J+N)QzoKzdsWn`CU39FL)`IxfSfwQq2=eV!ih8t@$lU@Wu(TtXfVg z0>S%&qU>Vx4~I=$1vx#QkR!qIz{LfsU~<{mn4=G(q6jSKLq7^?b9Y0TOqfqsUr*^t zKQug$!V)1q6V7P?$|V97Fh)Vlv`}XG9b^)JPbl7QxwF9qhs3*bDLl9U7(X-`x^1j-4LTxdOGa0~%SLv!;lyZMMzPli-`Fz)E%^At7lr zXSDl=wKQZ=n*@xnM6d2jo0^*Ubj0&f{Ye-Q#^krZ8N^Hgi4~xq=H=vwtE;QK@7Vl9 z7P@V82?N=-$D=~w=_j3;lM~01)Dx*K7Z5a^fU)1r<)IgFtRk8#{p4x0zc2I5L^tSLzs6V<3D5l?)j)tR?tjZNHQ$)yNB~o0xM2Mc zYJ5~tJbWmS&dpbQz$uxO#kPb@LLyS5EQ`#C&$hV;S=#G;6H2)7;Q2`7F(+W)>{#7YfjUB&4LWcB_ z5=J1Cd5*WOqVynVlp$ww?)$fg(4Dq_;$;0(+YDCP5}cO+q~UEuUcj) z*3Na+%!@ln4)vNt&5!oAFqT4j zgW9^oW&K2HI2!UGT;N)eeq#hN!T+R$peT6%5;g$MLh1XzX%;l0jc)t`HAIOOdbAN+ z!n2b<2Yuh#!lRNe9U)2m&F4r;O6nvRVms`pV0!yCHMy;^y~j^{*w06DB8yqvcd9>= zjP`v0j)pkpLFs&@V|??=%>0Y#M=!wpgOpd)nNG62di4skSp83ut-#3tPBhV+2xD6w z1!Mm5a$pdR1-x}xLPA1N6{y?GL;lZr1V?|rG4#(LQOLta6bC)jzc~}H3%emVAU4JtHGD*tX27kY3gi2+Jsz4UiJw+}w0>3Rh#yE(Q$rO^=Xs z%`7O%YWZZ-hgNE0qPMRdC#z?dE+rj;A_fG`>AaUk18nO)+C7oi&MKHrnKI=Kp>LW} zB=f~UTx9`B&g?@ZIo133t$ZKPl|MjTD$mfOe>B;5)R`tNfIzIo#N;8lmtPAL5@Sv- zBH;{)3CtkzTkAN;C285z!6MQEv$H%96fv=uNRLhpor!T(82YV`vapGpTU)Yi3==&1 z`{{qy>sA}5es{T~I}BcflYs0GtkUO*s+MlhB5eCiC~3DGiiwSXwFYpnnIDzM3-HXf zZRLPYJYggg5xF+dI$8QlkEE^_I%I|2qft<;Z#ncO{sQ%W5GGPfLbn=>_jt9r8>9lC z1rMRvtHPY1%Uz3|9OB)z3Q^P0fQIn@O&r;t57w5Z2M1q93^Lk?>#q4!?y+B@={uNx z`4$xPO!&`tS&4|jkB3Y0xUF!p3>=7c*{+7%PS;q8o^FLqNRZ6p;^MCJP}0+V2laKI zXISJ62u>VwaE)^_74HH4VeR+t#@gAuj*Mb7uXrsrWv%2mtn0PISNE(RsxJCp2=3?@B+>;dsTh$w)O9j zQFl&LH*4+9ZV05jw#KC6benkRyK9A)?$z|1Yq+!JtBL8;o^Vj&{(zTg2f!DP%W9UK zN15JdLALF1#gg3?*Lm3e?AHcE*9^O^HZt~8%lKSF^XKKJ{Hq0gN(39b+0a_1bv8{m z{E2V15+3h*N4~TP{;_K@>T@qj-zPE6&wl^tks7R{BSB(DaUA2Tl(-ka6C#}>=y6l5 z*QB9c;{#jO`d@P=(EqVstbMf0@SlK5^Se&Y0%;yh)?Ssj5?bR!OD?u5h?LY5Z$%7~ zDm=t=Cc4yqvDVn5&)74!94K_ou*k!5R{R^HO`f{===5u2{CfW1Q4!7JpnxCsdl@XO zMG=a3E)M^p06u|RK7UeaLW5{yz1)owJ3rNu|95QpFbjYIw(%k#y%@<<}mnCptaUkRTXog z08MgZPvw**1`1ao;-;gai6D5D?WgW^uM>rdwRg5N?id8h!Jw{IO~`H*9TGACp5jYK z`UAA+K%Ar!f<$Q!R(cj6tbepP_~mgyK|rS1oy=j$tBEf7i9ja6aXBjtolH`i6P$$h zrI0jW3k=jLz`%7I4VKbhX*8e!l13|+l$n!*Lr5s|Pm4et&;kC+3%t8DugK!88^P#_B7$JI`_e|km?^h_JL`}*;G z+Td^ADL)7df~UT|zU@-8>)q9>zj>$K5YYN%5H$eR>#-m-7`*Cs@D$3kuJQ3xU?83I zH{&LNnG4O>2bi}96oqlEIOhikk-ew@zW)0G z<Pxc3D6n~7}H=qd#0PliR z+HY{i;GFHutfjqMkLS#L%K^2ca=56bTo_n^EK2DDp1-V+lY zE&TV38il|Y#k92CXNvgQolt-tdFfr?9iA-k`2vg&n7RLaHCJB_A7p|TR4ahfucU;8 zd@{R?)#}R13ea~sim09JffVGEs3<-@ej6SMR0X^d=FMaX?I9>x|17;Q$7e~h&KD@7- z?|V^z;cn#g`wt&zZ@W%_k2x@q9>!C_nLP3WY>XyeTK`6%y-4dc-(}l-s)2xTbH2_N z)NOVmB_cQ0*MX-3E*>71s6ra=#@d>gi+HdnC|7@zio&Z+7%+Boa|;Xza59lb7(>`X z>MsR1KLB*_aNI}k2>*Op@csNmEozb?-_D@Q@Fd8dsU?QI_5!T2? z=D!{A7U=H5|A27+a(j%b5hms>&`Kr0HdW}#V8R<*TomBvPwkDy*ncA{dwzcI%AZ~i z4P>q$9r64loPQ%B5c-S*L-Q*zW3jz9%#jeV3eYK!FMl$yfbhJ|10aFIOzjx0(dIVqJktzK~Yg&ZY~i4!B1R* z?CGRe?~UHSx3sYE$NdBx-+{EeiH-W7=Ia5@BDvRHOk5n;i9T*l6sT^EX4qaEJ`+WV z>gzJ_{apOEOdnVcBM-vRhZ1pG5o|)`=YhpG1?1WV`T2K;UXMWHd*SBN?*dIAu*j>14K%|fCqLZV)6vwd2Le(sV_tui0~eg&g?%q9 zeB)wJl8l47vdNu%o=6QNr zWHJLY^ZlsT!zq}Sa~X*Ap`qd7jxH{Fd@m<(N5I7gz+&3N;~q#G-vd*UCn4pZP7*B} zZazLx)NtFIDgjGC(c-YH`2MT+fbmQHW`a}ibO6_^fI<-TACT98;z1HT1vQaAZZsVx zCMMX$fdU6;ZXNrcD*|QU<>9iel~uS9>KQC}vbD7pOcikLLModH(&8!wH9XkBUV{<@ z=-r;awQ3oA`wC!CV`o>MkwLy#k*Oe#Fx&F^^2e7KQc+|cXWPm^(g|ow(t<1Kupc+~ z%Gv}!UszdKFoJH!29`X{&HS7x@-7tl*ub4oF`0d*sI2v7GsPB2b6L@ar9XoGm(bSs z5SU)ChC}(o1_TBIPy20ffPxcqZ@R3H4-Ufuo8NFf+}F2rcd{sol-yiQz72Ovwb}KA0D9vnb%ZiF_*9YRBB!i@wq90LIp&Ho1l2`O%@FwP z3^XYe@t3tVQfLj>A^%14oHHE8Cw+K$QpuY5^c|AHxVtv;M)TF>FW+xIM z%Lbf9Jhk8oi0Lu|a$d(^@JhRmz_F|(jq=mom3=B@aI_;hMf!m15?r1*Rb~LZklwg3 zNvErIp&-$N|2Fk9`3NcyC~q$h$zK`426Z)fJw6mSY|Rt%v%qP5@(p-NuOzI@YUmO4 z{xjUSUBBz+rCl|Y;`rcY<^e(h8>7pg84(u>Y5#l*B`$wITjN{_+Ekh&PlM^6aZf-# z2fd1hgrp5Fvv@K@0buB0WpMS8wqwNVY8i%}*;m!!J7zjhH>UJFMMUBD&vPp50RTzX zf$jbh*Rp#R5b=Un1LN7aj*bJzSC!1Nb?Sq$;<7qSrnhXA7aST&#Ot^XzQks!dG?sl zH^lCSX6Cko`?Zn^Oa?Y!hP|<`rLdy@Dd8+h8w<3({VibbT<%U<&Q_WL=;3d_4Bs=0 z@w17qj@|_HagjzzGPnKHbs50)J}otsSC>YJlnnw-H4MEv%F8ua8RT5(kBZ95$G{5{ z+#V1S9GnR>>NzWiZI8}&b~6B9fD0QGzyJV6VTATIhxg-+M7|S9_rZfr0UW-%jgIE7 zt~`qs{qlTfv6N%LY#to*Jl<;lVef>%G$9J$*ypax@(HZbY^Y2mQYJI`V= z^Pt}(8=Am-TCQHCH151716PzK5KazLrI0>@utsTJ=1m_@?J2Jdmt|qpV=qlklvpjS z)9cQ|Qn~rC!Mn}3hqC6!ue`2ZG9paXN38vj+5N2LVTBw;clOSR-|cK0ptf173t(x# zHJtLcNhx+k;$o4Xw)_qPd49L+Jfdg|1*w-QA0xNA^<^Yxm+xSP9N6vE$@pZQt3m@7 z6^lxV&dGp0pxzH7cw(Qv`0!7qe!v*K9H2AQyal1#k)Qu6n%QA_s?rL`^|E611?hG5br;EBQ6V zM0^EDpL}^LU7vllaJqI19et4cJ%k>E$>xk}#;1?=iY}k~)LBy0m$Hr=-+(C%37*fQ zO)(p`<)2Q>cy2{8a{Kf`+DvGayTTKuaiTtKZ4qC-t5GmA{+OAm2d6)nlQxZ#u`4|n zq?idvcKv`tB{bL>tTE1}7l%ZBo&*L9=Rx?V2a`^1vF&oJh=>R{L7EoYQ=y3SIxbF+ z3JCyh=tKXaY8@Ld3g9pa!Rq-i_eR05h-q>|gdOSa0If;*D9^GaK*<$FA;eU+3<)$@t>WyL6Jg44}C075ZY8Db(%(HfSAbYTclk&}sq9vGZ|2-VtS)}td4HaT{z1Cc`f~*z;|p$P0Ev2Fo@N3_ zr{KDgv0uL|l*ONVGmUki@7)78D;KN@Tqb=c69r&CVb{o*D(w`2O$-DCjbDHf7Z8S< z4#a}2B@H-UQ>yn@o2ZWHvFoD@HmtgOJ{Zva!9 zM?wZiZ13Dy3=PtSai%?=<7d$()v!Q|e0s{4^_`Q)?JU2gMIdefh*&&@g}tLyVqu@= z>A3|L_cd_R<93Iy-w;NKcC>@Wa9^bJ16NTnuricp3g6Mi*-<0xe*IS?Q_}ccfMFas zYC;c$jmMssFKJ$SIX&y}Eh#sD9^cb>{=cnbkg5J(>zFoVeJkid9`REs2q>!EltuIv z`F!-#XtUni4S{qJ{^ok_4Zp&RI+1fClk)Klg6cgZt3+LXbg-ykCL;i;&Q#JPc9QUqZ0SP1e%4s^d4f`07MhgoiP;orjc^guea0df_ZzY&Bwvv4s6MH z(8M($!qU>xFL^Fuj!8t^%&{fVUL7blzm3-HFrRgg{S+-nEaJ#5uKeY!mJ*7uf3J`h z?qHuzH3*y3q*H_Lo@AhrwbN2lM??V@Lsl=lSmuw149Gb7Uom2M-#pGx1A>kmjgiHZ z_srSNZA~jX=1g}eKgD`@T{{*<8}h>bl*%6SN&nX`-Ba=OmB9%-Un?T1A9W2R{luLe zAK&BxP`}vd1hN4j&S7CuJV6EHY`n}dOD8PXTC77>l~S*TO^Wue0P6*Q7qBue7w|>}HXGo|HEaW-tPy2R zO>WZ>5@4qahl~fV0|L-F1K3}P4uhIo+WpBOi|$L8;baZ~G;;R-gwxg~ab1rRrT-+= z|3=JZqp72_-0TW&==liZ46p?{N{Qn)?66I|e)?fTV&X@zEcjdw!XqMp4Gy)$>yg@z zVXli_itWhSJew?LRnN_heVOJQMQiA-9eE=>7S+yE7T zNE4*kKpYbE8WuAa8SJ@UAmiTl_;CA#a?<%_z=M1*n3&Ga&jI2DLRB;Ci0Ky0Sai8Y z7-7ARuCBE>MZs$j5R;ISf(RMd-sBU5sd{>T4sOK)*y-r#C=*zoC=suy`=lEI=NjPe z0WPvCDk#X}!<#6X7{MSBaC2<)y#LoQ9Drd^rDtHc2k!u46X2--^qfbt=lvXrpMuCe zXTW7z#>P4C_I1ucCJ;{5hpc;U&JFxZ5Ok3)=>+He2KzBEw`8WL4~~j5Xmn(ljC47D z?r#jqg%%hD4viq569ge9P)i(t$HdCc&+i`{9R&s{-=YLTv`fThi~u}T{O16BGd&d@jTD{+~dB(6+lS>swjm*)yL1CEmj!e z$M@e0zR3E5L9c*XFp0}%6WkrxRe?d1>j9|C^?cbEga}1tWf1j%r0EDq|HYmF@ZY~9 zn4}IU4uM=%R8$mXzu`n+Pz2nz+W<+qwi*@4BZHBTfzk)KJ#F;G0;?$r zkl&*xfU@GxP^yE21E@GTg)mZ0AAyJQz2!>+rv*?m0`ezB$*k@$U=D!4+KJP3ru%k^78VN;50uvcQ<#t#H2bA&2j*54b)WU=jQ+!asU%UvqW(e z6QMcZSI&L%{u%u9;kq*^;ND%MT63s8KD1 zEAk(m?kz3?)u-$0YXFvk*$k6Fb)$gHa9)Q$Ab9?q80d44njCx?_dR>0-$^aHXT+H z)OuSmaY643pgMKt%fGX#uh{s*$lD zcc^E3T4n|rR9Nx0+naNPROengv$qk~DhCnl+~tNi7vi%U51(3>vX5XtISJ(EJb8iF zsDE@D`SXo%emhXUekq1zFLSWp<>?XpfKQ?XWj@sYB;@xrM^k5z@!DZen=0!4mYJm3j zFd@oN=Z?C;GI(M^a^rC1cjFaTI%YbtovaF!^lH z_{cz13;z<1ff<(6>fTgzJ)Ji-O?8TkZbd#vMwVqKw|fy%AQuoTnEFLBwYi`X9Q-y1 zOJ=Gd2cAFr;!N$_rZvXmfk|C%K@?;#l)FlXL{HAF;6r)wV6y=-Mr{%YTvI_pZ; z%S}97gWG`!u_q+^~M8X!O<3F_ zgG;qbkmb#X1fX_LF1r$LrkQSF=Ftwyr~!vtkw8h z?il6#caD$IVUDhgJaY4G7zUZ*yoD?LzHmBOU zfcTuv<@K!W?dk`ZHPtnbw`Y9;VITRGABaAxRIvTBwl(X8V-Wt0Pfi4Ly8h@6 z&m=;%T7|m7!SXEJq5+LG(=?uw^3XEK@rJy;Q^KDI5i#&H7h7z&qRZ_H9xtT0FnPIE zN>1~Lbe)d!!v8#7hnB(Vcx!TB$igx?;RO#BLGaG8dAm>6ZmxHqf4 z#7uj`i?=tVETdHc7TiYocLw>t&Bi~=1JPa#n&3vWw%k957fi6o*w~Bu<%g8>F2C+B z_QtT2>xE@pgKpq)$~R-|LlwoZoezi%rLk{7vV=iH`Ytjr5@7!{8bDVyXIBFPIe7^- zDTGnIQ(V~@84J7QPr2udf}+y-lX)s}BwR+LD1+j3(y@}WR=btuA6uSlO@By&1+uAX zHpkXVUF!Au=hw84Y`1!%)CFD7PWa#?z%Sv8IrfFweU*MTXzXbF2y%v$qAH#lDK-YzO5&t13MM&~NgJ%L8JTjs)tp{bkLlOK!R={po)3j~JN#?V4$un}*Odqksg? zrbO?-S!SCXVlKH1`g|^zKN*3?k3tA8!#jZiWYPv(kuW6arA>^_8p++*C~WHMK9$pe z_G*+ucff761Fd6WOhxnoHyu+H2dk5ADz{Nqp7EqCbZ5w3hX_8Y4!iv7&p>*4EC0d; zn%~S6q9;a|rycS}S~zGoLrRgou_kEP-9JHYx13Jr)~obwps0Kt)L&?1ZdyqZ7QGXCH*0r$>IHSsQM#RH?N&&Y z=E_*KGcct1RF$lnLY6HEfe_G%!vy5I$(=l+PnNp8CavDKdTqeuVYON$wcVf!EMI-* zfBoZgE-wef9Y6F*xe~*Ll|}@RHyEV4I4x(xMMdKBi4J}sa;LR&aCMTuq100!9E8=i z#Cdv6n=Krpv`Ll6F|c(dth-}p;zcS^RPjZ>;|Ht5s5PIS3QLKs{ulTv$?bTf3@@Q$ zQ?KEZk+Y)McXKxd(pj1$B?i8x4?OX=--ppgl2G3VF7A$ofLO&GtuG7H?M#|C2Tnb& zZe)5~`R-OOFR-sK&#eD^Nu$E>Lu06`MUqrGNuCWVinbTv3#55+TXnQDe6!tu{4LFP zBKSckL)4sUb)M(v`5AoXuGEIcq>_T!Zwv-&fhFgU^X!yyr2O2nUU(FS1@#X3olun- z#my|!Gb*065;R_UCv$uRKvO%jk5h7$6me&JM+Y$B)#t*nU-@DRai{(HpYk=3wy6uz zg1^m1U_XUG?19(;_!K>6C4tn7fZjAHW#WQCs<)ue`OmL$(hf!U$Fd-E#CYrFp4DqZ zmQacR(;e(}3|;L*@-w*)QnCOb24Q=N>#iX3dY7u>d*y{87do)wc$|SaL`m|PW2xGX zooCD%%zv|Y|0BKJCvmc-aWs{6L$-1WD=Z26wAd06z5Uin<5sfZuNgsFY0dRS2&!n^ z3?cmcaev*@Cm~!+I)msh8;VUPx*BbDHA%u6YlQAn9GKx0Ohk6$J2w&?O{4Q2lJp}i z6;?#dESt?IX;d(nk`c1GD!(MshY{kf8J$2+3oK~3B+eLv5!zg$g}#B9n-fck`wHiP zlT%Bt&jZLlen(0WG!5j<_^drqlBM{DN>A0p%y$HGe{L_I2+{c{V>pQDaEw_-zw5;= zuA5K&3zqBZ~E2$?W^x^ML9Q>@lwf1Y&9 zps2O+D{HO zd2v6Ny|EuUN8`L;qBulBc`Z-F`j0rO74I_xf@owEbnk1UW_Yq6pR!1#^Qst4Sm#jc zPUKTAdnY?1)^GnOXh$nuT%M8%$B{3GWFc-bpEWs@{?dqc_`^Ef+&#D`P50JPvaR+` zwTsqC5Y`~CgJXOue-w0PZHJ#fjRQ&P7ou&}`t0C90jtHs^I^!TC@&}b$m2|%Nt;_9 zC7ttPgJ_8v+sCEb8cX!-EAiC zT;)v#$K>SQs8E#>phw(YPBsC+VGGDw$MkB{BS6Y=LCqtnZD0VCi%r&;@SLfycZM?1 z(OF@|#uH(EexJFPcjE2sotLPnM1cL+bA^=83AQ;LZKnaX9~d(k;nR{ruxwE0ex49z1Zc$$#IgW)?Gga|-aCVfk%Q(li*DbLwLWCX5#$}Q8$X!W zVKlza@bhsNl%_>{O{K%r8c&B(p*lL_a3Lz43=aD~S)nu!1OyQnjc9NrEfG!vUvlaQ z!C#_cW~|X%1$Y1Z!i&+cuVSiRoz{XTl zS#7)6p*PNBa^#Y=P6Mo=sp4j4r3Vf7Xe%YKBu5Y*6Ku;>O_GQWS+!O z@o$mY6p>-*D?JP5vFFW}lMhe4oJUhAS@YZ5r7?Odk&OEo#G2ki7U^*{f#R~=C!SPx zw&=_h1;MS2T+uHX==vKh^e<#5(eZ+c$hx@yZLBlp`Uzss?er?{!_MsIVY!nV$c3ws za`I6d^E~x^o!`E>K#L|gr~^;&!-uusl(_RZFq=G;ss07PKq zb{5ZP3B`HBq+Xu`|ALUh_K%eNJ<{_}IM-$yo#|K8@$kpOJ3mQ~gH>p27@X7#_$RwM zboARUBc|DC<9pxy#tXKsFvxy2#!|9%wlLxa*O=tlG>*z>cw=zd9sx`wG~X7 zp+ZhByjm(%k5xu`2()O9xY#dCQG=b97Yx82(bXlUqEZ(t$FHI?+tWkK$EPvcg4k@- z$$Yxa@HK^DiIWk)20)c|)T5cmccmJ9AN0k?$5`#cMW}Hc?d=n|Z1OHL5Fodk#qtpM z5CBO|7N|^Kzx4#fazNT+P_M5ZPeX(R@zX+@KLNn_UdsN7cYiscpLGL#ZDnOeg@G2L z?*d}eIUN_*43IG)!(bf;Ld?F*>|ojeAZcS`qe|BpGGquai2WEKWFRQ$3HrX#(T6e~ zm6f(T5=2Dt0P_Td9SeZA0JoKZva+KRtxi^adPqH^)U6o=a-ISJ5+F@X#OnxLUjTW8 z`OTZdvop}#2XLMcK&pm@=*h^Iyq>*7K|xUj!@RyYVtf7?Qg3-bE>pl`0D%(F4gl8# z+&NDag%CiGyR%h;{rx6@s|RRUy)jH?H=GDdYyu#NvH~`SO&o}!ol{fxJ&`m3S^`*> z)BFbn{;g(Zgr%rg%a4f927G+uf>(;R0C)us40emYaIs$N!IkP9fr|d}UqN}n+iXRK z3+%g;YOQnQ<7a^rk-66!_eum1zsu1&Er69#qxmu-BQ->y2(0iQ+G+0jxYhr=Sf+~~ z7aP@l)#_VqTk!@1PB43X957`sooBC3btYZ(WJgachl+jc_ge#(_n5o`mltK24Fyk6 z& z@J3j_qft#3YLh{=hzXB|jGAVzA#!0az6G~$syygb8d*6{I3_J9;U z%{}2vo1r>+`!|(zpAY5Zuh2JV8a6y-#c8T>4wj#a6QfhEu8$70x#2TZ)S7#l$eC+M z3a7z)Fm0Uumaz{kuGIHJVCw~lqA&Pd`xMuoh_wBzE$*(Ho)?sN z4sqd5gkx{Li;0@xc!tevOq2kc@3pZ=_dx#q*zsR)9O0aWcqmI}AuH?0e2k&t6 z$=jZAx@4PAe7LXUovUME89VkAx%@k&Ex=c7A{%iYk9kdq4%Zm263AHzO)n&q>?0h6 z>Ixd4SM`L?6j_yZZ|8>aM>o%4vkqG4E$-X}O7tcqW&SWRPn7bL;m=5UEC^^9o#gZti%~t+3&3P(Zv& z>&*`6Eac1>{Vm847ihKXcBJjyM1M47kmeDQ+>|J9x%$J;=B_66^XWG~$ATW}sqOh= z6|;=&y1MiB0(U=vBbfOAS#I^AxFFpNJw)&!eo7+PMtz{K){^wZkS8FAXn3tx=4@s( zG3gQ|oLr8d)to$668G?g;$tXz>W5y^8!;Wi2&6Deya)U&U@OJ^SE-LdgYz^W;r-u( z4ZP#xzygcVbz)=$I$}C0eW9SHqN0*HnFrX?(Dvc8AWmGhcX!@?z-8m^m}vM#J;l!N zg^&c-kXZO^dv#((z+L(0gQ}$9a(MsA$=&f#z1Ej6{4lVUo*;E}1Z^_V`}q+jsf6B= zcwdAj;=&EEJ2CZS@colwVaMB4p)sN`>*JeRtLuupT4!n4@tEX?#P$p!E?AEe8l)e@ zl>Xis)?Zd5-#<}~o+i{W6v(~qP_&Jt!gR{r>iP@Tkn32!HXrjRmc=Iic<=|df>l7K z0tMZJsS*l-La#|ZRymFq}AP$E=37MP$sOn_xe>JPpRm&ej- zEA5UDOBC1E5r*Lqiw5%E>YXYAA)HLE*fk1D8wRx{S=57D5g zX#sjU2XZg%OyZ*EJnh>xp=p;PNu1z_|1(+STnMX#Q}Y)y7svC_%_8YJ#?a5bsAkCv z#pYMNi@MSrO96k(ed~83?i$}6h)9cP*6&x9asLu!7$m8)U2di{^L4TE3W;irojpVy zm#QGE#XMMi}M0=ZoZAJ4erQJ#^^L|$j3&$^SZK0ve6h< zCLWvhJ_3+#1e0)2;QvWTF<6w9$Bfb*Z6qi^rL;+wD67N7eEMAS62@1Mg4QxlsmkwU8TMJ}1ILv~(4pi!DrVcfabaP+lyCge~ zvHiJfpV#9SzsV?jFTTk1X1ZoNnwEk$dO}Sk#U9kByU))U!4<(C4tn@aekYxKyc>ZY zBNauDVmX|U1f&EmZ`nqD>zc;9<$k_m-x)ID-}h9b`RH9LJYL{}=|w!VwM8&nD>W4^ zf7|Cu_9WP-#q?If@nFs#xKPFc%Zt8vju2*fH>8&Bi0wM1zUeqPiqe&-@u5g`QRzwq z2Q#0ZE~T#EgZ0(^#+gnYuwz3{+tt{MwB`pZ?vid%aJE0P60HpMwMllm+t_}^+)`?l zGbk^>uIp1&HBVo#om4MXY(Eu#?RzRY_@hSZi@_Sx(JjIZUhwinstHb~1pw1QZxS?# z7C4#sIDnBymUQAJV3z``5UhEB^3HZvZf|xpd+!gxh7_0>LiA(7!3WS1myUfStdk0X zTziAokvkqRGXT~Y2_7CTF&eJ9a7D<{I9RHX&Ak2xz!`)BOMJ(Lyi^||8Vp?D*&Q(^ z6GzyHp^$Rb2%`!Fh}_!7#y#N_0Hc5(3l7cdnWU1aH5gR570Zn4f?x*o(>us8VbsDR{bHSrt9C^+duDHkhu2%$eojX?l0G4R*xVlQec2)jsHr^m`ooRxeKO>NlJoW+Xf?XcD@FzRlt8=a5>XS zLIW~+tn@?=A<#7N+5iomf}FgqvlBElK~tNGk?}rXL`1lK4=J}er$Am}>X<4}sREo~ z`|YWcSE9kdG`8X9bT&0Lb$NLi@HZ!|tsz72%{|?lN+-~l45ytFe$=x4Iy0dYQX?r5 zKC{a&BB zkLNyq7Kb09ez;xfR3=@BX0xaL6J&kcSk?T8Di!ZT5twmd;#NP6W}kG3E1SHiPy?tC za2y4qE`ZZ}3c8O!Vq&VayTd`@cCQq9vN?X=C>0)kCg48lJ?6M7x&8zn>XOe$8evDZ z$Ux837<0LEtbUm>>ay!5u@y)DZyG95i9ENqIG9Sa4&KIZU9wXIlkC2h#8*zT(`dcD z>a@cP&6HV9jla;zd^`D!{`3viym~-59iiFQkdvIf_2JF$!{3V`Oq)fbzg+WKqHd^s z;Mfi#YmKEYt#&YNEiD#MKis>c07f+M*8!bwcgmNb6`~yaM7Bye#)$AcjzUbMa9hpK zt{j&{49e4mgzPyOW30a5ujR#P*2po}#CXstX(0@w_*Ma$7iBVe^u6l^J7G^ku&@3D%)1N!i}MqBz|h*JaNDY+QW%f zdazu!a>B*}E)v3|(Rd)o#5+IV9wZS?9*#Id$oZzYp|r1OvI?1Ig#58vPUZG+kw$AJ zR7C|fEg>Nx#u+%jr{IB!mzG_GS5;h0)&nfEsY)qn#qUhj#yAf2TyI{bNmEKg)cu+3=3i1Q@h#Ve2Qe^=vnI<+z#- zwYt%tt8rPCj?s$rlz{F}39YT{@;{nY%DL?TCcZowi2T|aR=5ArKVMmwFz_T?+M-E7-IRH~@oKZQRKJ=5c##i}Bt^%QvCU)(P34XArb-i=*7gt%`Qtm%9o zG8DU^L*W{Dcd=zx=EJ2zw25*uV|Nr+f8gAhmg@g^=<=qn3`kkC`nIO@1(7=0Ncj)` zd_10G-c{>s6C`p-)^j-k zZq8)x2{7KjbnS0@l)2qki19UEIu;Lmt<>IW)B@@P1oE_*!RNf!^cIO-V^jcKk+pp9 z9sM6E0EJ}wy@EpbA2t#IDAx}Ij9v#6-zC1jodL55gJc2^A)m#NX5Kt|_H0g%G|3wf z2;}}>m<8aze~VQAM>@j)zl$h`tO4O6Gb2Nv>mh*6M=b)Gr8*%+NSzjO=m-m{{~ap< pa=w7_-@v-jU-9+-Yt*NA2*kwfzc3tsyWKxtabam#q2L$K{{pk;vGD)^ literal 0 HcmV?d00001 diff --git a/doc/claims.py b/doc/claims.py new file mode 100644 index 000000000..95e9ea463 --- /dev/null +++ b/doc/claims.py @@ -0,0 +1,55 @@ +#! pyreverse -o png +## Not real classes. Just to generate database diagram with pyreverse + +class CrmCaseSection: + """ + Represents a case handling team + + ATC: Atenció al Client + ATCI: INFO + ATCF: FACTURES + ATCCB: COBRAMENTS + ATCR: RECLAMACIONS + ATCC: CONTRACTES + ATCCA: CONTRACTES - A + ATCCB: CONTRACTES - B + ATCCC: CONTRACTES - C + ATCCM: CONTRACTES - M + + """ + name: str + code: str # example: ATCCB + parent: CrmCaseSection + +class CrmCaseCategory: + """ + Categoria pels casos CRM (Taula mestra) + """ + name: str # example: "[ENTITATS I EMPRESES] Informació com contractar administradors d" + catec_code: str + section_id: CrmCaseSection + +class CrmCase: + """ + La base del cas, sigui o no reclamació. Tambe es fa servir per casos ATR. + """ + name: str + section_id: CrmCaseSection + categ_id: CrmCategory + categ_ids: list(CrmCategory) + +class GiscedataSubtipusReclamacio: + """ + Subtipus de reclamació (Taula mestra) + """ + default_section: CrmCaseSection + name: str # example: '003' + desc: str + +class GiscedataAtc(CrmCase): + """ + S'afegeix al cas CRM si és una reclamació + """ + subtipus_id: GiscedataSubtipusReclamacio + + From 3536fe3253ce24d80f5dfe47e08f71e3cbb57720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Mon, 24 Jan 2022 20:28:19 +0100 Subject: [PATCH 066/120] subtypes-as-categories: enhanced script --- .../202201-subtypes-as-categories.py | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/scripts/migrations/202201-subtypes-as-categories.py b/scripts/migrations/202201-subtypes-as-categories.py index eed37bbaa..b8b24f929 100755 --- a/scripts/migrations/202201-subtypes-as-categories.py +++ b/scripts/migrations/202201-subtypes-as-categories.py @@ -3,7 +3,10 @@ from erppeek import Client import dbconfig from yamlns import namespace as ns +from consolemsg import step, warn, success from erppeek_wst import ClientWST +import sys + erp = ClientWST(**dbconfig.erppeek) def loadData(erp, context): @@ -24,12 +27,12 @@ def loadData(erp, context): ] def apply(): - # Fix double '][' in INFOENERGIA name + step("Fix double '][' in INFOENERGIA name") erp.CrmCaseCateg.write(91, dict( name='[INFOENERGIA] Consulta sobre els seus informes', )) - # Add category codes to existing categories + step("Add category codes to existing categories") for id, code in ns.loads("""\ 83: 'IN01' # [INFO] Dubtes informació general (com fer-me soci, com omplir) 85: 'IN02' # [INFO] Sóc Soci i vull Convidar. No sóc Soci i vull contractar @@ -64,7 +67,7 @@ def apply(): categ_code=code, )) - # Create a new category for each claim subtypes + step("Create a new category for each claim subtype") for sub in context.subtypes: erp.CrmCaseCateg.create(dict( name = sub.desc, @@ -76,17 +79,27 @@ def apply(): try: erp.begin() context = ns() + + step("Loading state before (dumped as content-before.yaml)") loadData(erp, context) context.dump('content-before.yaml') + apply() + + step("Loading state after (dumped as content-after.yaml)") loadData(erp, context) context.dump('content-after.yaml') except: + warn("Error detected rolling back") erp.rollback() raise -finally: - erp.rollback() - #erp.commit() +else: + if '--apply' in sys.argv: + success("Applying changes") + erp.commit() + else: + warn("Use --apply to really apply. Rolling back changes.") + erp.rollback() From 74ad181ddcd6a0b428cba6d3cf0a0c8e67b4e7f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Mon, 24 Jan 2022 21:01:54 +0100 Subject: [PATCH 067/120] b2bdata: prepended [CONSULTA] to all info in listing --- ...st.Claims_Test.test_crmCategories-expected | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/b2bdata/tomatic.claims_test.Claims_Test.test_crmCategories-expected b/b2bdata/tomatic.claims_test.Claims_Test.test_crmCategories-expected index 01d3afac9..6ebe932c5 100644 --- a/b2bdata/tomatic.claims_test.Claims_Test.test_crmCategories-expected +++ b/b2bdata/tomatic.claims_test.Claims_Test.test_crmCategories-expected @@ -1,27 +1,28 @@ categories: -- '[INFO] Dubtes informació general (com fer-me soci, com omplir)' -- '[INFO] Sóc Soci i vull Convidar. No sóc Soci i vull contractar' -- '[INFO] Bo social (com es demana, qui hi pot tenir accés, etc)' -- '[INFO] No tinc llum, què faig?' -- '[OV] Demanen canvis que els hem de dirigir a l''OV ' -- '[OV] Problemes amb l''accés a OV (contrassenya, usuari, activació' -- '[OV] Consultes sobre les seccions (infoenergia i corbes, GkWh, i' -- '[FACTURA] Dubtes informació sobre les seves factures ' -- '[FACTURA]Dubtes informació sobre les lectures - vol donar lectur' -- '[FACTURA] Info comptadors, lloguer, canvi a telegestió, trifàsic' -- '[COBRAMENTS] Dubtes informació amb factures impagades i com fer ' -- '[COBRAMENTS] Informació sobre el tall de subministrament (com to' -- '[CONTRACTES] Tot tipus de consultes referents a altes d''un nou p' -- '[CONTRACTES] Tot tipus de consultes referents a baixes d’un punt' -- '[CONTRACTES] Informació procés contractació, endarreriments, reb' -- '[CONTRACTES] Informació sobre possible canvi de comer fraudulent' -- '[CONTRACTES] Quina potència necessito? Info sobre nova tarifa' -- '[CONTRACTES]Canvi de Titular - Canvi de Pagador. Com es fa, on, ' -- '[CONTRACTES] Tràmits sobre l’autoconsum (com es fa, documentació' -- '[ENTITATS I EMPRESES] Informació com contractar administradors d' -- '[ENTITATS I EMPRESES] Informació sobre tarifa 3.X TD i 6.X TD' -- '[PROJECTES - GENERACIÓ] Informació sobre les nostres plantes' -- '[AUTOPRODUCCIÓ] Informació sobre les compres col·lectives, com i' -- '[APORTACIONS - GKWH] Informació sobre les seves aportacions al G' -- '[COMUNICACIÓ] Comentar noticies blog, campanyes, newsletter, mai' -- '[GL - PARTICIPA - AULA POPULAR] Info sobre assemblea, sobre el P' +- '[CONSULTA] [INFO] Dubtes informació general (com fer-me soci, com omplir)' +- '[CONSULTA] [INFO] Sóc Soci i vull Convidar. No sóc Soci i vull contractar' +- '[CONSULTA] [INFO] Bo social (com es demana, qui hi pot tenir accés, etc)' +- '[CONSULTA] [INFO] No tinc llum, què faig?' +- '[CONSULTA] [OV] Demanen canvis que els hem de dirigir a l''OV ' +- '[CONSULTA] [OV] Problemes amb l''accés a OV (contrassenya, usuari, activació' +- '[CONSULTA] [OV] Consultes sobre les seccions (infoenergia i corbes, GkWh, i' +- '[CONSULTA] [INFOENERGIA] Consulta sobre els seus informes' +- '[CONSULTA] [FACTURA] Dubtes informació sobre les seves factures ' +- '[CONSULTA] [FACTURA]Dubtes informació sobre les lectures - vol donar lectur' +- '[CONSULTA] [FACTURA] Info comptadors, lloguer, canvi a telegestió, trifàsic' +- '[CONSULTA] [COBRAMENTS] Dubtes informació amb factures impagades i com fer ' +- '[CONSULTA] [COBRAMENTS] Informació sobre el tall de subministrament (com to' +- '[CONSULTA] [CONTRACTES] Tot tipus de consultes referents a altes d''un nou p' +- '[CONSULTA] [CONTRACTES] Tot tipus de consultes referents a baixes d’un punt' +- '[CONSULTA] [CONTRACTES] Informació procés contractació, endarreriments, reb' +- '[CONSULTA] [CONTRACTES] Informació sobre possible canvi de comer fraudulent' +- '[CONSULTA] [CONTRACTES] Quina potència necessito? Info sobre nova tarifa' +- '[CONSULTA] [CONTRACTES]Canvi de Titular - Canvi de Pagador. Com es fa, on, ' +- '[CONSULTA] [CONTRACTES] Tràmits sobre l’autoconsum (com es fa, documentació' +- '[CONSULTA] [ENTITATS I EMPRESES] Informació com contractar administradors d' +- '[CONSULTA] [ENTITATS I EMPRESES] Informació sobre tarifa 3.X TD i 6.X TD' +- '[CONSULTA] [PROJECTES - GENERACIÓ] Informació sobre les nostres plantes' +- '[CONSULTA] [AUTOPRODUCCIÓ] Informació sobre les compres col·lectives, com i' +- '[CONSULTA] [APORTACIONS - GKWH] Informació sobre les seves aportacions al G' +- '[CONSULTA] [COMUNICACIÓ] Comentar noticies blog, campanyes, newsletter, mai' +- '[CONSULTA] [GL - PARTICIPA - AULA POPULAR] Info sobre assemblea, sobre el P' From bb1688a0a17a7028c24d70366f5021c5bc1c8070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Mon, 24 Jan 2022 21:22:21 +0100 Subject: [PATCH 068/120] b2bdata: subtypes changed default section --- ...est.Claims_Test.test_atcCategories-expected | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/b2bdata/tomatic.claims_test.Claims_Test.test_atcCategories-expected b/b2bdata/tomatic.claims_test.Claims_Test.test_atcCategories-expected index 82a9cd3dd..bcda8451c 100644 --- a/b2bdata/tomatic.claims_test.Claims_Test.test_atcCategories-expected +++ b/b2bdata/tomatic.claims_test.Claims_Test.test_atcCategories-expected @@ -1,9 +1,9 @@ categories: -- '[RECLAMACIONS] 001. ATENCION INCORRECTA' +- '[ASSIGNAR USUARI] 001. ATENCION INCORRECTA' - '[RECLAMACIONS] 002. PRIVACIDAD DE LOS DATOS' -- '[RECLAMACIONS] 003. INCIDENCIA EN EQUIPOS DE MEDIDA' +- '[FACTURA] 003. INCIDENCIA EN EQUIPOS DE MEDIDA' - '[RECLAMACIONS] 004. DAÑOS ORIGINADOS POR EQUIPO DE MEDIDA' -- '[RECLAMACIONS] 005. CONTADOR EN FACTURA NO CORRESPONDE CON INSTALADO' +- '[FACTURA] 005. CONTADOR EN FACTURA NO CORRESPONDE CON INSTALADO' - '[FACTURA] 006. CONTRATOS ATR QUE NO SE FACTURAN' - '[FACTURA] 007. CUPS NO PERTENECE A COMERCIALIZADORA O NO VIGENTE EN PERIODO DE FACTURA' @@ -13,8 +13,8 @@ categories: - '[FACTURA] 011. RECLAMACIÓN FACTURA PAGO DUPLICADO' - '[FACTURA] 012. REFACTURACION NO RECIBIDA' - '[CONTRACTES - C] 013. DISCONFORMIDAD CON CAMBIO DE SUMINISTRADOR' -- '[RECLAMACIONS] 014. REQUERIMIENTO DE FIANZA / DEPÓSITO DE GARANTÍA' -- '[CONTRACTES - B] 015. RETRASO CORTE DE SUMINISTRO' +- '[CONTRACTES - C] 014. REQUERIMIENTO DE FIANZA / DEPÓSITO DE GARANTÍA' +- '[COBRAMENTS] 015. RETRASO CORTE DE SUMINISTRO' - '[FACTURA] 018. DISCONFORMIDAD CON CRITERIOS ECONÓMICOS / COBROS' - '[FACTURA] 019. DISCONFORMIDAD CON CRITERIOS TÉCNICOS / OBRA EJECUTADA' - '[RECLAMACIONS] 020. CALIDAD DE ONDA' @@ -29,7 +29,7 @@ categories: - '[ASSIGNAR USUARI] 029. RETRASO EN LA ATENCIÓN A RECLAMACIONES' - '[CONTRACTES - A] 030. RETRASO PLAZO DE CONTESTACIÓN NUEVOS SUMINISTROS' - '[CONTRACTES - A] 031. RETRASO PLAZO DE EJECUCIÓN NUEVO SUMINISTRO' -- '[CONTRACTES - B] 032. RETRASO REENGANCHE TRAS CORTE' +- '[COBRAMENTS] 032. RETRASO REENGANCHE TRAS CORTE' - '[CONTRACTES - C] 034. DISCONFORMIDAD CON CONCEPTOS DE CONTRATACIÓN ATR-PEAJE' - '[ASSIGNAR USUARI] 035. DISCONFORMIDAD RECHAZO SOLICITUD ATR-PEAJE' - '[FACTURA] 036. PETICIÓN DE REFACTURACIÓN APORTANDO LECTURA' @@ -40,8 +40,8 @@ categories: - '[RECLAMACIONS] 041. SOLICITUD DE ACTUACIÓN SOBRE INSTALACIONES' - '[RECLAMACIONS] 042. SOLICITUD DE DESCARGO' - '[RECLAMACIONS] 043. PETICIÓN DE PRECINTADO / DESPRECINTADO DE EQUIPOS' -- '[RECLAMACIONS] 044. PETICIONES CON ORIGEN EN CAMPAÑAS DE TELEGESTIÓN' -- '[RECLAMACIONS] 045. ACTUALIZACION DIRECCIÓN PUNTO DE SUMINISTRO' +- '[FACTURA] 044. PETICIONES CON ORIGEN EN CAMPAÑAS DE TELEGESTIÓN' +- '[CONTRACTES - C] 045. ACTUALIZACION DIRECCIÓN PUNTO DE SUMINISTRO' - '[FACTURA] 046. CERTIFICADO DE LECTURA' - '[FACTURA] 047. SOLICITUD RECALCULO CCH SIN MODIFICACION CIERRE ATR' - '[ASSIGNAR USUARI] 048. PETICIÓN INFORMACIÓN ADICIONAL RECHAZO' @@ -58,7 +58,7 @@ categories: - '[CONTRACTES - A] 063. RETRASO EN PLAZO ACTIVACIÓN ALTA DE UN NUEVO SUMINISTRO' - '[CONTRACTES - B] 064. RETRASO EN PLAZO ACEPTACIÓN DE UNA BAJA DE UN SUMINISTRO' - '[CONTRACTES - B] 065. RETRASO EN PLAZO ACTIVACIÓN BAJA DE UN SUMINISTRO' -- '[ASSIGNAR USUARI] 066. INFORMACIÓN/VALIDACIÓN SOBRE DATOS DEL CONTRATO ATR/PEAJE' +- '[CONTRACTES - M] 066. INFORMACIÓN/VALIDACIÓN SOBRE DATOS DEL CONTRATO ATR/PEAJE' - '[FACTURA] 067. VERIFICACIÓN DE CONTADOR' - '[Atenció al Client] 068. RECLAMACIÓN POR APLICACIÓN DEL FACTOR DE CONVERSIÓN O EL PCS' From 527746489eaeabf8adcaa12ee91a26119520071b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Mon, 24 Jan 2022 22:32:40 +0100 Subject: [PATCH 069/120] categories for crmcases, claim and info fixtures --- tomatic/claims.py | 53 ++++++++++++++++++++++++++++-------------- tomatic/claims_test.py | 53 +++++++++++++++++++++++------------------- 2 files changed, 65 insertions(+), 41 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index 2546166ec..f491e7630 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -4,6 +4,7 @@ from typing import Optional from enum import Enum from .persons import persons +from consolemsg import warn class Resolution(str, Enum): unsolved = 'unsolved' @@ -101,6 +102,15 @@ def crmSectionID(erp, section): sections_model = erp.CrmCaseSection return sections_model.search([('name', 'ilike', section)])[0] +def crmSectionHelpdesk(erp): + if hasattr(crmSectionHelpdesk, 'cached'): + return crmSectionHelpdesk.cached + section_ids = erp.CrmCaseSection.search([ + ('code','=','CI'), + ]) + assert section_ids, "A CRM Section with code CI should exist" + crmSectionHelpdesk.cached = section_ids[0] + return crmSectionHelpdesk(erp) class Claims(object): @@ -146,12 +156,27 @@ def create_crm_case(self, case): partner_id = partnerId(self.erp, case.partner) partner_address = partnerAddress(self.erp, partner_id) - crm_section_id = crmSectionID(self.erp, case.claimsection) + category_description = case.reason.split('.',1)[-1].strip() + categ_ids = self.erp.CrmCaseCateg.search([ + ('name', 'ilike', category_description), + ]) + if not categ_ids: + warn(f"Category not found {category_description}") + categ_id = False + else: + categ_id = categ_ids[0] + + crm_section_id = ( + crmSectionID(self.erp, case.claimsection) + if 'claimsection' in case and case.claimsection else + crmSectionHelpdesk(self.erp) + ) data_crm = { 'section_id': crm_section_id, - 'name': case.reason.split('.',1)[-1].strip(), + 'name': category_description, 'canal_id': PHONE_CHANNEL, + 'categ_id': categ_id, 'polissa_id': contractId(self.erp, case.contract), 'partner_id': partner_id, 'partner_address_id': partner_address.get('id') if partner_address else False, @@ -171,21 +196,15 @@ def create_crm_case(self, case): def create_atc_case(self, case): ''' Expected case: - - namespace( - person: - - date: D-M-YYYY H:M:S - user: person - reason: '[´section.name´] ´claim.name´. ´claim.desc´' - partner: partner number - contract: contract number - # maybe unsolved, fair, unfair, irresolvable or empty - resolution: fair - claimsection: section.name - notes: comments - - ... - ... - ) + date: D-M-YYYY H:M:S + user: person + reason: '[´section.name´] ´claim.name´. ´claim.desc´' + partner: partner number + contract: contract number + # maybe unsolved, fair, unfair, irresolvable or empty + resolution: fair + claimsection: section.name + notes: comments ''' CallAnnotation(**case) diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index 6e0bf32ac..106b825a2 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -64,11 +64,12 @@ def tearDown(self): from yamlns.testutils import assertNsEqual - def crmCase(self, case_id): - """Retrieves the data of a CrmCase to check its fields""" + def crmCase(self, case_id, deep=False): + """Retrieves checkeable erp fields for a CrmCase""" result = ns(self.erp.CrmCase.read(case_id, [ 'section_id', 'name', + 'categ_id', 'canal_id', 'polissa_id', 'partner_id', @@ -78,6 +79,7 @@ def crmCase(self, case_id): ])) fkname(result, "section_id") + fkname(result, "categ_id") fkname(result, "canal_id") fkname(result, "partner_id") fkname(result, "partner_address_id") @@ -133,7 +135,7 @@ def assertAtcCase(self, case_id, expected): result = self.atcCase(case_id) self.assertNsEqual(result, expected) - def atc_base(self, **kwds): + def claim_base(self, **kwds): """Provides a base for input atc cases to be feed""" base = ns.loads(""" date: '2021-11-11T15:13:39.998Z' @@ -144,21 +146,20 @@ def atc_base(self, **kwds): contract: '0013117' resolution: fair claimsection: RECLAMACIONS - notes: adfasd + notes: User annotated text """) base.update(**kwds) return base - def crm_base(self, **kwds): + def info_base(self, **kwds): base = ns.loads("""\ date: '2021-11-11T15:13:39.998Z' phone: '555444333' user: albert - reason: '[RECLAMACIONS] 003. INCIDENCIA EN EQUIPOS DE MEDIDA' + reason: "[COBRAMENTS] Informació sobre el tall de subministrament" partner: S001975 contract: '0013117' - claimsection: RECLAMACIONS - notes: adfasd + notes: User annotated text """) base.update(**kwds) return base @@ -175,7 +176,7 @@ def test_crmCategories(self): self.assertB2BEqual(ns(categories=categories).dump()) def test_createAtcCase_procedente(self): - case = self.atc_base() + case = self.claim_base() claims = Claims(self.erp) case_id = claims.create_atc_case(case) @@ -194,7 +195,7 @@ def test_createAtcCase_procedente(self): """.format(case_id)) def test_createAtcCase_improcedente(self): - case = self.atc_base( + case = self.claim_base( resolution='unfair', ) @@ -215,7 +216,7 @@ def test_createAtcCase_improcedente(self): """.format(case_id)) def test_createAtcCase_noSolution(self): - case = self.atc_base( + case = self.claim_base( resolution='irresolvable', ) @@ -236,7 +237,7 @@ def test_createAtcCase_noSolution(self): """.format(case_id)) def test_createAtcCase_unsolved(self): - case = self.atc_base( + case = self.claim_base( resolution='unsolved', ) @@ -257,29 +258,31 @@ def test_createAtcCase_unsolved(self): """.format(case_id)) def test_createCrmCase(self): - case = self.crm_base() + case = self.info_base() claims = Claims(self.erp) case_id = claims.create_crm_case(case) self.assertCrmCase(case_id, """\ canal_id: Teléfono + categ_id: '[COBRAMENTS] Informació sobre el tall de subministrament (com to' id: {} - name: INCIDENCIA EN EQUIPOS DE MEDIDA + name: '[COBRAMENTS] Informació sobre el tall de subministrament' partner_address_id: ...spí partner_id: ...osé polissa_id: '0013117' - section_id: Atenció al Client / RECLAMACIONS + section_id: HelpDesk state: open user_id: false """.format(case_id)) def test_createCrmCase_erpuserInPersons(self): - case = self.crm_base( + case = self.claim_base( user='marc', ) claims = Claims(self.erp) case_id = claims.create_crm_case(case) self.assertCrmCase(case_id, """\ canal_id: Teléfono + categ_id: INCIDENCIA EN EQUIPOS DE MEDIDA id: {} name: INCIDENCIA EN EQUIPOS DE MEDIDA partner_address_id: ...spí @@ -287,42 +290,44 @@ def test_createCrmCase_erpuserInPersons(self): polissa_id: '0013117' section_id: Atenció al Client / RECLAMACIONS state: open - user_id: ...lló + user_id: ...lló # <--THIS CHANGES """.format(case_id)) def test_createCrmCase_noContract(self): - case = self.crm_base( + case = self.info_base( contract = '', ) claims = Claims(self.erp) case_id = claims.create_crm_case(case) self.assertCrmCase(case_id, """\ canal_id: Teléfono + categ_id: '[COBRAMENTS] Informació sobre el tall de subministrament (com to' id: {} - name: INCIDENCIA EN EQUIPOS DE MEDIDA + name: '[COBRAMENTS] Informació sobre el tall de subministrament' partner_address_id: ...spí partner_id: ...osé polissa_id: False # <--------- THIS CHANGES - section_id: Atenció al Client / RECLAMACIONS + section_id: HelpDesk state: open user_id: false """.format(case_id)) def test_createCrmCase_noPartner(self): - case = self.crm_base( + case = self.info_base( contract = '', partner = '', ) claims = Claims(self.erp) case_id = claims.create_crm_case(case) self.assertCrmCase(case_id, """\ - canal_id: Teléfono id: {} - name: INCIDENCIA EN EQUIPOS DE MEDIDA + canal_id: Teléfono + categ_id: '[COBRAMENTS] Informació sobre el tall de subministrament (com to' + name: '[COBRAMENTS] Informació sobre el tall de subministrament' partner_address_id: False partner_id: False # <--------- THIS CHANGES polissa_id: False # <--------- THIS CHANGES - section_id: Atenció al Client / RECLAMACIONS + section_id: HelpDesk state: open user_id: false """.format(case_id)) From 60822ab73e695ce34b8ebeea00d9bd5760ddf9ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Mon, 24 Jan 2022 22:33:51 +0100 Subject: [PATCH 070/120] createCase: neutral entry for info and claims --- tomatic/claims.py | 8 ++++ tomatic/claims_test.py | 90 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 92 insertions(+), 6 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index f491e7630..df517c39d 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -237,5 +237,13 @@ def create_atc_case(self, case): def is_atc_case(self, case): return case.get('claimsection', '') != '' + def create_case(self, case): + if not self.is_atc_case(case): + return self.create_crm_case(case) + # TODO: we had this, why asking again? + atc_case_id = self.create_atc_case(case) + atc_case = self.erp.GiscedataAtc.read(atc_case_id, ['crm_id']) + return atc_case['crm_id'][0] + # vim: et ts=4 sw=4 diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index 106b825a2..0f3d88c3c 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -89,10 +89,18 @@ def crmCase(self, case_id, deep=False): anonymize(result, 'partner_address_id') anonymize(result, 'user_id') + if deep: + atcCase_id = self.erp.GiscedataAtc.search([ + ('crm_id', '=', case_id), + ]) + result.atc_id = self.atcCase(atcCase_id[0]) if atcCase_id else False + if atcCase_id: + del result.atc_id.id + return result - def atcCase(self, case_id): - """Retrieves the data of a AtcCase to check its fields""" + def atcCase(self, case_id, deep=False): + """Retrieves checkeable erp fields for an AtcCase""" result = ns(self.erp.GiscedataAtc.read(case_id, [ 'provincia', 'total_cups', @@ -104,6 +112,7 @@ def atcCase(self, case_id): 'email_from', 'time_tracking_id', 'state', + 'crm_id', ])) fkname(result, "cups_id") @@ -113,20 +122,26 @@ def atcCase(self, case_id): anonymize(result, "cups_id") anonymize(result, "email_from") + crm_id = result.pop('crm_id') + if deep: + result.crm_id = crm_id and self.crmCase(crm_id[0]) + if crm_id: + del result.crm_id.id + return result - def assertCrmCase(self, case_id, expected): - """Asserts that the CrmCase fields to be checked have + def assertCrmCase(self, case_id, expected, deep=False): + """Asserts that the CrmCase checkable erp fields do have the proper values""" if not expected: self.assertFalse(case_id) return self.assertTrue(case_id) - result = self.crmCase(case_id) + result = self.crmCase(case_id, deep=deep) self.assertNsEqual(result, expected) def assertAtcCase(self, case_id, expected): - """Asserts that the AtcCase fields to be checked have + """Asserts that the AtcCase checkable erp fields do have the proper values""" if not expected: self.assertFalse(case_id) @@ -332,5 +347,68 @@ def test_createCrmCase_noPartner(self): user_id: false """.format(case_id)) + def test_createCrmCase_withClaim(self): + case = self.claim_base() # <- This changes + claims = Claims(self.erp) + case_id = claims.create_crm_case(case) + self.assertCrmCase(case_id, """\ + canal_id: Teléfono + categ_id: INCIDENCIA EN EQUIPOS DE MEDIDA # <--- THIS CHANGES + id: {} + name: INCIDENCIA EN EQUIPOS DE MEDIDA # <--- THIS CHANGES + partner_address_id: ...spí + partner_id: ...osé + polissa_id: '0013117' + section_id: Atenció al Client / RECLAMACIONS # <--- THIS CHANGES + state: open + user_id: false + """.format(case_id)) + + def test_createCase_createsCrmAsWell(self): + case = self.claim_base() + claims = Claims(self.erp) + case_id = claims.create_case(case) + self.assertCrmCase(case_id, """\ + id: {} + canal_id: Teléfono + categ_id: INCIDENCIA EN EQUIPOS DE MEDIDA + name: INCIDENCIA EN EQUIPOS DE MEDIDA + partner_address_id: '...spí' + partner_id: '...osé' + polissa_id: '0013117' + section_id: Atenció al Client / RECLAMACIONS + state: open + user_id: false + atc_id: # <----- THIS CHANGES + cups_id: ...M0F + date: '2021-11-11 15:13:39.998' + email_from: ...oop + provincia: Barcelona + reclamante: '01' + resultat: '01' + subtipus_id: '003' + time_tracking_id: Comercialitzadora + state: open + total_cups: 1 + """.format(case_id), deep=True) + + def test_createCase_doesNotCreateCrmWhenItIsNot(self): + case = self.info_base() + claims = Claims(self.erp) + case_id = claims.create_case(case) + self.assertCrmCase(case_id, """\ + id: {} + canal_id: Teléfono + categ_id: '[COBRAMENTS] Informació sobre el tall de subministrament (com to' + name: '[COBRAMENTS] Informació sobre el tall de subministrament' + partner_address_id: '...spí' + partner_id: '...osé' + polissa_id: '0013117' + section_id: HelpDesk + state: open + user_id: false + atc_id: false # <----- THIS CHANGES + """.format(case_id), deep=True) + # vim: et ts=4 sw=4 From 5ceb4c8bcb55090f20e6cbac43bdfd9fdb028685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Mon, 24 Jan 2022 22:34:43 +0100 Subject: [PATCH 071/120] questionnaire: reasons list moved to callinfo --- tomatic/static/components/callinfo.js | 22 +++++++++++++++++++- tomatic/static/components/questionnaire.js | 24 ++-------------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index 1b4cf570d..60171b1d7 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -101,7 +101,27 @@ CallInfo.getExtras = function (extras) { return extras.map(function(extra) { return CallInfo.extras_dict[extra]; }); -} +}; + +CallInfo.filteredReasons = function(filter) { + var call_reasons = CallInfo.call_reasons; + function contains(value) { + var contains = value.toLowerCase().includes(filter.toLowerCase()); + return contains; + } + var list_reasons = [].concat( + call_reasons.infos, + call_reasons.general, + ); + + if (reason_filter === "") { + return list_reasons + } + var filtered_regular = list_reasons.filter(contains); + var filtered_extras = call_reasons.extras.filter(contains); + var extras = CallInfo.getExtras(filtered_extras); + return filtered_regular.concat(extras); +}; CallInfo.selectedPartner = function() { diff --git a/tomatic/static/components/questionnaire.js b/tomatic/static/components/questionnaire.js index c8a3b20aa..9e2777a72 100644 --- a/tomatic/static/components/questionnaire.js +++ b/tomatic/static/components/questionnaire.js @@ -23,7 +23,6 @@ var Login = require('./login'); var Questionnaire = {}; -var call_reasons = CallInfo.call_reasons; var reason_filter = ""; @@ -72,35 +71,16 @@ var saveLogCalls = function(phone, user, claim, contract, partner) { }); } -var filteredCaseCategories = function(categoriesFilter) { - var call_reasons = CallInfo.call_reasons; - function contains(value) { - var contains = value.toLowerCase().includes(categoriesFilter.toLowerCase()); - return contains; - } - var list_reasons = [].concat( - call_reasons.infos, - call_reasons.general, - ); - - if (reason_filter === "") { - return list_reasons - } - var filtered_regular = list_reasons.filter(contains); - var filtered_extras = call_reasons.extras.filter(contains); - var extras = CallInfo.getExtras(filtered_extras); - return filtered_regular.concat(extras); -} var llistaMotius = function() { - var filtered = filteredCaseCategories(reason_filter) + var reasons = CallInfo.filteredReasons(reason_filter) var disabled = (CallInfo.savingAnnotation || CallInfo.call.date === "" ); return m(".motius", m(List, { compact: true, indentedBorder: true, - tiles: filtered.map(function(reason) { + tiles: reasons.map(function(reason) { return m(ListTile, { className: ( CallInfo.call.reason === reason From 32d78245c96d08df140adbfb55b452fa93947956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Mon, 24 Jan 2022 22:54:21 +0100 Subject: [PATCH 072/120] claims: helpers inside class --- tomatic/claims.py | 97 +++++++++++++++++++++++------------------------ 1 file changed, 48 insertions(+), 49 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index df517c39d..065a7a4a9 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -38,48 +38,6 @@ def unknownState(erp): ])[0] return unknownState.cached -def partnerId(erp, partner_nif): - if not partner_nif: return None - partner = erp.ResPartner.search([ - ('ref', '=', partner_nif) - ]) - return partner[0] if partner else None - - -def partnerAddress(erp, partner_id): - if not partner_id: return None - return erp.ResPartnerAddress.read( - [('partner_id', '=', partner_id)], - ['id', 'state_id', 'email'] - )[0] - - -def contractId(erp, contract): - if not contract: return None - contract_id = erp.GiscedataPolissa.search([("name", "=", contract)]) - if contract_id: return contract_id[0] - -def erpUser(erp, person): - # Try with explicit erpuser in persons.yaml - erplogin = persons().get('erpusers',{}).get(person,None) - if erplogin: - user_ids = erp.ResUsers.search([ - ('login', '=', erplogin) - ]) - if user_ids: return user_ids[0] - # if not found try with email - email = persons().get('emails',{}).get(person,None) - if email: - address_ids = erp.ResPartnerAddress.search([ - ('email', '=', email), - ]) - user_ids = erp.ResUsers.search([ - ('address_id', 'in', address_ids), - ]) - if user_ids: return user_ids[0] - # No match found - return None - def resolutionCode(case): return dict( unsolved = '', @@ -117,6 +75,47 @@ class Claims(object): def __init__(self, erp): self.erp = erp + def partnerId(self, partner_nif): + if not partner_nif: return None + partner = self.erp.ResPartner.search([ + ('ref', '=', partner_nif) + ]) + return partner[0] if partner else None + + + def partnerAddress(self, partner_id): + if not partner_id: return None + return self.erp.ResPartnerAddress.read( + [('partner_id', '=', partner_id)], + ['id', 'state_id', 'email'] + )[0] + + def contractId(self, contract): + if not contract: return None + contract_id = self.erp.GiscedataPolissa.search([("name", "=", contract)]) + if contract_id: return contract_id[0] + + def erpUser(self, person): + # Try with explicit erpuser in persons.yaml + erplogin = persons().get('erpusers',{}).get(person,None) + if erplogin: + user_ids = self.erp.ResUsers.search([ + ('login', '=', erplogin) + ]) + if user_ids: return user_ids[0] + # if not found try with email + email = persons().get('emails',{}).get(person,None) + if email: + address_ids = self.erp.ResPartnerAddress.search([ + ('email', '=', email), + ]) + user_ids = self.erp.ResUsers.search([ + ('address_id', 'in', address_ids), + ]) + if user_ids: return user_ids[0] + # No match found + return None + def get_claims(self): claims_model = self.erp.GiscedataSubtipusReclamacio claims = [] @@ -153,8 +152,8 @@ def crm_categories(self): def create_crm_case(self, case): CallAnnotation(**case) - partner_id = partnerId(self.erp, case.partner) - partner_address = partnerAddress(self.erp, partner_id) + partner_id = self.partnerId(case.partner) + partner_address = self.partnerAddress(partner_id) category_description = case.reason.split('.',1)[-1].strip() categ_ids = self.erp.CrmCaseCateg.search([ @@ -177,11 +176,11 @@ def create_crm_case(self, case): 'name': category_description, 'canal_id': PHONE_CHANNEL, 'categ_id': categ_id, - 'polissa_id': contractId(self.erp, case.contract), + 'polissa_id': self.contractId(case.contract), 'partner_id': partner_id, 'partner_address_id': partner_address.get('id') if partner_address else False, 'state': 'open', # TODO: 'done' if case.solved else 'open', - 'user_id': erpUser(self.erp, case.user), + 'user_id': self.erpUser(case.user), } crm_id = self.erp.CrmCase.create(data_crm).id @@ -210,12 +209,12 @@ def create_atc_case(self, case): crm_case_id = self.create_crm_case(case) - partner_id = partnerId(self.erp, case.partner) - partner_address = partnerAddress(self.erp, partner_id) + partner_id = self.partnerId(case.partner) + partner_address = self.partnerAddress(partner_id) claim_section_id = claimSectionID( self.erp, case.reason.split('.',1)[-1].strip() ) - contract_id = contractId(self.erp, case.contract) + contract_id = self.contractId(case.contract) contract = self.erp.GiscedataPolissa.read(contract_id, ['cups']) state_id = partner_address.get('state_id')[0] if partner_address else unknownState(self.erp) data_atc = { From cd3f5839b6f2d8c3aa3fab9ed44bf77491837f94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Mon, 24 Jan 2022 22:56:22 +0100 Subject: [PATCH 073/120] claims: helpers marked as private --- tomatic/claims.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index 065a7a4a9..c4f7c49c6 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -75,7 +75,7 @@ class Claims(object): def __init__(self, erp): self.erp = erp - def partnerId(self, partner_nif): + def _partnerId(self, partner_nif): if not partner_nif: return None partner = self.erp.ResPartner.search([ ('ref', '=', partner_nif) @@ -83,19 +83,19 @@ def partnerId(self, partner_nif): return partner[0] if partner else None - def partnerAddress(self, partner_id): + def _partnerAddress(self, partner_id): if not partner_id: return None return self.erp.ResPartnerAddress.read( [('partner_id', '=', partner_id)], ['id', 'state_id', 'email'] )[0] - def contractId(self, contract): + def _contractId(self, contract): if not contract: return None contract_id = self.erp.GiscedataPolissa.search([("name", "=", contract)]) if contract_id: return contract_id[0] - def erpUser(self, person): + def _erpUser(self, person): # Try with explicit erpuser in persons.yaml erplogin = persons().get('erpusers',{}).get(person,None) if erplogin: @@ -152,8 +152,8 @@ def crm_categories(self): def create_crm_case(self, case): CallAnnotation(**case) - partner_id = self.partnerId(case.partner) - partner_address = self.partnerAddress(partner_id) + partner_id = self._partnerId(case.partner) + partner_address = self._partnerAddress(partner_id) category_description = case.reason.split('.',1)[-1].strip() categ_ids = self.erp.CrmCaseCateg.search([ @@ -176,11 +176,11 @@ def create_crm_case(self, case): 'name': category_description, 'canal_id': PHONE_CHANNEL, 'categ_id': categ_id, - 'polissa_id': self.contractId(case.contract), + 'polissa_id': self._contractId(case.contract), 'partner_id': partner_id, 'partner_address_id': partner_address.get('id') if partner_address else False, 'state': 'open', # TODO: 'done' if case.solved else 'open', - 'user_id': self.erpUser(case.user), + 'user_id': self._erpUser(case.user), } crm_id = self.erp.CrmCase.create(data_crm).id @@ -209,12 +209,12 @@ def create_atc_case(self, case): crm_case_id = self.create_crm_case(case) - partner_id = self.partnerId(case.partner) - partner_address = self.partnerAddress(partner_id) + partner_id = self._partnerId(case.partner) + partner_address = self._partnerAddress(partner_id) claim_section_id = claimSectionID( self.erp, case.reason.split('.',1)[-1].strip() ) - contract_id = self.contractId(case.contract) + contract_id = self._contractId(case.contract) contract = self.erp.GiscedataPolissa.read(contract_id, ['cups']) state_id = partner_address.get('state_id')[0] if partner_address else unknownState(self.erp) data_atc = { From 7e7548fac9684a440540b939f52b6cc5a03c547a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Mon, 24 Jan 2022 23:02:08 +0100 Subject: [PATCH 074/120] claims: claimSectionID as method --- tomatic/claims.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index c4f7c49c6..d367bb430 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -46,15 +46,6 @@ def resolutionCode(case): irresolvable = '03', ).get(case.resolution, 'bad') -def sectionName(erp, section_id): - claims_model = erp.GiscedataSubtipusReclamacio - return claims_model.read(section_id, ['desc']).get('desc') - - -def claimSectionID(erp, section_description): - claims_model = erp.GiscedataSubtipusReclamacio - return claims_model.search([('desc', '=', section_description)])[0] - def crmSectionID(erp, section): sections_model = erp.CrmCaseSection @@ -116,6 +107,10 @@ def _erpUser(self, person): # No match found return None + def _claimSectionID(self, section_description): + claims_model = self.erp.GiscedataSubtipusReclamacio + return claims_model.search([('desc', '=', section_description)])[0] + def get_claims(self): claims_model = self.erp.GiscedataSubtipusReclamacio claims = [] @@ -211,8 +206,8 @@ def create_atc_case(self, case): partner_id = self._partnerId(case.partner) partner_address = self._partnerAddress(partner_id) - claim_section_id = claimSectionID( - self.erp, case.reason.split('.',1)[-1].strip() + claim_section_id = self._claimSectionID( + case.reason.split('.',1)[-1].strip() ) contract_id = self._contractId(case.contract) contract = self.erp.GiscedataPolissa.read(contract_id, ['cups']) From bd42cfffd0664233734ceb9caa1719f728e82054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Mon, 24 Jan 2022 23:02:56 +0100 Subject: [PATCH 075/120] resolutionCode as Resolution enum method --- tomatic/claims.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index d367bb430..ae56f4f89 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -23,6 +23,15 @@ class CallAnnotation(BaseModel): claimsection: Optional[str] resolution: Optional[Resolution] + def resolutionCode(self): + return dict( + unsolved = '', + fair = '01', + unfair = '02', + irresolvable = '03', + ).get(self.resolution, 'bad') + + PHONE_CHANNEL = 2 TIME_TRACKER_COMERCIALIZADORA = 1 CLAIMANT = '01' # Titular de PS/ Usuario efectivo (Tabla 83) @@ -38,14 +47,6 @@ def unknownState(erp): ])[0] return unknownState.cached -def resolutionCode(case): - return dict( - unsolved = '', - fair = '01', - unfair = '02', - irresolvable = '03', - ).get(case.resolution, 'bad') - def crmSectionID(erp, section): sections_model = erp.CrmCaseSection @@ -218,7 +219,7 @@ def create_atc_case(self, case): 'cups_id': contract['cups'][0] if contract else None, 'subtipus_id': claim_section_id, 'reclamante': CLAIMANT, - 'resultat': resolutionCode(case), + 'resultat': case.resolutionCode(), 'date': case.date, 'email_from': partner_address.get('email') if partner_address else False, 'time_tracking_id': TIME_TRACKER_COMERCIALIZADORA, From f10dc0933ac32f844269d87227f4733932495078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Mon, 24 Jan 2022 23:08:30 +0100 Subject: [PATCH 076/120] reverted resolutionCode as method --- tomatic/claims.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index ae56f4f89..4d6379ff7 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -23,13 +23,13 @@ class CallAnnotation(BaseModel): claimsection: Optional[str] resolution: Optional[Resolution] - def resolutionCode(self): - return dict( - unsolved = '', - fair = '01', - unfair = '02', - irresolvable = '03', - ).get(self.resolution, 'bad') +def resolutionCode(case): + return dict( + unsolved = '', + fair = '01', + unfair = '02', + irresolvable = '03', + ).get(case.resolution, 'bad') PHONE_CHANNEL = 2 @@ -219,7 +219,7 @@ def create_atc_case(self, case): 'cups_id': contract['cups'][0] if contract else None, 'subtipus_id': claim_section_id, 'reclamante': CLAIMANT, - 'resultat': case.resolutionCode(), + 'resultat': resolutionCode(case), 'date': case.date, 'email_from': partner_address.get('email') if partner_address else False, 'time_tracking_id': TIME_TRACKER_COMERCIALIZADORA, From 41d2591c757ff903ec23827f41af2cc3553f4a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Mon, 24 Jan 2022 23:08:52 +0100 Subject: [PATCH 077/120] crmSectionID as method --- tomatic/claims.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index 4d6379ff7..9fc279fab 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -48,10 +48,6 @@ def unknownState(erp): return unknownState.cached -def crmSectionID(erp, section): - sections_model = erp.CrmCaseSection - return sections_model.search([('name', 'ilike', section)])[0] - def crmSectionHelpdesk(erp): if hasattr(crmSectionHelpdesk, 'cached'): return crmSectionHelpdesk.cached @@ -112,6 +108,10 @@ def _claimSectionID(self, section_description): claims_model = self.erp.GiscedataSubtipusReclamacio return claims_model.search([('desc', '=', section_description)])[0] + def _crmSectionID(self, section): + sections_model = self.erp.CrmCaseSection + return sections_model.search([('name', 'ilike', section)])[0] + def get_claims(self): claims_model = self.erp.GiscedataSubtipusReclamacio claims = [] @@ -162,7 +162,7 @@ def create_crm_case(self, case): categ_id = categ_ids[0] crm_section_id = ( - crmSectionID(self.erp, case.claimsection) + self._crmSectionID(case.claimsection) if 'claimsection' in case and case.claimsection else crmSectionHelpdesk(self.erp) ) From f843f5d7de2b92441d9038bae4590ac383f046a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Mon, 24 Jan 2022 23:17:31 +0100 Subject: [PATCH 078/120] corrected test names --- tomatic/claims_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index 0f3d88c3c..1e5f7be34 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -364,7 +364,7 @@ def test_createCrmCase_withClaim(self): user_id: false """.format(case_id)) - def test_createCase_createsCrmAsWell(self): + def test_createCase_withClaim_createsAtcAsWell(self): case = self.claim_base() claims = Claims(self.erp) case_id = claims.create_case(case) @@ -392,7 +392,7 @@ def test_createCase_createsCrmAsWell(self): total_cups: 1 """.format(case_id), deep=True) - def test_createCase_doesNotCreateCrmWhenItIsNot(self): + def test_createCase_withInfo_doesNotCreateAtc(self): case = self.info_base() claims = Claims(self.erp) case_id = claims.create_case(case) From 5ad7b6d316d1c5069edc35a8504aa8da595882a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Mon, 24 Jan 2022 23:48:36 +0100 Subject: [PATCH 079/120] Fix: With no claimSection return helpdesk --- tomatic/claims.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index 9fc279fab..cdbe2d06c 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -48,16 +48,6 @@ def unknownState(erp): return unknownState.cached -def crmSectionHelpdesk(erp): - if hasattr(crmSectionHelpdesk, 'cached'): - return crmSectionHelpdesk.cached - section_ids = erp.CrmCaseSection.search([ - ('code','=','CI'), - ]) - assert section_ids, "A CRM Section with code CI should exist" - crmSectionHelpdesk.cached = section_ids[0] - return crmSectionHelpdesk(erp) - class Claims(object): def __init__(self, erp): @@ -161,11 +151,7 @@ def create_crm_case(self, case): else: categ_id = categ_ids[0] - crm_section_id = ( - self._crmSectionID(case.claimsection) - if 'claimsection' in case and case.claimsection else - crmSectionHelpdesk(self.erp) - ) + crm_section_id = self._crmSectionID(case.get('claimsection', 'HelpDesk')) data_crm = { 'section_id': crm_section_id, From 44eeae2ebc2e83186dc81a26453d89ff145ec8fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Mon, 24 Jan 2022 23:49:58 +0100 Subject: [PATCH 080/120] Covering bad reason --- tomatic/claims_test.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index 1e5f7be34..e527288ee 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -364,6 +364,25 @@ def test_createCrmCase_withClaim(self): user_id: false """.format(case_id)) + def test_createCrmCase_witBadReason(self): + case = self.info_base( + reason="Bad reason", + ) + claims = Claims(self.erp) + case_id = claims.create_crm_case(case) + self.assertCrmCase(case_id, """\ + canal_id: Teléfono + categ_id: False + id: {} + name: Bad reason + partner_address_id: ...spí + partner_id: ...osé + polissa_id: '0013117' + section_id: HelpDesk + state: open + user_id: false + """.format(case_id)) + def test_createCase_withClaim_createsAtcAsWell(self): case = self.claim_base() claims = Claims(self.erp) From ba472e6ab5d4531d6c55e814b8403ca2bc1998ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 00:17:46 +0100 Subject: [PATCH 081/120] claimSectionId -> claimSubtypeByDescription --- tomatic/claims.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index cdbe2d06c..4a3f66b03 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -94,7 +94,7 @@ def _erpUser(self, person): # No match found return None - def _claimSectionID(self, section_description): + def _claimSubtypeByDescription(self, section_description): claims_model = self.erp.GiscedataSubtipusReclamacio return claims_model.search([('desc', '=', section_description)])[0] @@ -193,7 +193,7 @@ def create_atc_case(self, case): partner_id = self._partnerId(case.partner) partner_address = self._partnerAddress(partner_id) - claim_section_id = self._claimSectionID( + claim_subtype_id = self._claimSubtypeByDescription( case.reason.split('.',1)[-1].strip() ) contract_id = self._contractId(case.contract) @@ -203,7 +203,7 @@ def create_atc_case(self, case): 'provincia': state_id, 'total_cups': 1, 'cups_id': contract['cups'][0] if contract else None, - 'subtipus_id': claim_section_id, + 'subtipus_id': claim_subtype_id, 'reclamante': CLAIMANT, 'resultat': resolutionCode(case), 'date': case.date, From a6d32bce76556f223e1be038a6af4d3c1d216f26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 01:11:04 +0100 Subject: [PATCH 082/120] claims: avoid model attribs, tests 25% faster --- tomatic/claims.py | 28 ++++++++++++++++------------ tomatic/claims_test.py | 15 ++++++++------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index 4a3f66b03..2346b3e40 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -63,31 +63,33 @@ def _partnerId(self, partner_nif): def _partnerAddress(self, partner_id): if not partner_id: return None - return self.erp.ResPartnerAddress.read( + return self.erp.read('res.partner.address', [('partner_id', '=', partner_id)], ['id', 'state_id', 'email'] )[0] def _contractId(self, contract): if not contract: return None - contract_id = self.erp.GiscedataPolissa.search([("name", "=", contract)]) + contract_id = self.erp.search('giscedata.polissa',[ + ("name", "=", contract) + ]) if contract_id: return contract_id[0] def _erpUser(self, person): # Try with explicit erpuser in persons.yaml erplogin = persons().get('erpusers',{}).get(person,None) if erplogin: - user_ids = self.erp.ResUsers.search([ + user_ids = self.erp.search('res.users', [ ('login', '=', erplogin) ]) if user_ids: return user_ids[0] # if not found try with email email = persons().get('emails',{}).get(person,None) if email: - address_ids = self.erp.ResPartnerAddress.search([ + address_ids = self.erp.search('res.partner.address', [ ('email', '=', email), ]) - user_ids = self.erp.ResUsers.search([ + user_ids = self.erp.search('res.users', [ ('address_id', 'in', address_ids), ]) if user_ids: return user_ids[0] @@ -95,12 +97,14 @@ def _erpUser(self, person): return None def _claimSubtypeByDescription(self, section_description): - claims_model = self.erp.GiscedataSubtipusReclamacio - return claims_model.search([('desc', '=', section_description)])[0] + return self.erp.search('giscedata.subtipus.reclamacio', [ + ('desc', '=', section_description) + ])[0] def _crmSectionID(self, section): - sections_model = self.erp.CrmCaseSection - return sections_model.search([('name', 'ilike', section)])[0] + return self.erp.search('crm.case.section', [ + ('name', 'ilike', section) + ])[0] def get_claims(self): claims_model = self.erp.GiscedataSubtipusReclamacio @@ -142,7 +146,7 @@ def create_crm_case(self, case): partner_address = self._partnerAddress(partner_id) category_description = case.reason.split('.',1)[-1].strip() - categ_ids = self.erp.CrmCaseCateg.search([ + categ_ids = self.erp.search('crm.case.categ', [ ('name', 'ilike', category_description), ]) if not categ_ids: @@ -164,13 +168,13 @@ def create_crm_case(self, case): 'state': 'open', # TODO: 'done' if case.solved else 'open', 'user_id': self._erpUser(case.user), } - crm_id = self.erp.CrmCase.create(data_crm).id + crm_id = self.erp.create('crm.case', data_crm) data_history = { 'case_id': crm_id, 'description': case.notes, } - crm_history_id = self.erp.CrmCaseHistory.create(data_history).id + crm_history_id = self.erp.create('crm.case.history', data_history) return crm_id diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index e527288ee..3ef34add3 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -90,18 +90,17 @@ def crmCase(self, case_id, deep=False): anonymize(result, 'user_id') if deep: - atcCase_id = self.erp.GiscedataAtc.search([ + result.atc_id = self.atcCase([ ('crm_id', '=', case_id), ]) - result.atc_id = self.atcCase(atcCase_id[0]) if atcCase_id else False - if atcCase_id: + if result.atc_id: del result.atc_id.id return result - def atcCase(self, case_id, deep=False): + def atcCase(self, atc_case_id, deep=False): """Retrieves checkeable erp fields for an AtcCase""" - result = ns(self.erp.GiscedataAtc.read(case_id, [ + result = self.erp.GiscedataAtc.read(atc_case_id, [ 'provincia', 'total_cups', 'cups_id', @@ -113,7 +112,9 @@ def atcCase(self, case_id, deep=False): 'time_tracking_id', 'state', 'crm_id', - ])) + ]) + if not result: return False + result = ns(result[0]) fkname(result, "cups_id") fkname(result, "subtipus_id") @@ -147,7 +148,7 @@ def assertAtcCase(self, case_id, expected): self.assertFalse(case_id) return self.assertTrue(case_id) - result = self.atcCase(case_id) + result = self.atcCase([case_id]) self.assertNsEqual(result, expected) def claim_base(self, **kwds): From ef29414d731f139bddf652f99ab7be3da40785f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 03:07:13 +0100 Subject: [PATCH 083/120] fix: bad rename for reason filter parameter --- tomatic/static/components/callinfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index 60171b1d7..856ae7ae4 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -114,7 +114,7 @@ CallInfo.filteredReasons = function(filter) { call_reasons.general, ); - if (reason_filter === "") { + if (filter === "") { return list_reasons } var filtered_regular = list_reasons.filter(contains); From 7f02cb44b91202dad14605c5d8f922b6fdc880e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 03:08:41 +0100 Subject: [PATCH 084/120] reason list more compact --- tomatic/static/components/callinfo_style.styl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tomatic/static/components/callinfo_style.styl b/tomatic/static/components/callinfo_style.styl index 4d98f7296..ffb98793c 100644 --- a/tomatic/static/components/callinfo_style.styl +++ b/tomatic/static/components/callinfo_style.styl @@ -357,13 +357,17 @@ body margin-left: 10px .textfield-filter no_width: 445px - .motius no-display: inline-block background-color: #f5f5f5 no-width: 530px height: 40vh overflow-y: auto + .pe-list--compact + .pe-list-tile.pe-list-tile--compact + .pe-list-tile__content + padding-top: 4px + padding-bottom: 4px .pe-list-tile--disabled .pe-list-tile__title color: gray From 35bbd8fcf372432919fede186db396bcd0ef5632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 03:17:34 +0100 Subject: [PATCH 085/120] nicer reason list in kumato and normal mode --- tomatic/static/components/callinfo_style.styl | 28 ++++++++++++------- tomatic/static/components/questionnaire.js | 3 +- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/tomatic/static/components/callinfo_style.styl b/tomatic/static/components/callinfo_style.styl index ffb98793c..7cc931b21 100644 --- a/tomatic/static/components/callinfo_style.styl +++ b/tomatic/static/components/callinfo_style.styl @@ -359,11 +359,10 @@ body no_width: 445px .motius no-display: inline-block - background-color: #f5f5f5 no-width: 530px height: 40vh overflow-y: auto - .pe-list--compact + .pe-list.pe-list--compact .pe-list-tile.pe-list-tile--compact .pe-list-tile__content padding-top: 4px @@ -373,8 +372,9 @@ body color: gray .llista-motius-selected font-size: 14px - background-color: #e6ffe6 - color: black + nobackground-color: #e6ffe6 + background-color: #2a6a2a + color: white .llista-motius-unselected font-size: 14px color: black @@ -425,18 +425,26 @@ body .alert-case color: red -.pe-dark-tone +#tomatic > .pe-dark-tone + background: #373721 + color: #d9dfdf + min-height: 100% + .callinfo .all-info-call .card-attended-calls .attended-calls-list background: #242424 -#tomatic > .pe-dark-tone - nobackground: #3c1509 - background: #373721 - color: #d9dfdf - min-height: 100% + .card-annotate-case + .motius + .pe-list + .pe-list-tile.llista-motius-unselected + background-color: #333 + color: white + .pe-list-tile.llista-motius-selected + background-color: #3a3 + color: black // vim: et sw=2 ts=2 diff --git a/tomatic/static/components/questionnaire.js b/tomatic/static/components/questionnaire.js index 9e2777a72..5db386d5a 100644 --- a/tomatic/static/components/questionnaire.js +++ b/tomatic/static/components/questionnaire.js @@ -80,6 +80,7 @@ var llistaMotius = function() { return m(".motius", m(List, { compact: true, indentedBorder: true, + compact: true, tiles: reasons.map(function(reason) { return m(ListTile, { className: ( @@ -315,7 +316,7 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { m(".filter", m(Textfield, { className: "textfield-filter", - label: "Escriure per filtrar", + label: "Escriu per a filtrar", value: reason_filter, dense: true, onChange: function(params) { From 0e63c1e1de4a46c6590898e22d94d6bc02aff0f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 04:20:16 +0100 Subject: [PATCH 086/120] claims.categories() --- ..._test.Claims_Test.test_categories-expected | 587 ++++++++++++++++++ tomatic/claims.py | 65 ++ tomatic/claims_test.py | 5 + 3 files changed, 657 insertions(+) create mode 100644 b2bdata/tomatic.claims_test.Claims_Test.test_categories-expected diff --git a/b2bdata/tomatic.claims_test.Claims_Test.test_categories-expected b/b2bdata/tomatic.claims_test.Claims_Test.test_categories-expected new file mode 100644 index 000000000..14d13a064 --- /dev/null +++ b/b2bdata/tomatic.claims_test.Claims_Test.test_categories-expected @@ -0,0 +1,587 @@ +categories: + categories: + - name: '[APORTACIONS - GKWH] Informació sobre les seves aportacions al G' + code: AP01 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[AUTOPRODUCCIÓ] Informació sobre les compres col·lectives, com i' + code: AU01 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[COBRAMENTS] Dubtes informació amb factures impagades i com fer ' + code: CB01 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[COBRAMENTS] Informació sobre el tall de subministrament (com to' + code: CB02 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[COMUNICACIÓ] Comentar noticies blog, campanyes, newsletter, mai' + code: CO01 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[CONTRACTES] Tot tipus de consultes referents a altes d''un nou p' + code: CT01 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[CONTRACTES] Tot tipus de consultes referents a baixes d’un punt' + code: CT02 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[CONTRACTES] Informació procés contractació, endarreriments, reb' + code: CT03 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[CONTRACTES] Informació sobre possible canvi de comer fraudulent' + code: CT04 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[CONTRACTES] Quina potència necessito? Info sobre nova tarifa' + code: CT05 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[CONTRACTES]Canvi de Titular - Canvi de Pagador. Com es fa, on, ' + code: CT06 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[CONTRACTES] Tràmits sobre l’autoconsum (com es fa, documentació' + code: CT07 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[ENTITATS I EMPRESES] Informació com contractar administradors d' + code: EE01 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[ENTITATS I EMPRESES] Informació sobre tarifa 3.X TD i 6.X TD' + code: EE02 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[FACTURA] Dubtes informació sobre les seves factures ' + code: FA01 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[FACTURA]Dubtes informació sobre les lectures - vol donar lectur' + code: FA02 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[FACTURA] Info comptadors, lloguer, canvi a telegestió, trifàsic' + code: FA03 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[INFOENERGIA] Consulta sobre els seus informes' + code: IE01 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[INFO] Dubtes informació general (com fer-me soci, com omplir)' + code: IN01 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[INFO] Sóc Soci i vull Convidar. No sóc Soci i vull contractar' + code: IN02 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[INFO] Bo social (com es demana, qui hi pot tenir accés, etc)' + code: IN03 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[INFO] No tinc llum, què faig?' + code: IN04 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[OV] Demanen canvis que els hem de dirigir a l''OV ' + code: OV01 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[OV] Problemes amb l''accés a OV (contrassenya, usuari, activació' + code: OV02 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[OV] Consultes sobre les seccions (infoenergia i corbes, GkWh, i' + code: OV03 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[GL - PARTICIPA - AULA POPULAR] Info sobre assemblea, sobre el P' + code: PA01 + section: HelpDesk + isclaim: false + keywords: '' + - name: '[PROJECTES - GENERACIÓ] Informació sobre les nostres plantes' + code: PR01 + section: HelpDesk + isclaim: false + keywords: '' + - name: ATENCION INCORRECTA + code: R001 + section: null + isclaim: true + keywords: '' + - name: PRIVACIDAD DE LOS DATOS + code: R002 + section: RECLAMACIONS + isclaim: true + keywords: protecció de dades + - name: INCIDENCIA EN EQUIPOS DE MEDIDA + code: R003 + section: FACTURA + isclaim: true + keywords: comptador + - name: DAÑOS ORIGINADOS POR EQUIPO DE MEDIDA + code: R004 + section: RECLAMACIONS + isclaim: true + keywords: '' + - name: CONTADOR EN FACTURA NO CORRESPONDE CON INSTALADO + code: R005 + section: FACTURA + isclaim: true + keywords: creuament comptadors + - name: CONTRATOS ATR QUE NO SE FACTURAN + code: R006 + section: FACTURA + isclaim: true + keywords: endarrerida + - name: 'CUPS NO PERTENECE A COMERCIALIZADORA O NO VIGENTE EN PERIODO DE ' + code: R007 + section: FACTURA + isclaim: true + keywords: '' + - name: DISCONFORMIDAD CON CONCEPTOS FACTURADOS + code: R008 + section: FACTURA + isclaim: true + keywords: '' + - name: DISCONFORMIDAD CON LECTURA FACTURADA + code: R009 + section: FACTURA + isclaim: true + keywords: estimada + - name: DISCONFORMIDAD EN FACTURA ANOMALÍA / FRAUDE + code: R010 + section: FACTURA + isclaim: true + keywords: expedient + - name: RECLAMACIÓN FACTURA PAGO DUPLICADO + code: R011 + section: FACTURA + isclaim: true + keywords: '' + - name: REFACTURACION NO RECIBIDA + code: R012 + section: FACTURA + isclaim: true + keywords: '' + - name: DISCONFORMIDAD CON CAMBIO DE SUMINISTRADOR + code: R013 + section: CONTRACTES - C + isclaim: true + keywords: '' + - name: REQUERIMIENTO DE FIANZA / DEPÓSITO DE GARANTÍA + code: R014 + section: CONTRACTES - C + isclaim: true + keywords: '' + - name: RETRASO CORTE DE SUMINISTRO + code: R015 + section: COBRAMENTS + isclaim: true + keywords: '' + - name: DISCONFORMIDAD CON CRITERIOS ECONÓMICOS / COBROS + code: R018 + section: FACTURA + isclaim: true + keywords: '' + - name: DISCONFORMIDAD CON CRITERIOS TÉCNICOS / OBRA EJECUTADA + code: R019 + section: FACTURA + isclaim: true + keywords: '' + - name: CALIDAD DE ONDA + code: R020 + section: RECLAMACIONS + isclaim: true + keywords: '' + - name: CON PETICIÓN DE INDEMNIZACIÓN + code: R021 + section: RECLAMACIONS + isclaim: true + keywords: '' + - name: SIN PETICIÓN DE INDEMNIZACIÓN + code: R022 + section: RECLAMACIONS + isclaim: true + keywords: '' + - name: RETRASO EN PAGO INDEMNIZACION + code: R023 + section: RECLAMACIONS + isclaim: true + keywords: '' + - name: DAÑOS A TERCEROS POR INSTALACIONES + code: R024 + section: RECLAMACIONS + isclaim: true + keywords: '' + - name: IMPACTO AMBIENTAL INSTALACIONES + code: R025 + section: RECLAMACIONS + isclaim: true + keywords: '' + - name: RECLAMACIONES SOBRE INSTALACIONES + code: R026 + section: RECLAMACIONS + isclaim: true + keywords: '' + - name: DISCONFORMIDAD DESCUENTO SERVICIO INDIVIDUAL + code: R027 + section: RECLAMACIONS + isclaim: true + keywords: '' + - name: EJECUCIÓN INDEBIDA DE CORTE + code: R028 + section: RECLAMACIONS + isclaim: true + keywords: '' + - name: RETRASO EN LA ATENCIÓN A RECLAMACIONES + code: R029 + section: null + isclaim: true + keywords: '' + - name: RETRASO PLAZO DE CONTESTACIÓN NUEVOS SUMINISTROS + code: R030 + section: CONTRACTES - A + isclaim: true + keywords: '' + - name: RETRASO PLAZO DE EJECUCIÓN NUEVO SUMINISTRO + code: R031 + section: CONTRACTES - A + isclaim: true + keywords: altres + - name: RETRASO REENGANCHE TRAS CORTE + code: R032 + section: COBRAMENTS + isclaim: true + keywords: '' + - name: POR URGENCIAS + code: R033 + section: Atenció al Client + isclaim: true + keywords: '' + - name: DISCONFORMIDAD CON CONCEPTOS DE CONTRATACIÓN ATR-PEAJE + code: R034 + section: CONTRACTES - C + isclaim: true + keywords: '' + - name: DISCONFORMIDAD RECHAZO SOLICITUD ATR-PEAJE + code: R035 + section: null + isclaim: true + keywords: '' + - name: PETICIÓN DE REFACTURACIÓN APORTANDO LECTURA + code: R036 + section: FACTURA + isclaim: true + keywords: '' + - name: FICHERO XML INCORRECTO + code: R037 + section: FACTURA + isclaim: true + keywords: '' + - name: PRIVACIDAD DE LOS DATOS + code: R038 + section: RECLAMACIONS + isclaim: true + keywords: '' + - name: SOLICITUD DE CERTIFICADO / INFORME DE CALIDAD + code: R039 + section: RECLAMACIONS + isclaim: true + keywords: '' + - name: SOLICITUD DE DUPLICADO DE FACTURA + code: R040 + section: FACTURA + isclaim: true + keywords: '' + - name: SOLICITUD DE ACTUACIÓN SOBRE INSTALACIONES + code: R041 + section: RECLAMACIONS + isclaim: true + keywords: '' + - name: SOLICITUD DE DESCARGO + code: R042 + section: RECLAMACIONS + isclaim: true + keywords: '' + - name: PETICIÓN DE PRECINTADO / DESPRECINTADO DE EQUIPOS + code: R043 + section: RECLAMACIONS + isclaim: true + keywords: '' + - name: PETICIONES CON ORIGEN EN CAMPAÑAS DE TELEGESTIÓN + code: R044 + section: FACTURA + isclaim: true + keywords: '' + - name: ACTUALIZACION DIRECCIÓN PUNTO DE SUMINISTRO + code: R045 + section: CONTRACTES - C + isclaim: true + keywords: '' + - name: CERTIFICADO DE LECTURA + code: R046 + section: FACTURA + isclaim: true + keywords: '' + - name: SOLICITUD RECALCULO CCH SIN MODIFICACION CIERRE ATR + code: R047 + section: FACTURA + isclaim: true + keywords: '' + - name: PETICIÓN INFORMACIÓN ADICIONAL RECHAZO + code: R048 + section: null + isclaim: true + keywords: '' + - name: FALTA FICHERO MEDIDA + code: R049 + section: FACTURA + isclaim: true + keywords: '' + - name: DESACUERDO FACTURACIÓN + code: R050 + section: Atenció al Client + isclaim: true + keywords: '' + - name: CONDUCTA INADECUADA + code: R051 + section: Atenció al Client + isclaim: true + keywords: '' + - name: DISCONFORMIDAD TRABAJOS REALIZADOS + code: R052 + section: Atenció al Client + isclaim: true + keywords: '' + - name: INCUMPLIMIENTO HORA + code: R053 + section: Atenció al Client + isclaim: true + keywords: '' + - name: DAÑOS INSPECCIÓN + code: R054 + section: Atenció al Client + isclaim: true + keywords: '' + - name: DISCONFORMIDAD SOBRE IMPORTE FACTURADO AUTOCONSUMO + code: R055 + section: FACTURA + isclaim: true + keywords: '' + - name: PETICIÓN DESGLOSE IMPORTE A FACTURAR AUTOCONSUMO + code: R056 + section: FACTURA + isclaim: true + keywords: '' + - name: 'DISCONFORMIDAD CON EXPEDIENTE DE ANOMALIA Y FRAUDE (sin factura ' + code: R057 + section: RECLAMACIONS + isclaim: true + keywords: expedient + - name: RETRASO EN PLAZO ACEPTACIÓN CAMBIO DE COMERCIALIZADOR + code: R058 + section: CONTRACTES - C + isclaim: true + keywords: '' + - name: 'RETRASO EN PLAZO ACTIVACIÓN CAMBIO DE COMERCIALIZADOR ' + code: R059 + section: CONTRACTES - C + isclaim: true + keywords: '' + - name: RETRASO EN PLAZO ACEPTACIÓN MODIFICACIÓN CONTRACTUAL + code: R060 + section: CONTRACTES - M + isclaim: true + keywords: '' + - name: RETRASO EN PLAZO ACTIVACIÓN MODIFICACIÓN CONTRACTUAL + code: R061 + section: CONTRACTES - M + isclaim: true + keywords: '' + - name: RETRASO EN PLAZO ACEPTACIÓN ALTA DE UN NUEVO SUMINISTRO + code: R062 + section: CONTRACTES - A + isclaim: true + keywords: '' + - name: RETRASO EN PLAZO ACTIVACIÓN ALTA DE UN NUEVO SUMINISTRO + code: R063 + section: CONTRACTES - A + isclaim: true + keywords: '' + - name: RETRASO EN PLAZO ACEPTACIÓN DE UNA BAJA DE UN SUMINISTRO + code: R064 + section: CONTRACTES - B + isclaim: true + keywords: '' + - name: RETRASO EN PLAZO ACTIVACIÓN BAJA DE UN SUMINISTRO + code: R065 + section: CONTRACTES - B + isclaim: true + keywords: '' + - name: INFORMACIÓN/VALIDACIÓN SOBRE DATOS DEL CONTRATO ATR/PEAJE + code: R066 + section: CONTRACTES - M + isclaim: true + keywords: '' + - name: VERIFICACIÓN DE CONTADOR + code: R067 + section: FACTURA + isclaim: true + keywords: '' + - name: RECLAMACIÓN POR APLICACIÓN DEL FACTOR DE CONVERSIÓN O EL PCS + code: R068 + section: Atenció al Client + isclaim: true + keywords: '' + - name: COPIA F1 EN PDF + code: R069 + section: FACTURA + isclaim: true + keywords: '' + - name: RETRASO EN LA ATENCIÓN A RECLAMACIONES NO SUJETAS A ATENCIÓN REG + code: R070 + section: null + isclaim: true + keywords: '' + - name: RETRASO EN PLAZO DE ACEPTACIÓN DESISTIMIENTO + code: R071 + section: null + isclaim: true + keywords: '' + - name: RETRASO EN PLAZO DE ACTIVACIÓN DESISTIMIENTO + code: R072 + section: null + isclaim: true + keywords: '' + - name: PARÁMETROS DE COMUNICACIÓN + code: R073 + section: null + isclaim: true + keywords: '' + - name: RETRASO EN PLAZO DE ACEPTACIÓN ANULACIÓN + code: R074 + section: null + isclaim: true + keywords: '' + - name: INCIDENCIAS CONTRATACIÓN BONO SOCIAL + code: R100 + section: CONTRACTES - C + isclaim: true + keywords: '' + - name: DATOS BANCARIOS/FORMA DE PAGO ERRÓNEA + code: R101 + section: COBRAMENTS + isclaim: true + keywords: compte bancari + - name: ERRORES EN COBROS/ ABONOS + code: R102 + section: COBRAMENTS + isclaim: true + keywords: duplicat + - name: DISCONFORMIDAD PRECIOS FACTURADOS O REPERCUTIDOS POR LA COMERCIA + code: R103 + section: FACTURA + isclaim: true + keywords: '' + - name: DISCONFORMIDAD FRACCIONAMIENTO O GASTOS ESPECIALES COBRADOS + code: R104 + section: COBRAMENTS + isclaim: true + keywords: '' + - name: DISCONFORMIDAD CON EL RECOBRO + code: R105 + section: COBRAMENTS + isclaim: true + keywords: '' + - name: DISCONFORMIDAD CON PENALIZACIÓN POR PRONTA RESOLUCIÓN + code: R106 + section: RECLAMACIONS + isclaim: true + keywords: '' + - name: INSUFICIENTE INFORMACIÓN EN EL MOMENTO DE LA CONTRATACIÓN (Condi + code: R107 + section: CONTRACTES - C + isclaim: true + keywords: '' + - name: RECLAMACION RESPECTO AL DERECHO DE DESISTIMIENTO + code: R108 + section: CONTRACTES - C + isclaim: true + keywords: '' + - name: FACTURACION DE OTROS SERVICIOS TRAS LA CANCELACIÓN DEL SUMINISTR + code: R109 + section: FACTURA + isclaim: true + keywords: '' + - name: 'FALTA DE CLARIDAD EN LAS FACTURAS ' + code: R110 + section: FACTURA + isclaim: true + keywords: no entenen factures + - name: FALTA DE CLARIDAD CONDICIONES CONTRACTUALES + code: R111 + section: RECLAMACIONS + isclaim: true + keywords: no entenen contracte + - name: DIFICULTAD EN LA CONTRATACIÓN DE LA TUR/PVPC CON EL CUR/COR + code: R112 + section: RECLAMACIONS + isclaim: true + keywords: '' + - name: RECLAMACIONES POR PRACTICAS COMERCIALES INCORRECTAS + code: R113 + section: RECLAMACIONS + isclaim: true + keywords: '' + - name: RETRASO EN FACTURACIÓN COMERCIALIZADOR + code: R114 + section: FACTURA + isclaim: true + keywords: encallada endarrerida + sections: + - code: ATCF + name: FACTURA + - code: ATCR + name: RECLAMACIONS + - code: ATCCB + name: COBRAMENTS + - code: ATCC-A + name: CONTRACTES - A + - code: ATCC-B + name: CONTRACTES - B + - code: ATCC-C + name: CONTRACTES - C + - code: ATCC-M + name: CONTRACTES - M diff --git a/tomatic/claims.py b/tomatic/claims.py index 2346b3e40..56e629486 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -131,6 +131,71 @@ def get_claims(self): return claims + def _last_path(self, fullpath): + return fullpath.split('/')[-1].strip() + + def categories(self): + # TODO: Use a config file or a db backend + keywords = ns.loads(""" + R002: protecció de dades + R003: comptador + R005: creuament comptadors + R006: endarrerida + R009: estimada + R010: expedient + R031: altres + R057: expedient + R101: compte bancari + R102: duplicat + R110: no entenen factures + R111: no entenen contracte + R114: encallada endarrerida + """) + + ids = self.erp.search('crm.case.section', []) + sections = [ ns(s) for s in self.erp.read('crm.case.section', [ + ('code', 'ilike', 'ATC'), + ('name', '!=', 'INFO'), + ], [ + 'name', + 'code', + 'parent_id', + ] + )] + parentSections = { s.parent_id[0] for s in sections if s.parent_id } + sections = [s for s in sections if s.id not in parentSections] + + ids = self.erp.search('crm.case.categ', []) + categories = [ + ns(x) for x in self.erp.read('crm.case.categ', ids, [ + 'name', + 'desc', + 'categ_code', + 'section_id', + ]) or [] + ] + return ns( + categories=[ + ns( + name = cat.name, + code = cat.categ_code, + section = self._last_path(cat.section_id[1]) if cat.section_id else None, + isclaim = cat.categ_code and cat.categ_code[0] == 'R', + keywords = keywords.get(cat.categ_code,''), + ) + for cat in sorted(categories, key=lambda x:x.categ_code or '') + if cat.name[:1] == '[' + or (cat.categ_code and cat.categ_code[0] == 'R') + ], + sections=[ + dict( + code = s.code, + name = s.name, + ) + for s in (ns(x) for x in sections) + ], + ) + def crm_categories(self): ids = self.erp.CrmCaseCateg.search([]) return [ diff --git a/tomatic/claims_test.py b/tomatic/claims_test.py index 3ef34add3..6e6203f74 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -191,6 +191,11 @@ def test_crmCategories(self): categories = claims.crm_categories() self.assertB2BEqual(ns(categories=categories).dump()) + def test_categories(self): + claims = Claims(self.erp) + categories = claims.categories() + self.assertB2BEqual(ns(categories=categories).dump()) + def test_createAtcCase_procedente(self): case = self.claim_base() From d4b9870584606fe82046ec1fe2a3f79ce359a99e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 04:40:48 +0100 Subject: [PATCH 087/120] section options moved to callinfo --- tomatic/static/components/callinfo.js | 12 ++++++++++++ tomatic/static/components/questionnaire.js | 10 +--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index 856ae7ae4..2cf3d8e17 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -76,6 +76,18 @@ CallInfo.callSelected = function(date, phone) { retrieveInfo(); } +CallInfo.selectableSections = function() { + return [ + "RECLAMA", + "FACTURA", + "COBRAMENTS", + "ATR A - COMER", + "ATR B - COMER", + "ATR C - COMER", + "ATR M - COMER" + ]; +} + function formatContractNumber(number) { // some contract numbers get converted to int and lose their padding diff --git a/tomatic/static/components/questionnaire.js b/tomatic/static/components/questionnaire.js index 5db386d5a..f541d9f49 100644 --- a/tomatic/static/components/questionnaire.js +++ b/tomatic/static/components/questionnaire.js @@ -169,15 +169,7 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { var seleccionaUsuari = function(reclamacio, tag) { var section = tag; - var options = [ - "RECLAMA", - "FACTURA", - "COBRAMENTS", - "ATR A - COMER", - "ATR B - COMER", - "ATR C - COMER", - "ATR M - COMER" - ] + var options = CallInfo.selectableSections() return m("", [ m("p", "Equip: " ), m("select", From 20f38faa1ce9a55d430b06b1bfbb2faa61217d2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 04:51:01 +0100 Subject: [PATCH 088/120] changelog --- TODO.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/TODO.md b/TODO.md index 466eb4cd6..4abfe3985 100644 --- a/TODO.md +++ b/TODO.md @@ -57,16 +57,18 @@ - [ ] Dubte AiS: renombrar user -> seccion/team (preguntar a AiS per terminologia de domini) - [ ] Dubte AiS: tenim les llistes a produccio que fem amb elles (mostrarles perque hi ha brossa i textos que poden canviar) -- [ ] callinfo log: unite resolution fields -- [ ] callinfo log: join infos and claims +- [ ] entry point to obtain categories +- [ ] encapsulate access to the categories info in frontend +- [x] callinfo log: unite resolution fields +- [x] callinfo log: join infos and claims - [ ] callinfo log: join infos/claims with log? (consider performance and usage) -- [ ] Importar categories que falten de atc com a categorias de crmcases -- [ ] anotate_case: sensitive to the case fields creates atc or not +- [x] Importar categories que falten de atc com a categorias de crmcases +- [x] anotate_case: sensitive to the case fields creates atc or not - [ ] !!! create crm: solved = True (lo comentamos cuando lo movimos a una funcion a parte) - [ ] create crm: extract seccio del reason and remove the field - [ ] create crm: cas contracte no existeix - [ ] callreg: Rename Claims to reflect its repurposing -- [ ] callreg: create crm: Inserir usuari correcte al CRM (es fa servir l'usuari loggejat a l'erp: Scriptlauncher i no veiem com canviar-ho) +- [x] callreg: create crm: Inserir usuari correcte al CRM (es fa servir l'usuari loggejat a l'erp: Scriptlauncher i no veiem com canviar-ho) - [ ] callreg: On failing annotation, ui notifies the user From 8e5f590539ce8cac8faa54b52eab91d3444b8a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 04:51:47 +0100 Subject: [PATCH 089/120] icon needs a container --- tomatic/claims.py | 1 - tomatic/static/components/questionnaire.js | 1 - 2 files changed, 2 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index 56e629486..3d2efa628 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -204,7 +204,6 @@ def crm_categories(self): if category['name'].startswith('[') ] - def create_crm_case(self, case): CallAnnotation(**case) partner_id = self._partnerId(case.partner) diff --git a/tomatic/static/components/questionnaire.js b/tomatic/static/components/questionnaire.js index f541d9f49..667544e32 100644 --- a/tomatic/static/components/questionnaire.js +++ b/tomatic/static/components/questionnaire.js @@ -118,7 +118,6 @@ Questionnaire.annotationButton = function() { var contract = CallInfo.selectedContract(); return m("", m(IconButton, { - icon: m("i.far.fa-clipboard.icon-clipboard"), icon: clipboardIcon(), wash: true, compact: true, From 198754ae3abea157b45dc058914d7def58f69599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 04:53:04 +0100 Subject: [PATCH 090/120] fix: returns ends the same line --- tomatic/claims.py | 2 +- tomatic/static/components/questionnaire.js | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/tomatic/claims.py b/tomatic/claims.py index 3d2efa628..d1a9252c9 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -250,7 +250,7 @@ def create_atc_case(self, case): reason: '[´section.name´] ´claim.name´. ´claim.desc´' partner: partner number contract: contract number - # maybe unsolved, fair, unfair, irresolvable or empty + # maybe unsolved, fair, unfair, irresolvable or null resolution: fair claimsection: section.name notes: comments diff --git a/tomatic/static/components/questionnaire.js b/tomatic/static/components/questionnaire.js index 667544e32..e56b1c31f 100644 --- a/tomatic/static/components/questionnaire.js +++ b/tomatic/static/components/questionnaire.js @@ -182,16 +182,11 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { }, }, options.map(function(option) { - return - m("option", { + return m("option", { "value": option, "selected": section === option }, option); - }), - m("option", { - "value": section, - "selected": !options.includes(section) - }, section) + }) ) ]); } From e35d272ce6d4858935f89e4b15733b8a161fd9e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 04:54:34 +0100 Subject: [PATCH 091/120] Rename preguntarResolt -> resolutionOptions --- tomatic/static/components/questionnaire.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tomatic/static/components/questionnaire.js b/tomatic/static/components/questionnaire.js index e56b1c31f..9050d5b2c 100644 --- a/tomatic/static/components/questionnaire.js +++ b/tomatic/static/components/questionnaire.js @@ -191,7 +191,7 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { ]); } - var preguntarResolt = function(reclamacio) { + var resolutionOptions = function(reclamacio) { return m(".case-resolution", [ m("p", "Resolució:"), m(RadioGroup, { @@ -258,7 +258,7 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { body: [ seleccionaUsuari(reclamacio, tag), m("br"), - preguntarResolt(reclamacio), + resolutionOptions(reclamacio), ], footerButtons: buttons(reclamacio), };},{id:'fillReclama'}); From 67f1455059cd5fa1a430c75e87214f9db8b0e2c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 11:00:00 +0100 Subject: [PATCH 092/120] section selection logic moved to CallInfo --- tomatic/static/components/callinfo.js | 21 +++++++++++ tomatic/static/components/questionnaire.js | 44 +++++++++++----------- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index 2cf3d8e17..35233dfb2 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -34,7 +34,28 @@ CallInfo.call_reasons = { 'extras': [] } CallInfo.extras_dict = {}; + CallInfo.savingAnnotation = false; +CallInfo.annotation = {}; + +CallInfo.resetAnnotation = function(tag) { + CallInfo.annotation = { + resolution: 'unsolved', + tag: tag, + } +}; + +CallInfo.annotationRequiresSection = function() { + return CallInfo.annotation === "ASSIGNAR USUARI"; +}; +CallInfo.annotationReasonTag = function() { + var reason = CallInfo.call.reason; + var matches = reason.match(/\[(.*?)\]/); + if (matches) { + return matches[1].trim(); + } + return ""; +}; CallInfo.clear = function() { diff --git a/tomatic/static/components/questionnaire.js b/tomatic/static/components/questionnaire.js index 9050d5b2c..abcbd8011 100644 --- a/tomatic/static/components/questionnaire.js +++ b/tomatic/static/components/questionnaire.js @@ -166,25 +166,30 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { return (type != info); } - var seleccionaUsuari = function(reclamacio, tag) { - var section = tag; + var sectionSelector = function(reasonTag) { + var reclamacio = CallInfo.annotation; var options = CallInfo.selectableSections() + var selectable = reasonTag === "ASSIGNAR USUARI"; return m("", [ m("p", "Equip: " ), m("select", { id: "select-user", class: ".select-user", - disabled: section !== "ASSIGNAR USUARI", - default: section, - onchange: function() { - reclamacio.tag = document.getElementById("select-user").value; + disabled: !selectable, + default: reasonTag, + oninput: function(ev) { + reclamacio.tag = ev.target.value; }, }, - options.map(function(option) { + m("option", { + value: reasonTag, + selected: !options.includes(reasonTag) + }, reasonTag), + selectable && options.map(function(option) { return m("option", { "value": option, - "selected": section === option + "selected": reclamacio.tag === option }, option); }) ) @@ -197,9 +202,9 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { m(RadioGroup, { name: 'resolution', onChange: function(state) { - reclamacio.resolution = state.value; + CallInfo.annotation.resolution = state.value; }, - checkedValue: reclamacio.resolution, + checkedValue: CallInfo.annotation.resolution, buttons: [{ defaultChecked: true, label: "No resolt", @@ -218,7 +223,7 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { ]) } - var buttons = function(reclamacio) { + var buttons = function() { return [ m(Button, { label: "Cancel·lar", @@ -233,12 +238,12 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { label: "Desa", events: { onclick: function() { - enviar(reclamacio); + enviar(Callinfo.annotation); Dialog.hide({ id: 'fillReclama' }); Dialog.hide({ id: 'settingsDialog' }); }, }, - disabled: (reclamacio.tag === "ASSIGNAR USUARI"), + disabled: !CallInfo.annotationRequiresSection(), contained: true, raised: true, }) @@ -246,21 +251,18 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { } var emplenaReclamacio = function(tag) { - var reclamacio = { - "resolution": "unsolved", - "tag": tag - } + CallInfo.resetAnnotation(tag) Dialog.show(function() { return { className: 'dialog-reclama', title: 'Reclamació:', backdrop: true, body: [ - seleccionaUsuari(reclamacio, tag), + sectionSelector(tag), m("br"), - resolutionOptions(reclamacio), + resolutionOptions(CallInfo.annotation), ], - footerButtons: buttons(reclamacio), + footerButtons: buttons(), };},{id:'fillReclama'}); } @@ -344,7 +346,7 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { label: CallInfo.savingAnnotation?"Desant":"Desa", events: { onclick: function() { - var tag = getTag(CallInfo.call.reason); + var tag = CallInfo.annotationReasonTag() if (esReclamacio(tag)) { emplenaReclamacio(tag); } From e62df67ed1552e1b4c423000c2f89c23a2dfd3d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 11:00:57 +0100 Subject: [PATCH 093/120] getTag duplicated --- tomatic/static/components/questionnaire.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tomatic/static/components/questionnaire.js b/tomatic/static/components/questionnaire.js index abcbd8011..51ad71e8d 100644 --- a/tomatic/static/components/questionnaire.js +++ b/tomatic/static/components/questionnaire.js @@ -153,14 +153,6 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { CallInfo.call.reason = ""; } - var getTag = function(reason) { - var matches = reason.match(/\[(.*?)\]/); - if (matches) { - return matches[1].trim(); - } - return ""; - } - var esReclamacio = function(type) { const info = "CONSULTA"; return (type != info); From aa382c36a0270073aaccfebb811837fdbcf9552e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 11:26:23 +0100 Subject: [PATCH 094/120] section list uses directly CallInfo.annotation --- tomatic/static/components/callinfo.js | 4 ++-- tomatic/static/components/questionnaire.js | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index 35233dfb2..0ab3c85d8 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -45,8 +45,8 @@ CallInfo.resetAnnotation = function(tag) { } }; -CallInfo.annotationRequiresSection = function() { - return CallInfo.annotation === "ASSIGNAR USUARI"; +CallInfo.hasNoSection = function() { + return CallInfo.annotation.tag === "ASSIGNAR USUARI"; }; CallInfo.annotationReasonTag = function() { var reason = CallInfo.call.reason; diff --git a/tomatic/static/components/questionnaire.js b/tomatic/static/components/questionnaire.js index 51ad71e8d..d2261b348 100644 --- a/tomatic/static/components/questionnaire.js +++ b/tomatic/static/components/questionnaire.js @@ -159,7 +159,6 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { } var sectionSelector = function(reasonTag) { - var reclamacio = CallInfo.annotation; var options = CallInfo.selectableSections() var selectable = reasonTag === "ASSIGNAR USUARI"; return m("", [ @@ -171,7 +170,7 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { disabled: !selectable, default: reasonTag, oninput: function(ev) { - reclamacio.tag = ev.target.value; + CallInfo.annotation.tag = ev.target.value; }, }, m("option", { @@ -181,7 +180,7 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { selectable && options.map(function(option) { return m("option", { "value": option, - "selected": reclamacio.tag === option + "selected": CallInfo.annotation.tag === option }, option); }) ) From 34912aa63f09d33fcfc0da403fd60b39feffeac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 11:27:24 +0100 Subject: [PATCH 095/120] resolutionOptions uses directly CallInfo.annotation --- tomatic/static/components/questionnaire.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tomatic/static/components/questionnaire.js b/tomatic/static/components/questionnaire.js index d2261b348..9e08231e9 100644 --- a/tomatic/static/components/questionnaire.js +++ b/tomatic/static/components/questionnaire.js @@ -187,7 +187,7 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { ]); } - var resolutionOptions = function(reclamacio) { + var resolutionOptions = function() { return m(".case-resolution", [ m("p", "Resolució:"), m(RadioGroup, { @@ -234,7 +234,7 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { Dialog.hide({ id: 'settingsDialog' }); }, }, - disabled: !CallInfo.annotationRequiresSection(), + disabled: CallInfo.hasNoSection(), contained: true, raised: true, }) @@ -251,7 +251,7 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { body: [ sectionSelector(tag), m("br"), - resolutionOptions(CallInfo.annotation), + resolutionOptions(), ], footerButtons: buttons(), };},{id:'fillReclama'}); From a869a94045cece455a43c76c69e67c2b0d802d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 11:46:38 +0100 Subject: [PATCH 096/120] "ASSIGNAR USUARI" constant inside CallInfo --- tomatic/static/components/callinfo.js | 4 +++- tomatic/static/components/questionnaire.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index 0ab3c85d8..e318f2983 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -45,8 +45,10 @@ CallInfo.resetAnnotation = function(tag) { } }; +CallInfo.noSection = "ASSIGNAR USUARI" + CallInfo.hasNoSection = function() { - return CallInfo.annotation.tag === "ASSIGNAR USUARI"; + return CallInfo.annotation.tag === CallInfo.noSection; }; CallInfo.annotationReasonTag = function() { var reason = CallInfo.call.reason; diff --git a/tomatic/static/components/questionnaire.js b/tomatic/static/components/questionnaire.js index 9e08231e9..37b18cb83 100644 --- a/tomatic/static/components/questionnaire.js +++ b/tomatic/static/components/questionnaire.js @@ -160,7 +160,7 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { var sectionSelector = function(reasonTag) { var options = CallInfo.selectableSections() - var selectable = reasonTag === "ASSIGNAR USUARI"; + var selectable = reasonTag === CallInfo.noSection; return m("", [ m("p", "Equip: " ), m("select", From 32b11bd8a4b987927a8beaa7ed3e749faecf272d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 14:59:02 +0100 Subject: [PATCH 097/120] SaveCallLog moved to CallInfo --- tomatic/static/components/callinfo.js | 58 ++++++++++- tomatic/static/components/questionnaire.js | 107 +++++---------------- 2 files changed, 78 insertions(+), 87 deletions(-) diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index e318f2983..73529abf1 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -38,7 +38,8 @@ CallInfo.extras_dict = {}; CallInfo.savingAnnotation = false; CallInfo.annotation = {}; -CallInfo.resetAnnotation = function(tag) { +CallInfo.resetAnnotation = function() { + var tag = CallInfo.reasonTag() CallInfo.annotation = { resolution: 'unsolved', tag: tag, @@ -50,7 +51,7 @@ CallInfo.noSection = "ASSIGNAR USUARI" CallInfo.hasNoSection = function() { return CallInfo.annotation.tag === CallInfo.noSection; }; -CallInfo.annotationReasonTag = function() { +CallInfo.reasonTag = function() { var reason = CallInfo.call.reason; var matches = reason.match(/\[(.*?)\]/); if (matches) { @@ -59,6 +60,59 @@ CallInfo.annotationReasonTag = function() { return ""; }; +var postAnnotation = function(annotation) { + m.request({ + method: 'POST', + url: '/api/call/annotate', + extract: deyamlize, + body: annotation + }).then(function(response){ + console.debug("Info POST Response: ",response); + if (response.info.message !== "ok" ) { + console.debug("Error al desar motius telefon: ", response.info.message) + } + else { + console.debug("INFO case saved") + CallInfo.savingAnnotation = false; + CallInfo.call.extra = ""; + CallInfo.call.date = ""; + } + }, function(error) { + console.debug('Info POST apicall failed: ', error); + }); + CallInfo.call.reason = ""; +} + +CallInfo.annotationIsClaim = function() { + return CallInfo.reasonTag() === "CONSULTA"; +} + +CallInfo.saveCallLog = function(claim) { + CallInfo.savingAnnotation = true; + var partner = CallInfo.selectedPartner(); + var contract = CallInfo.selectedContract(); + var user = Login.myName(); + var partner_code = partner!==null ? partner.id_soci : ""; + var contract_number = contract!==null ? contract.number : ""; + var isodate = CallInfo.call.date || new Date().toISOString(); + var isClaim = CallInfo.annotationIsClaim(); + var claim = CallInfo.annotation; + postAnnotation({ + "user": user, + "date": isodate, + "phone": CallInfo.call.phone, + "partner": partner_code, + "contract": contract_number, + "reason": CallInfo.call.reason, + "notes": CallInfo.call.extra, + "claimsection": ( + !isClaim ? "" : ( + claim.tag ? claim.tag : ( + "INFO" + ))), + "resolution": isClaim ? claim.resolution:'', + }); +} CallInfo.clear = function() { CallInfo.call.phone = ""; diff --git a/tomatic/static/components/questionnaire.js b/tomatic/static/components/questionnaire.js index 37b18cb83..84b2d1198 100644 --- a/tomatic/static/components/questionnaire.js +++ b/tomatic/static/components/questionnaire.js @@ -26,52 +26,6 @@ var Questionnaire = {}; var reason_filter = ""; -var postAnnotation = function(annotation) { - m.request({ - method: 'POST', - url: '/api/call/annotate', - extract: deyamlize, - body: annotation - }).then(function(response){ - console.debug("Info POST Response: ",response); - if (response.info.message !== "ok" ) { - console.debug("Error al desar motius telefon: ", response.info.message) - } - else { - console.debug("INFO case saved") - CallInfo.savingAnnotation = false; - CallInfo.call.extra = ""; - reason_filter = ""; - CallInfo.call.date = ""; - } - }, function(error) { - console.debug('Info POST apicall failed: ', error); - }); -} - -var saveLogCalls = function(phone, user, claim, contract, partner) { - CallInfo.savingAnnotation = true; - var partner_code = partner!==null ? partner.id_soci : ""; - var contract_number = contract!==null ? contract.number : ""; - var isodate = CallInfo.call.date || new Date().toISOString() - postAnnotation({ - "user": user, - "date": isodate, - "phone": CallInfo.call.phone, - "partner": partner_code, - "contract": contract_number, - "reason": CallInfo.call.reason, - "notes": CallInfo.call.extra, - "claimsection": ( - !claim ? "" : ( - claim.tag ? claim.tag : ( - "INFO" - ))), - "resolution": claim ? claim.resolution:'', - }); -} - - var llistaMotius = function() { var reasons = CallInfo.filteredReasons(reason_filter) @@ -114,8 +68,6 @@ var clipboardIcon = function(){ } Questionnaire.annotationButton = function() { - var partner = CallInfo.selectedPartner(); - var contract = CallInfo.selectedContract(); return m("", m(IconButton, { icon: clipboardIcon(), @@ -124,8 +76,7 @@ Questionnaire.annotationButton = function() { title: "Anota la trucada fent servir aquest contracte", events: { onclick: function() { - console.log("VEURE QÜESTIONARI INFOS") - Questionnaire.openCaseAnnotationDialog(contract, partner); + Questionnaire.openCaseAnnotationDialog(); }, }, disabled: ( @@ -137,30 +88,17 @@ Questionnaire.annotationButton = function() { } -Questionnaire.openCaseAnnotationDialog = function(contract, partner) { +Questionnaire.openCaseAnnotationDialog = function() { + var partner = CallInfo.selectedPartner(); + var contract = CallInfo.selectedContract(); if (CallInfo.call.date === "") { CallInfo.call.date = new Date().toISOString(); } - var enviar = function(reclamacio) { - saveLogCalls( - CallInfo.call.phone, - Login.myName(), - reclamacio, - contract, - partner - ); - CallInfo.call.reason = ""; - } - - var esReclamacio = function(type) { - const info = "CONSULTA"; - return (type != info); - } - - var sectionSelector = function(reasonTag) { - var options = CallInfo.selectableSections() - var selectable = reasonTag === CallInfo.noSection; + var sectionSelector = function() { + var defaultSection = CallInfo.reasonTag(); // From the chosen category + var sections = CallInfo.selectableSections(); + var selectable = defaultSection === CallInfo.noSection; return m("", [ m("p", "Equip: " ), m("select", @@ -168,16 +106,16 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { id: "select-user", class: ".select-user", disabled: !selectable, - default: reasonTag, + default: defaultSection, oninput: function(ev) { CallInfo.annotation.tag = ev.target.value; }, }, m("option", { - value: reasonTag, - selected: !options.includes(reasonTag) - }, reasonTag), - selectable && options.map(function(option) { + value: defaultSection, + selected: !sections.includes(defaultSection) + }, defaultSection), + selectable && sections.map(function(option) { return m("option", { "value": option, "selected": CallInfo.annotation.tag === option @@ -187,7 +125,7 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { ]); } - var resolutionOptions = function() { + var resolutionChoser = function() { return m(".case-resolution", [ m("p", "Resolució:"), m(RadioGroup, { @@ -229,7 +167,7 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { label: "Desa", events: { onclick: function() { - enviar(Callinfo.annotation); + CallInfo.saveCallLog(); Dialog.hide({ id: 'fillReclama' }); Dialog.hide({ id: 'settingsDialog' }); }, @@ -241,17 +179,17 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { ]; } - var emplenaReclamacio = function(tag) { - CallInfo.resetAnnotation(tag) + var emplenaReclamacio = function() { + CallInfo.resetAnnotation(); Dialog.show(function() { return { className: 'dialog-reclama', title: 'Reclamació:', backdrop: true, body: [ - sectionSelector(tag), + sectionSelector(), m("br"), - resolutionOptions(), + resolutionChoser(), ], footerButtons: buttons(), };},{id:'fillReclama'}); @@ -337,12 +275,11 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { label: CallInfo.savingAnnotation?"Desant":"Desa", events: { onclick: function() { - var tag = CallInfo.annotationReasonTag() - if (esReclamacio(tag)) { - emplenaReclamacio(tag); + if (CallInfo.annotationIsClaim()) { + emplenaReclamacio(); } else { - enviar(""); + CallInfo.saveCallLog(); Dialog.hide({id:'settingsDialog'}); } }, From 9ca744f2db94165d6bc1a76aa63c3cf52bd2a148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 15:07:06 +0100 Subject: [PATCH 098/120] motius/reasons -> topics --- tomatic/static/components/callinfo.js | 2 +- tomatic/static/components/questionnaire.js | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index 73529abf1..7483c7df9 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -192,7 +192,7 @@ CallInfo.getExtras = function (extras) { }); }; -CallInfo.filteredReasons = function(filter) { +CallInfo.filteredTopics = function(filter) { var call_reasons = CallInfo.call_reasons; function contains(value) { var contains = value.toLowerCase().includes(filter.toLowerCase()); diff --git a/tomatic/static/components/questionnaire.js b/tomatic/static/components/questionnaire.js index 84b2d1198..fa5a3ca03 100644 --- a/tomatic/static/components/questionnaire.js +++ b/tomatic/static/components/questionnaire.js @@ -23,12 +23,11 @@ var Login = require('./login'); var Questionnaire = {}; -var reason_filter = ""; +var topicFilter = ""; -var llistaMotius = function() { - var reasons = CallInfo.filteredReasons(reason_filter) - +var topicList = function() { + var reasons = CallInfo.filteredTopics(topicFilter) var disabled = (CallInfo.savingAnnotation || CallInfo.call.date === "" ); return m(".motius", m(List, { @@ -234,14 +233,14 @@ Questionnaire.openCaseAnnotationDialog = function() { m(Textfield, { className: "textfield-filter", label: "Escriu per a filtrar", - value: reason_filter, + value: topicFilter, dense: true, onChange: function(params) { - reason_filter = params.value + topicFilter = params.value } })), ]), - llistaMotius(), + topicList(), m(".final-motius", [ m(Textfield, { className: "textfield-comentaris", From c2bb23af3e0e1b81afebed2a7e5ecb15e37e8fb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 15:51:50 +0100 Subject: [PATCH 099/120] updated infotypes --- callinfo/info_types.txt | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/callinfo/info_types.txt b/callinfo/info_types.txt index c84ae071e..0c96c4434 100644 --- a/callinfo/info_types.txt +++ b/callinfo/info_types.txt @@ -1,12 +1,27 @@ -[INFO] Explicació de la cooperativa (Primer contacte amb la cooperativa) -[INFO] Problemes Formulari (informàtics, CNAE, adjunts..) -[INFO] Demanen canvis que els hem de dirigir a l'OV (mail, telefon, compte bancari, tarifa, potència) -[INFO] Problemes amb l'accés a l'OV (contrassenya, usuari, activació, etc.) -[INFO] Generation kWh, Generació i Inversions -[INFO] Autoproducció -[INFO] Avaries -[INFO] Altres -[INFO] FACTURA -[INFO] COBRAMENTS -[INFO] RECLAMA -[INFO] CONTRACTES +[CONSULTA] [INFO] Dubtes informació general (com fer-me soci, com omplir) +[CONSULTA] [INFO] Sóc Soci i vull Convidar. No sóc Soci i vull contractar +[CONSULTA] [INFO] Bo social (com es demana, qui hi pot tenir accés, etc) +[CONSULTA] [INFO] No tinc llum, què faig? +[CONSULTA] [OV] Demanen canvis que els hem de dirigir a l'OV +[CONSULTA] [OV] Problemes amb l'accés a OV (contrassenya, usuari, activació +[CONSULTA] [OV] Consultes sobre les seccions (infoenergia i corbes, GkWh, i +[CONSULTA] [INFOENERGIA] Consulta sobre els seus informes +[CONSULTA] [FACTURA] Dubtes informació sobre les seves factures +[CONSULTA] [FACTURA]Dubtes informació sobre les lectures - vol donar lectur +[CONSULTA] [FACTURA] Info comptadors, lloguer, canvi a telegestió, trifàsic +[CONSULTA] [COBRAMENTS] Dubtes informació amb factures impagades i com fer +[CONSULTA] [COBRAMENTS] Informació sobre el tall de subministrament (com to +[CONSULTA] [CONTRACTES] Tot tipus de consultes referents a altes d'un nou p +[CONSULTA] [CONTRACTES] Tot tipus de consultes referents a baixes d’un punt +[CONSULTA] [CONTRACTES] Informació procés contractació, endarreriments, reb +[CONSULTA] [CONTRACTES] Informació sobre possible canvi de comer fraudulent +[CONSULTA] [CONTRACTES] Quina potència necessito? Info sobre nova tarifa +[CONSULTA] [CONTRACTES]Canvi de Titular - Canvi de Pagador. Com es fa, on, +[CONSULTA] [CONTRACTES] Tràmits sobre l’autoconsum (com es fa, documentació +[CONSULTA] [ENTITATS I EMPRESES] Informació com contractar administradors d +[CONSULTA] [ENTITATS I EMPRESES] Informació sobre tarifa 3.X TD i 6.X TD +[CONSULTA] [PROJECTES - GENERACIÓ] Informació sobre les nostres plantes +[CONSULTA] [AUTOPRODUCCIÓ] Informació sobre les compres col·lectives, com i +[CONSULTA] [APORTACIONS - GKWH] Informació sobre les seves aportacions al G +[CONSULTA] [COMUNICACIÓ] Comentar noticies blog, campanyes, newsletter, mai +[CONSULTA] [GL - PARTICIPA - AULA POPULAR] Info sobre assemblea, sobre el P \ No newline at end of file From 9848b1d7d4a45708b68bf74fda2e5a7b98cedb2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 15:52:29 +0100 Subject: [PATCH 100/120] annotationIsClaim: fix inverted logic --- tomatic/static/components/callinfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index 7483c7df9..d0405d91c 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -84,7 +84,7 @@ var postAnnotation = function(annotation) { } CallInfo.annotationIsClaim = function() { - return CallInfo.reasonTag() === "CONSULTA"; + return CallInfo.reasonTag() !== "CONSULTA"; } CallInfo.saveCallLog = function(claim) { From 1a71b53a5c1d6ae28848d8d8f36ef2ebaf6de536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 15:53:53 +0100 Subject: [PATCH 101/120] special button text when claim required --- tomatic/static/components/questionnaire.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tomatic/static/components/questionnaire.js b/tomatic/static/components/questionnaire.js index fa5a3ca03..1c8fd3538 100644 --- a/tomatic/static/components/questionnaire.js +++ b/tomatic/static/components/questionnaire.js @@ -271,7 +271,9 @@ Questionnaire.openCaseAnnotationDialog = function() { }), m(Button, { className: "save", - label: CallInfo.savingAnnotation?"Desant":"Desa", + label: CallInfo.savingAnnotation?"Desant":( + CallInfo.annotationIsClaim()?"Continua": + "Desa"), events: { onclick: function() { if (CallInfo.annotationIsClaim()) { From 8106ce634c0b51765e3357c33a181e1e81e0a6e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 15:56:28 +0100 Subject: [PATCH 102/120] reason -> topic --- tomatic/static/components/callinfo.js | 12 ++++++------ tomatic/static/components/questionnaire.js | 17 ++++++++--------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index d0405d91c..8ea2c2e8c 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -24,7 +24,7 @@ CallInfo.autoRefresh = true; // whether we are auto searching on incomming calls CallInfo.call = { 'phone': "", // phone of the currently selected call registry 'date': "", // isodate of the last unbinded search or the currently selected call registry - 'reason': "", // annotated reason for the call + 'topic': "", // annotated topic for the call 'extra': "", // annotated comments for the call 'log_call_reasons': [], }; @@ -52,8 +52,8 @@ CallInfo.hasNoSection = function() { return CallInfo.annotation.tag === CallInfo.noSection; }; CallInfo.reasonTag = function() { - var reason = CallInfo.call.reason; - var matches = reason.match(/\[(.*?)\]/); + var topic = CallInfo.call.topic; + var matches = topic.match(/\[(.*?)\]/); if (matches) { return matches[1].trim(); } @@ -80,7 +80,7 @@ var postAnnotation = function(annotation) { }, function(error) { console.debug('Info POST apicall failed: ', error); }); - CallInfo.call.reason = ""; + CallInfo.call.topic = ""; } CallInfo.annotationIsClaim = function() { @@ -103,7 +103,7 @@ CallInfo.saveCallLog = function(claim) { "phone": CallInfo.call.phone, "partner": partner_code, "contract": contract_number, - "reason": CallInfo.call.reason, + "reason": CallInfo.call.topic, "notes": CallInfo.call.extra, "claimsection": ( !isClaim ? "" : ( @@ -117,7 +117,7 @@ CallInfo.saveCallLog = function(claim) { CallInfo.clear = function() { CallInfo.call.phone = ""; CallInfo.call.log_call_reasons = []; - CallInfo.call.reason = ""; + CallInfo.call.topic = ""; CallInfo.call.extra = ""; CallInfo.currentPerson = 0; CallInfo.currentContract = 0; diff --git a/tomatic/static/components/questionnaire.js b/tomatic/static/components/questionnaire.js index 1c8fd3538..f922bbba6 100644 --- a/tomatic/static/components/questionnaire.js +++ b/tomatic/static/components/questionnaire.js @@ -27,17 +27,16 @@ var topicFilter = ""; var topicList = function() { - var reasons = CallInfo.filteredTopics(topicFilter) - var disabled = (CallInfo.savingAnnotation || CallInfo.call.date === "" ); + var topics = CallInfo.filteredTopics(topicFilter) return m(".motius", m(List, { compact: true, indentedBorder: true, compact: true, - tiles: reasons.map(function(reason) { + tiles: topics.map(function(topic) { return m(ListTile, { className: ( - CallInfo.call.reason === reason + CallInfo.call.topic === topic ? "llista-motius-selected" : "llista-motius-unselected" ), @@ -45,14 +44,14 @@ var topicList = function() { selectable: true, ink: true, hover: true, - title: reason, - selected: CallInfo.call.reason == reason, + title: topic, + selected: CallInfo.call.topic == topic, events: { onclick: function(ev) { - CallInfo.call.reason = reason + CallInfo.call.topic = topic } }, - disabled: disabled, + disabled: CallInfo.savingAnnotation, bordered: true, }); }), @@ -288,7 +287,7 @@ Questionnaire.openCaseAnnotationDialog = function() { border: 'true', disabled: ( CallInfo.savingAnnotation || - CallInfo.call.reason === "" || + CallInfo.call.topic === "" || CallInfo.call.extra === "" || CallInfo.call.date === "" || Login.myName() === "" From fcedcdad4734ce5351018450322f626ea886a2dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 16:05:39 +0100 Subject: [PATCH 103/120] Callinfo.call.extra -> notes --- tomatic/static/components/callinfo.js | 8 ++++---- tomatic/static/components/questionnaire.js | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index 8ea2c2e8c..67de1eb03 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -25,7 +25,7 @@ CallInfo.call = { 'phone': "", // phone of the currently selected call registry 'date': "", // isodate of the last unbinded search or the currently selected call registry 'topic': "", // annotated topic for the call - 'extra': "", // annotated comments for the call + 'notes': "", // annotated comments for the call 'log_call_reasons': [], }; CallInfo.call_reasons = { @@ -74,7 +74,7 @@ var postAnnotation = function(annotation) { else { console.debug("INFO case saved") CallInfo.savingAnnotation = false; - CallInfo.call.extra = ""; + CallInfo.call.notes = ""; CallInfo.call.date = ""; } }, function(error) { @@ -104,7 +104,7 @@ CallInfo.saveCallLog = function(claim) { "partner": partner_code, "contract": contract_number, "reason": CallInfo.call.topic, - "notes": CallInfo.call.extra, + "notes": CallInfo.call.notes, "claimsection": ( !isClaim ? "" : ( claim.tag ? claim.tag : ( @@ -118,7 +118,7 @@ CallInfo.clear = function() { CallInfo.call.phone = ""; CallInfo.call.log_call_reasons = []; CallInfo.call.topic = ""; - CallInfo.call.extra = ""; + CallInfo.call.notes = ""; CallInfo.currentPerson = 0; CallInfo.currentContract = 0; CallInfo.savingAnnotation = false; diff --git a/tomatic/static/components/questionnaire.js b/tomatic/static/components/questionnaire.js index f922bbba6..a31bf8482 100644 --- a/tomatic/static/components/questionnaire.js +++ b/tomatic/static/components/questionnaire.js @@ -249,9 +249,9 @@ Questionnaire.openCaseAnnotationDialog = function() { floatingLabel: true, rows: 5, dense: true, - value: CallInfo.call.extra, + value: CallInfo.call.notes, onChange: function(params) { - CallInfo.call.extra = params.value + CallInfo.call.notes = params.value }, disabled: CallInfo.savingAnnotation || CallInfo.call.date === "", }), @@ -288,7 +288,7 @@ Questionnaire.openCaseAnnotationDialog = function() { disabled: ( CallInfo.savingAnnotation || CallInfo.call.topic === "" || - CallInfo.call.extra === "" || + CallInfo.call.notes === "" || CallInfo.call.date === "" || Login.myName() === "" ), From d1cb767000c8a861e355821c5f991aebea6f6abf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 16:15:03 +0100 Subject: [PATCH 104/120] log_call_reasons unused, removed --- tomatic/static/components/callinfo.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index 67de1eb03..7570e0cd0 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -26,7 +26,6 @@ CallInfo.call = { 'date': "", // isodate of the last unbinded search or the currently selected call registry 'topic': "", // annotated topic for the call 'notes': "", // annotated comments for the call - 'log_call_reasons': [], }; CallInfo.call_reasons = { 'general': [], @@ -116,7 +115,6 @@ CallInfo.saveCallLog = function(claim) { CallInfo.clear = function() { CallInfo.call.phone = ""; - CallInfo.call.log_call_reasons = []; CallInfo.call.topic = ""; CallInfo.call.notes = ""; CallInfo.currentPerson = 0; From 8e3b262960610a27e7435d28ecd7c1ccd11e330d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 17:55:05 +0100 Subject: [PATCH 105/120] fix: Literal INFO -> CONSULTA --- tomatic/static/components/callinfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index 7570e0cd0..2a66190f6 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -107,7 +107,7 @@ CallInfo.saveCallLog = function(claim) { "claimsection": ( !isClaim ? "" : ( claim.tag ? claim.tag : ( - "INFO" + "CONSULTA" ))), "resolution": isClaim ? claim.resolution:'', }); From eb5c0ae4cc6b1f8a524fbe48e9530429048ae5c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 17:56:21 +0100 Subject: [PATCH 106/120] file_info -> searchResults, handling inside CallInfo --- tomatic/static/components/callinfo.js | 46 +++++++++++++++-------- tomatic/static/components/callinfopage.js | 21 ++++++----- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index 2a66190f6..ead73de19 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -14,7 +14,7 @@ var CallInfo = {}; var websock = null; CallInfo.search = ""; // Search value CallInfo.search_by = "phone"; // Search file -CallInfo.file_info = {}; // Retrieved search data +CallInfo.searchResults = {}; // Retrieved search data CallInfo.currentPerson = 0; // Selected person from search data CallInfo.currentContract = 0; // Selected contract selected person CallInfo.callLog = []; // User call registry @@ -120,7 +120,7 @@ CallInfo.clear = function() { CallInfo.currentPerson = 0; CallInfo.currentContract = 0; CallInfo.savingAnnotation = false; - CallInfo.file_info = {}; + CallInfo.searchResults = {}; } @@ -128,7 +128,7 @@ CallInfo.changeUser = function(newUser) { CallInfo.search = ""; CallInfo.clear(); CallInfo.call.date = ""; - CallInfo.file_info = {}; + CallInfo.searchResults = {}; CallInfo.callLog = []; CallInfo.call.iden = newUser; CallInfo.autoRefresh = true; @@ -147,7 +147,7 @@ CallInfo.callSelected = function(date, phone) { CallInfo.call.phone = phone; CallInfo.search = phone; CallInfo.search_by = "phone"; - CallInfo.file_info = { 1: "empty" }; + CallInfo.searchResults = { 1: "empty" }; retrieveInfo(); } @@ -210,12 +210,28 @@ CallInfo.filteredTopics = function(filter) { return filtered_regular.concat(extras); }; +function isEmpty(obj) { + return Object.keys(obj).length === 0; +} + +CallInfo.searchStatus = function() { + if (isEmpty(CallInfo.searchResults)) { + return "ZERORESULTS"; + } + if (CallInfo.searchResults[1] === "empty") { + return "SEARCHING"; + } + if (CallInfo.searchResults[1] === "toomuch") + return "TOOMANYRESULTS"; + + return "SUCCESS" +} CallInfo.selectedPartner = function() { - if (!CallInfo.file_info) { return null; } - if (!CallInfo.file_info.partners) { return null; } - if (CallInfo.file_info.partners.length===0) { return null; } - var partner = CallInfo.file_info.partners[CallInfo.currentPerson]; + if (!CallInfo.searchResults) { return null; } + if (!CallInfo.searchResults.partners) { return null; } + if (CallInfo.searchResults.partners.length===0) { return null; } + var partner = CallInfo.searchResults.partners[CallInfo.currentPerson]; if (partner === undefined) { return null; } return partner; }; @@ -243,22 +259,22 @@ var retrieveInfo = function () { }).then(function(response){ console.debug("Info GET Response: ", response); if(response.info.message === "response_too_long") { - CallInfo.file_info = { 1: "toomuch" }; + CallInfo.searchResults = { 1: "toomuch" }; return; } if (response.info.message !== "ok" ) { console.debug("Error al obtenir les dades: ", response.info.message) - CallInfo.file_info = {} + CallInfo.searchResults = {} return; } - CallInfo.file_info=response.info.info; + CallInfo.searchResults=response.info.info; if (CallInfo.call.date === "") { // TODO: If selection is none CallInfo.call.date = new Date().toISOString(); } // Keep the context, just in case a second query is started - // and CallInfo.file_info is overwritten - var context = CallInfo.file_info; + // and CallInfo.searchResults is overwritten + var context = CallInfo.searchResults; m.request({ method: 'POST', url: '/api/info/contractdetails', @@ -416,13 +432,13 @@ CallInfo.searchCustomer = function() { CallInfo.clear(); if (CallInfo.search !== 0 && CallInfo.search !== ""){ CallInfo.call.phone = ""; - CallInfo.file_info = { 1: "empty" }; + CallInfo.searchResults = { 1: "empty" }; CallInfo.currentPerson = 0; retrieveInfo(); } else { CallInfo.call.date = ""; - CallInfo.file_info = {} + CallInfo.searchResults = {} } } diff --git a/tomatic/static/components/callinfopage.js b/tomatic/static/components/callinfopage.js index d88faeea8..6c250161f 100644 --- a/tomatic/static/components/callinfopage.js +++ b/tomatic/static/components/callinfopage.js @@ -290,18 +290,21 @@ CallInfoPage.view = function() { m(".layout.vertical.flex", [ customerSearch(), m('.plane-info', - isEmpty(CallInfo.file_info)? - m(".searching", 'No s\'ha trobat cap resultat.'):( - CallInfo.file_info[1] === "empty"? - m(".searching", m(Spinner, { show: "true" } )):( - CallInfo.file_info[1] === "toomuch"? - m(".searching", 'Cerca poc específica, retorna masses resultats.'):( + CallInfo.searchStatus()==='ZERORESULTS'? + m(".searching", 'No s\'ha trobat cap resultat.') + :( + CallInfo.searchStatus()==='SEARCHING'? + m(".searching", m(Spinner, { show: "true" } )) + :( + CallInfo.searchStatus()==='TOOMANYRESULTS'? + m(".searching", 'Cerca poc específica, retorna masses resultats.') + :( m('.plane-info', [ m(".layout.vertical.flex", [ - PartnerInfo.allInfo(CallInfo.file_info), - ContractInfo.mainPanel(CallInfo.file_info), + PartnerInfo.allInfo(CallInfo.searchResults), + ContractInfo.mainPanel(CallInfo.searchResults), ]), - ContractInfo.detailsPanel(CallInfo.file_info), + ContractInfo.detailsPanel(CallInfo.searchResults), ]) ))) ), From c3469e108172aaae93f8375ed018d09ce05f40f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 18:07:32 +0100 Subject: [PATCH 107/120] unused structures removed --- tomatic/static/components/callinfo.js | 1 - tomatic/static/components/login.js | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index ead73de19..1fbfabf26 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -130,7 +130,6 @@ CallInfo.changeUser = function(newUser) { CallInfo.call.date = ""; CallInfo.searchResults = {}; CallInfo.callLog = []; - CallInfo.call.iden = newUser; CallInfo.autoRefresh = true; } diff --git a/tomatic/static/components/login.js b/tomatic/static/components/login.js index 7b2d250a1..7092eba7d 100644 --- a/tomatic/static/components/login.js +++ b/tomatic/static/components/login.js @@ -47,12 +47,8 @@ Login.onUserChanged = []; Login.logout = function(){ document.cookie = tomaticCookie + "=; expires = Thu, 01 Jan 1970 00:00:00 GMT;SameSite=Strict;path=/" - var info = { - iden: "", - ext: -1 - } Login.onLogout.map(function(callback) { - callback(info); + callback(); }) } From 56d1266c595be563c3083dc0e36ecd972848b025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 18:10:32 +0100 Subject: [PATCH 108/120] Login.currentExtension simpler definition --- tomatic/static/components/login.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tomatic/static/components/login.js b/tomatic/static/components/login.js index 7092eba7d..2a818101b 100644 --- a/tomatic/static/components/login.js +++ b/tomatic/static/components/login.js @@ -65,13 +65,12 @@ var getMyExt = function() { } Login.getMyExt = getMyExt; -var currentExtension = function() { +Login.currentExtension = function() { var cookie_value = getCookie(tomaticCookie); if (cookie_value === ":") return -1; var extension = cookie_value.split(":")[1].toString(); return extension === "" ? -1 : extension; } -Login.currentExtension = currentExtension; var setCookieInfo = function(vnode){ From 67522296ee3911a4f27a4e98e320002ec2a4f889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 18:11:54 +0100 Subject: [PATCH 109/120] removed unused Login.getMyExt --- tomatic/static/components/login.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tomatic/static/components/login.js b/tomatic/static/components/login.js index 2a818101b..cf60db651 100644 --- a/tomatic/static/components/login.js +++ b/tomatic/static/components/login.js @@ -58,13 +58,6 @@ Date.prototype.addHours = function(h) { } -var getMyExt = function() { - var cookie_value = getCookie(tomaticCookie); - if (cookie_value === ":") return ""; - return cookie_value.split(":")[1].toString(); -} -Login.getMyExt = getMyExt; - Login.currentExtension = function() { var cookie_value = getCookie(tomaticCookie); if (cookie_value === ":") return -1; From 74b2c3ac8b47815f2601e59c08f7a4470d874365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 18:23:32 +0100 Subject: [PATCH 110/120] fix: triming and urlencoding searched text --- tomatic/static/components/callinfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index 1fbfabf26..77c01f1e4 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -253,7 +253,7 @@ CallInfo.selectContract = function(idx) { var retrieveInfo = function () { m.request({ method: 'GET', - url: '/api/info/'+CallInfo.search_by+"/"+CallInfo.search, + url: '/api/info/'+CallInfo.search_by+"/"+encodeURIComponent(CallInfo.search.trim()), extract: deyamlize, }).then(function(response){ console.debug("Info GET Response: ", response); From 9997d517cd10f2cfa9f69c6fe137486dc6bd490d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 23:34:01 +0100 Subject: [PATCH 111/120] moved MyName() --- tomatic/static/components/login.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tomatic/static/components/login.js b/tomatic/static/components/login.js index cf60db651..bb0d5573a 100644 --- a/tomatic/static/components/login.js +++ b/tomatic/static/components/login.js @@ -36,16 +36,11 @@ Login.watchLoginChanges = function() { Login.watchLoginChanges, 500); } -Login.myName = function() { - var cookie = whoAreYou(); - var user = cookie.split(":")[0]; - return user; -} Login.onLogout = []; Login.onLogin = []; Login.onUserChanged = []; -Login.logout = function(){ +Login.logout = function() { document.cookie = tomaticCookie + "=; expires = Thu, 01 Jan 1970 00:00:00 GMT;SameSite=Strict;path=/" Login.onLogout.map(function(callback) { callback(); @@ -57,6 +52,11 @@ Date.prototype.addHours = function(h) { return this; } +Login.myName = function() { + var cookie = whoAreYou(); + var user = cookie.split(":")[0]; + return user; +} Login.currentExtension = function() { var cookie_value = getCookie(tomaticCookie); From 600314daaa1bdd41962e01847270934cdf676df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Tue, 25 Jan 2022 23:34:42 +0100 Subject: [PATCH 112/120] renamings --- tomatic/api.py | 4 ++-- tomatic/static/components/callinfo.js | 21 +++++++++++---------- tomatic/static/components/callinfopage.js | 4 ++-- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/tomatic/api.py b/tomatic/api.py index 414d61ebb..dcd35aed4 100644 --- a/tomatic/api.py +++ b/tomatic/api.py @@ -366,12 +366,12 @@ def yamlinfoerror(code, message, *args, **kwds): @app.get('/api/info/{field}/{value}') def getInfoPersonBy(field, value): - decoded_field = urllib.parse.unquote(value) + decoded_value = urllib.parse.unquote(value) data = None with erp() as O: callinfo = CallInfo(O) try: - data = callinfo.getByField(field, decoded_field, shallow=True) + data = callinfo.getByField(field, decoded_value, shallow=True) except ValueError: return yamlinfoerror('error_getBy'+field.title(), "Getting information searching {}='{}'.", field, value) diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index 77c01f1e4..a8aa97427 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -190,23 +190,24 @@ CallInfo.getExtras = function (extras) { }; CallInfo.filteredTopics = function(filter) { - var call_reasons = CallInfo.call_reasons; - function contains(value) { - var contains = value.toLowerCase().includes(filter.toLowerCase()); - return contains; + function matches_search(value) { + return value.toLowerCase().includes(filter.toLowerCase()); } - var list_reasons = [].concat( + var call_reasons = CallInfo.call_reasons; + var topics = [].concat( call_reasons.infos, call_reasons.general, ); if (filter === "") { - return list_reasons + return topics } - var filtered_regular = list_reasons.filter(contains); - var filtered_extras = call_reasons.extras.filter(contains); - var extras = CallInfo.getExtras(filtered_extras); - return filtered_regular.concat(extras); + var topics_by_name = topics.filter(matches_search); + var filtered_keywords = call_reasons.extras.filter(matches_search); + var topics_by_keyword = filtered_keywords.map(function(keyword) { + return CallInfo.extras_dict[keyword]; + }) + return topics_by_name.concat(topics_by_keyword); }; function isEmpty(obj) { diff --git a/tomatic/static/components/callinfopage.js b/tomatic/static/components/callinfopage.js index 6c250161f..943a6b85d 100644 --- a/tomatic/static/components/callinfopage.js +++ b/tomatic/static/components/callinfopage.js @@ -178,7 +178,7 @@ var responsesMessage = function(info) { ); } -var atencionsLog = function() { +var attendedCallList = function() { var aux = []; if (CallInfo.callLog.length === 0) { return m(".attended-calls-list", m(List, { @@ -276,7 +276,7 @@ var attendedCalls = function() { }},{ text: { content: (CallInfo.callLog[0] === "lookingfor" ? - m('center', m(Spinner, { show: "true" } )) : atencionsLog()) + m('center', m(Spinner, { show: "true" } )) : attendedCallList()) }}, ] }); From f1bf99e59d5a22fe459e07f161ec031dad3bd018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Wed, 26 Jan 2022 07:52:56 +0100 Subject: [PATCH 113/120] extra_dict -> keyword2topic --- tomatic/static/components/callinfo.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index a8aa97427..da6cfbaed 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -32,7 +32,7 @@ CallInfo.call_reasons = { 'infos': [], 'extras': [] } -CallInfo.extras_dict = {}; +CallInfo.keyword2topic = {}; CallInfo.savingAnnotation = false; CallInfo.annotation = {}; @@ -46,7 +46,6 @@ CallInfo.resetAnnotation = function() { }; CallInfo.noSection = "ASSIGNAR USUARI" - CallInfo.hasNoSection = function() { return CallInfo.annotation.tag === CallInfo.noSection; }; @@ -185,7 +184,7 @@ function contractNumbers(info) { CallInfo.getExtras = function (extras) { return extras.map(function(extra) { - return CallInfo.extras_dict[extra]; + return CallInfo.keyword2topic[extra]; }); }; @@ -205,7 +204,7 @@ CallInfo.filteredTopics = function(filter) { var topics_by_name = topics.filter(matches_search); var filtered_keywords = call_reasons.extras.filter(matches_search); var topics_by_keyword = filtered_keywords.map(function(keyword) { - return CallInfo.extras_dict[keyword]; + return CallInfo.keyword2topic[keyword]; }) return topics_by_name.concat(topics_by_keyword); }; @@ -311,7 +310,7 @@ CallInfo.getClaims = function() { } else { CallInfo.call_reasons.general = response.info.claims; - CallInfo.extras_dict = response.info.dict; + CallInfo.keyword2topic = response.info.dict; CallInfo.call_reasons.extras = Object.keys(response.info.dict); } }, function(error) { From a3c6cb2a7e2a74217732a55327728eb71eb50a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Thu, 27 Jan 2022 13:33:30 +0100 Subject: [PATCH 114/120] code style: if with no claudators --- tomatic/static/components/callinfo.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index da6cfbaed..b515ccfd9 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -220,9 +220,9 @@ CallInfo.searchStatus = function() { if (CallInfo.searchResults[1] === "empty") { return "SEARCHING"; } - if (CallInfo.searchResults[1] === "toomuch") + if (CallInfo.searchResults[1] === "toomuch") { return "TOOMANYRESULTS"; - + } return "SUCCESS" } From 66f625259aa032ae7698cfe83adc54e07ae1021c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Thu, 27 Jan 2022 16:32:53 +0100 Subject: [PATCH 115/120] Topics and sections taken from new entry point --- tomatic/api.py | 8 ++- tomatic/static/components/callinfo.js | 82 ++++++++++++++++----------- 2 files changed, 56 insertions(+), 34 deletions(-) diff --git a/tomatic/api.py b/tomatic/api.py index dcd35aed4..5db98e549 100644 --- a/tomatic/api.py +++ b/tomatic/api.py @@ -455,7 +455,13 @@ async def callAnnotate(request: Request): message="ok" )) - +@app.get('/api/call/annotate/topics') +def annotationCategories(): + # TODO: Caching through CallRegistry + with erp() as O: + from .claims import Claims + c = Claims(O) + return yamlfy(**c.categories()) @app.get('/api/updateClaims') diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index b515ccfd9..61b475481 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -12,6 +12,8 @@ var styleCallinfo = require('./callinfo_style.styl'); var CallInfo = {}; var websock = null; +CallInfo.topics = []; +CallInfo.sections = []; CallInfo.search = ""; // Search value CallInfo.search_by = "phone"; // Search file CallInfo.searchResults = {}; // Retrieved search data @@ -45,7 +47,8 @@ CallInfo.resetAnnotation = function() { } }; -CallInfo.noSection = "ASSIGNAR USUARI" +CallInfo.noSection = "ASSIGNAR USUARI"; +CallInfo.helpdeskSection = "CONSULTA"; CallInfo.hasNoSection = function() { return CallInfo.annotation.tag === CallInfo.noSection; }; @@ -82,7 +85,7 @@ var postAnnotation = function(annotation) { } CallInfo.annotationIsClaim = function() { - return CallInfo.reasonTag() !== "CONSULTA"; + return CallInfo.reasonTag() !== CallInfo.helpdeskSection; } CallInfo.saveCallLog = function(claim) { @@ -106,7 +109,7 @@ CallInfo.saveCallLog = function(claim) { "claimsection": ( !isClaim ? "" : ( claim.tag ? claim.tag : ( - "CONSULTA" + CallInfo.helpdeskSection ))), "resolution": isClaim ? claim.resolution:'', }); @@ -150,17 +153,10 @@ CallInfo.callSelected = function(date, phone) { } CallInfo.selectableSections = function() { - return [ - "RECLAMA", - "FACTURA", - "COBRAMENTS", - "ATR A - COMER", - "ATR B - COMER", - "ATR C - COMER", - "ATR M - COMER" - ]; -} - + return CallInfo.sections.map(function(section) { + return section.name; + }); +}; function formatContractNumber(number) { // some contract numbers get converted to int and lose their padding @@ -189,24 +185,20 @@ CallInfo.getExtras = function (extras) { }; CallInfo.filteredTopics = function(filter) { - function matches_search(value) { - return value.toLowerCase().includes(filter.toLowerCase()); - } - var call_reasons = CallInfo.call_reasons; - var topics = [].concat( - call_reasons.infos, - call_reasons.general, - ); - - if (filter === "") { - return topics - } - var topics_by_name = topics.filter(matches_search); - var filtered_keywords = call_reasons.extras.filter(matches_search); - var topics_by_keyword = filtered_keywords.map(function(keyword) { - return CallInfo.keyword2topic[keyword]; - }) - return topics_by_name.concat(topics_by_keyword); + var lowerFilter = filter.toLowerCase() + return CallInfo.topics + .filter(function(topic) { + if (topic.description.toLowerCase().includes(lowerFilter)) { + return true; + } + if (topic.keywords.toLowerCase().includes(lowerFilter)) { + return true; + } + return false; + }) + .map(function(topic) { + return topic.description; + }); }; function isEmpty(obj) { @@ -297,6 +289,30 @@ var retrieveInfo = function () { }); }; +CallInfo.getTopics = function() { + m.request({ + method: 'GET', + url: '/api/call/annotate/topics', + extract: deyamlize, + }).then(function(response){ + console.debug("Topics GET Response: ",response); + + CallInfo.topics = response.categories; + CallInfo.topics.map(function(topic) { + var section = topic.section; + if (section === null) { + section = CallInfo.noSection; + } + if (section === "HelpDesk") { + section = CallInfo.helpdeskSection; + } + topic.description = "["+section+"] "+topic.code+". "+topic.name; + }) + CallInfo.sections = response.sections; + }, function(error) { + console.debug('Info GET apicall failed: ', error); + }); +} CallInfo.getClaims = function() { m.request({ @@ -473,7 +489,7 @@ CallInfo.onMessageReceived = function(event) { } console.debug("Message received from WebSockets and type not recognized."); } - +CallInfo.getTopics(); CallInfo.getClaims(); CallInfo.getInfos(); CallInfo.getLogPerson() From c1bb7d891866524fdbdd1a8e489b7e78bdb4e527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Thu, 27 Jan 2022 16:34:26 +0100 Subject: [PATCH 116/120] bye extras and keyword2topic, unused --- tomatic/static/components/callinfo.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index 61b475481..775f931a5 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -34,7 +34,6 @@ CallInfo.call_reasons = { 'infos': [], 'extras': [] } -CallInfo.keyword2topic = {}; CallInfo.savingAnnotation = false; CallInfo.annotation = {}; @@ -178,12 +177,6 @@ function contractNumbers(info) { } -CallInfo.getExtras = function (extras) { - return extras.map(function(extra) { - return CallInfo.keyword2topic[extra]; - }); -}; - CallInfo.filteredTopics = function(filter) { var lowerFilter = filter.toLowerCase() return CallInfo.topics @@ -326,7 +319,6 @@ CallInfo.getClaims = function() { } else { CallInfo.call_reasons.general = response.info.claims; - CallInfo.keyword2topic = response.info.dict; CallInfo.call_reasons.extras = Object.keys(response.info.dict); } }, function(error) { From 4b526c2defceba27683488c2cdb28c577103c678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Thu, 27 Jan 2022 16:41:50 +0100 Subject: [PATCH 117/120] cleaning call_reasons, getClaims and getInfos --- tomatic/static/components/callinfo.js | 56 +++------------------------ 1 file changed, 6 insertions(+), 50 deletions(-) diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index 775f931a5..6fa789a43 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -12,8 +12,8 @@ var styleCallinfo = require('./callinfo_style.styl'); var CallInfo = {}; var websock = null; -CallInfo.topics = []; -CallInfo.sections = []; +CallInfo.topics = []; // Call topics +CallInfo.sections = []; // Teams to assign a call CallInfo.search = ""; // Search value CallInfo.search_by = "phone"; // Search file CallInfo.searchResults = {}; // Retrieved search data @@ -29,11 +29,6 @@ CallInfo.call = { 'topic': "", // annotated topic for the call 'notes': "", // annotated comments for the call }; -CallInfo.call_reasons = { - 'general': [], - 'infos': [], - 'extras': [] -} CallInfo.savingAnnotation = false; CallInfo.annotation = {}; @@ -307,45 +302,7 @@ CallInfo.getTopics = function() { }); } -CallInfo.getClaims = function() { - m.request({ - method: 'GET', - url: '/api/getClaims', - extract: deyamlize, - }).then(function(response){ - console.debug("Info GET Response: ",response); - if (response.info.message !== "ok" ) { - console.debug("Error al obtenir les reclamacions: ", response.info.message) - } - else { - CallInfo.call_reasons.general = response.info.claims; - CallInfo.call_reasons.extras = Object.keys(response.info.dict); - } - }, function(error) { - console.debug('Info GET apicall failed: ', error); - }); -}; - - -CallInfo.getInfos = function() { - m.request({ - method: 'GET', - url: '/api/getInfos', - extract: deyamlize, - }).then(function(response){ - console.debug("Info GET Response: ",response); - if (response.info.message !== "ok" ) { - console.debug("Error al obtenir els infos: ", response.info.message) - } - else { - CallInfo.call_reasons.infos = response.info.infos; - } - }, function(error) { - console.debug('Info GET apicall failed: ', error); - }); -}; - - +// TODO: updateTopics instead CallInfo.updateClaims = function() { CallInfo.updatingClaims = true; m.request({ @@ -359,13 +316,14 @@ CallInfo.updateClaims = function() { } else{ CallInfo.updatingClaims = false; - CallInfo.getClaims(); + CallInfo.getTopics(); } }, function(error) { console.debug('Info GET apicall failed: ', error); }); }; +// TODO: updateTopics instead CallInfo.updateCrmCategories = function() { CallInfo.updatingCrmCategories = true; m.request({ @@ -379,7 +337,7 @@ CallInfo.updateCrmCategories = function() { } else{ CallInfo.updatingCrmCategories = false; - CallInfo.getInfos(); + CallInfo.getTopics(); } }, function(error) { console.debug('Info GET apicall failed: ', error); @@ -482,8 +440,6 @@ CallInfo.onMessageReceived = function(event) { console.debug("Message received from WebSockets and type not recognized."); } CallInfo.getTopics(); -CallInfo.getClaims(); -CallInfo.getInfos(); CallInfo.getLogPerson() Login.onLogin.push(CallInfo.sendIdentification); From 122393136516d4f3781d23515c0dcc96241abc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Thu, 27 Jan 2022 18:30:38 +0100 Subject: [PATCH 118/120] cli seteable banners for ketchup and pebrotic --- scripts/tomatic_api.py | 13 +++- tomatic/api.py | 3 +- tomatic/static/components/tomatic.js | 2 + tomatic/static/graella.js | 6 +- tomatic/static/ketchup.png | Bin 0 -> 26039 bytes tomatic/static/ketchup.svg | 100 +++++++++++++++++++++++++++ tomatic/static/pebrotic.jpg | Bin 0 -> 16949 bytes tomatic/static/style.styl | 13 ++++ 8 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 tomatic/static/ketchup.png create mode 100644 tomatic/static/ketchup.svg create mode 100644 tomatic/static/pebrotic.jpg diff --git a/scripts/tomatic_api.py b/scripts/tomatic_api.py index bdbdb27aa..3edfcd194 100755 --- a/scripts/tomatic_api.py +++ b/scripts/tomatic_api.py @@ -7,6 +7,7 @@ from tomatic.api import app, pbx, schedules from tomatic import __version__ from tomatic.pbx import pbxqueue, pbxtypes +import os def now(date, time): from yamlns.dateutils import Date @@ -58,10 +59,15 @@ def now(date, time): default=None, help="Hora del dia a simular en comptes d'ara" ) +@click.option('--variant', + default=None, + type=click.Choice(['tomatic', 'pebrotic', 'ketchup']), + help="Identify the instance as this variant", + ) -def main(fake, debug, host, port, printrules, date, time, backend, queue): +def main(fake, debug, host, port, printrules, date, time, backend, queue, variant): "Runs the Tomatic web and API" - print(fake, debug, host, port, printrules, date, time, backend, queue) + print(fake, debug, host, port, printrules, date, time, backend, queue, variant) if printrules: for rule in app.routes: @@ -76,6 +82,9 @@ def main(fake, debug, host, port, printrules, date, time, backend, queue): initialQueue = schedules.queueScheduledFor(now(date,time)) p.setQueue(initialQueue) + if variant: + os.environ["TOMATIC_VARIANT"] = variant + step("Starting API") if printrules: for rule in app.routes: diff --git a/tomatic/api.py b/tomatic/api.py index 5db98e549..1bbdfb440 100644 --- a/tomatic/api.py +++ b/tomatic/api.py @@ -16,6 +16,7 @@ from . import __version__ as version import asyncio import re +import os from datetime import datetime, timedelta, timezone import urllib.parse import decorator @@ -143,7 +144,6 @@ def sender(message): except WebSocketDisconnect: backchannel.onDisconnect(session_id) - @app.get('/') @app.get('/{file}') def tomatic(file=None): @@ -154,6 +154,7 @@ def tomatic(file=None): def apiVersion(): return yamlfy( version = version, + variant = os.environ.get('TOMATIC_VARIANT', 'tomatic') ) @app.get('/api/graella/list') diff --git a/tomatic/static/components/tomatic.js b/tomatic/static/components/tomatic.js index 11122c9b8..37c4ca191 100644 --- a/tomatic/static/components/tomatic.js +++ b/tomatic/static/components/tomatic.js @@ -16,6 +16,7 @@ var Tomatic = { packageinfo: Package, }; +Tomatic.variant = 'tomatic'; Tomatic.queue = m.prop([]); Tomatic.persons = m.prop({}); Tomatic.init = function() { @@ -39,6 +40,7 @@ Tomatic.checkVersion = function() { url: '/api/version', extract: deyamlize, }).then(function(response){ + Tomatic.variant = response.variant; if (response.version == Tomatic.packageinfo.version) return; console.log( "New server version", response.version, "detected.", diff --git a/tomatic/static/graella.js b/tomatic/static/graella.js index 05beee716..26995d212 100644 --- a/tomatic/static/graella.js +++ b/tomatic/static/graella.js @@ -813,7 +813,11 @@ TomaticApp.view = function() { console.log("Page: ", m.route.get()); var currentTabIndex = indexForRoute(m.route.get()); var current = m(pages[tabs[currentTabIndex].label]); - return m(kumato?'.pe-dark-tone':'',[ + return m('' + +(kumato?'.pe-dark-tone':'') + +('.variant-'+Tomatic.variant) + + ,[ PersonStyles(), m(Page, {content:current}), ]); diff --git a/tomatic/static/ketchup.png b/tomatic/static/ketchup.png new file mode 100644 index 0000000000000000000000000000000000000000..8d546261d119a019059fff70b0eac1293e2ca4a3 GIT binary patch literal 26039 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 H10L~zYUELZ literal 0 HcmV?d00001 diff --git a/tomatic/static/ketchup.svg b/tomatic/static/ketchup.svg new file mode 100644 index 000000000..ad82ad877 --- /dev/null +++ b/tomatic/static/ketchup.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + diff --git a/tomatic/static/pebrotic.jpg b/tomatic/static/pebrotic.jpg new file mode 100644 index 0000000000000000000000000000000000000000..878e5a294b387b577de7e037d7de00a1a81c29a4 GIT binary patch literal 16949 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 Date: Thu, 27 Jan 2022 18:52:25 +0100 Subject: [PATCH 119/120] changelog --- CHANGES.md | 14 ++++++++++++++ TODO.md | 27 +++++++++++++++------------ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 282ef7255..77e2f7fa7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,19 @@ # Changelog +## unreleased + +- Call annotation workflow redesign + - Claims and infos unified, single log, api entry, topic list... + - Structured info on topics/categories are retrieved + from api avoiding fragile parsing of the description + - Save Annotation without call, person or contract + - `tomatic_uploadcases.py` upload both info and claims + - All categories have a code and optionally a section + - Translate HelpDesk section as CONSULTA + - Translate + - ERP user is set on cases +- Custom banners for pebrotic and ketchup variants by CLI + ## 4.3.2 2022-01-21 - Persistent kumato mode using local browser storage diff --git a/TODO.md b/TODO.md index 4abfe3985..4b8df38c4 100644 --- a/TODO.md +++ b/TODO.md @@ -53,28 +53,31 @@ ## Trello https://trello.com/c/ljKRzvz5/4221-0-3-p7-centraleta-kalinfo-desar-els-casos-de-consultes-del-kalinfo-al-erp -- [ ] Dubte AiS: cal pujar les anotacios que heu fet de proves +- [ ] Cache topics. Avoid retrieving from erp every time +- [ ] Clean up del retrieveClaims/Infos: menu, dialog, frontend call, api... +- [ ] !!! create crm: solved = True (lo comentamos cuando lo movimos a una funcion a parte) - [ ] Dubte AiS: renombrar user -> seccion/team (preguntar a AiS per terminologia de domini) -- [ ] Dubte AiS: tenim les llistes a produccio que fem amb elles (mostrarles perque hi ha brossa i textos que poden canviar) - -- [ ] entry point to obtain categories -- [ ] encapsulate access to the categories info in frontend -- [x] callinfo log: unite resolution fields -- [x] callinfo log: join infos and claims +- [ ] Dubte AiS/ERP: how to value solved, depending of resolution - [ ] callinfo log: join infos/claims with log? (consider performance and usage) -- [x] Importar categories que falten de atc com a categorias de crmcases -- [x] anotate_case: sensitive to the case fields creates atc or not -- [ ] !!! create crm: solved = True (lo comentamos cuando lo movimos a una funcion a parte) - [ ] create crm: extract seccio del reason and remove the field - [ ] create crm: cas contracte no existeix - [ ] callreg: Rename Claims to reflect its repurposing -- [x] callreg: create crm: Inserir usuari correcte al CRM (es fa servir l'usuari loggejat a l'erp: Scriptlauncher i no veiem com canviar-ho) - [ ] callreg: On failing annotation, ui notifies the user +- [ ] Urlencoding the search does not work (search something with slash or commas +- [ ] Manual annotations with some search renders "Registre..." in the call list ## Dones - +- [x] encapsulate access to the categories info in frontend +- [x] entry point to obtain categories +- [x] callreg: create crm: Inserir usuari correcte al CRM (es fa servir l'usuari loggejat a l'erp: Scriptlauncher i no veiem com canviar-ho) +- [x] Dubte AiS: cal pujar les anotacios que heu fet de proves -> No, quan les posem netejem +- [x] Dubte AiS: tenim les llistes a produccio que fem amb elles (mostrarles perque hi ha brossa i textos que poden canviar) -> Ara les bones estan a testing, Script de migracio +- [x] callinfo log: unite resolution fields +- [x] callinfo log: join infos and claims +- [x] Importar categories que falten de atc com a categorias de crmcases +- [x] anotate_case: sensitive to the case fields creates atc or not - [x] Use contrast text color for person boxes - [x] Editable erpuser in PersonEditor - [x] move scripts to a folder From b96461a6616b34859324ca1b1bc0aa421104752a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa=20Garz=C3=B3n?= Date: Thu, 27 Jan 2022 18:54:59 +0100 Subject: [PATCH 120/120] search values trimmed and urlencoded --- CHANGES.md | 1 + tomatic/static/components/callinfo.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 77e2f7fa7..dda7aec40 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,7 @@ - Translate - ERP user is set on cases - Custom banners for pebrotic and ketchup variants by CLI +- Fix: search values trimmed and urlencoded ## 4.3.2 2022-01-21 diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index 6fa789a43..7b32a50e8 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -231,9 +231,10 @@ CallInfo.selectContract = function(idx) { var retrieveInfo = function () { + var encodedValue = encodeURIComponent(CallInfo.search.trim()); m.request({ method: 'GET', - url: '/api/info/'+CallInfo.search_by+"/"+encodeURIComponent(CallInfo.search.trim()), + url: '/api/info/'+CallInfo.search_by+"/"+encodedValue, extract: deyamlize, }).then(function(response){ console.debug("Info GET Response: ", response);