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* diff --git a/CHANGES.md b/CHANGES.md index 4ecda6fed..dda7aec40 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,20 @@ # 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 +- Fix: search values trimmed and urlencoded + ## 4.3.2 2022-01-21 - Persistent kumato mode using local browser storage @@ -23,14 +38,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 + +- 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 ## 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 a9ccfb523..4b8df38c4 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,109 @@ +# TODO's + +## 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 +- [ ] Call Info: Intercept backend connection errors and behave +- [ ] Call info: List previous calls from same person/contract +- [ ] Unify call log also into the case log +- [ ] Google login +- [ ] API tests in fastapi +- [ ] Accept fragile erp tests +- [ ] Strip spaces in the search +- [ ] Edit previous annotations +- [ ] 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} +- [ ] 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 +- [ ] 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 + +- 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 scheduler script +- Person editor: + - [ ] Disable ok until all fields are valid + - [ ] Check extension not taken already + - [ ] Check erp user exists + - [ ] Focus on first dialog field on open + - [ ] Take person info from holidays manager + - [ ] List/admin mode +- 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 + +- [ ] 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/ERP: how to value solved, depending of resolution +- [ ] callinfo log: join infos/claims with log? (consider performance and usage) +- [ ] create crm: extract seccio del reason and remove the field +- [ ] create crm: cas contracte no existeix +- [ ] callreg: Rename Claims to reflect its repurposing +- [ ] 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 +- [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) +- [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` +- [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 @@ -25,35 +131,26 @@ - [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 -- [ ] Create Phone Call Case -- [ ] One endpoint for call registry in API -- [ ] One method for call registry in CallRegistry -- [ ] `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 -- [ ] 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 -- [ ] 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/{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 +- [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 + 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..bcda8451c --- /dev/null +++ b/b2bdata/tomatic.claims_test.Claims_Test.test_atcCategories-expected @@ -0,0 +1,93 @@ +categories: +- '[ASSIGNAR USUARI] 001. ATENCION INCORRECTA' +- '[RECLAMACIONS] 002. PRIVACIDAD DE LOS DATOS' +- '[FACTURA] 003. INCIDENCIA EN EQUIPOS DE MEDIDA' +- '[RECLAMACIONS] 004. DAÑOS ORIGINADOS POR EQUIPO DE MEDIDA' +- '[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' +- '[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' +- '[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' +- '[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' +- '[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' +- '[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' +- '[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' +- '[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' +- '[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' +- '[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_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/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..6ebe932c5 --- /dev/null +++ b/b2bdata/tomatic.claims_test.Claims_Test.test_crmCategories-expected @@ -0,0 +1,28 @@ +categories: +- '[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' 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 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 000000000..c6ad287f8 Binary files /dev/null and b/doc/claims.png differ 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 + + diff --git a/scripts/migrations/202201-subtypes-as-categories.py b/scripts/migrations/202201-subtypes-as-categories.py new file mode 100755 index 000000000..b8b24f929 --- /dev/null +++ b/scripts/migrations/202201-subtypes-as-categories.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python + +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): + 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(): + step("Fix double '][' in INFOENERGIA name") + erp.CrmCaseCateg.write(91, dict( + name='[INFOENERGIA] Consulta sobre els seus informes', + )) + + 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 + 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, + )) + + step("Create a new category for each claim subtype") + 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() + + 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 +else: + if '--apply' in sys.argv: + success("Applying changes") + erp.commit() + else: + warn("Use --apply to really apply. Rolling back changes.") + erp.rollback() + + + + 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/scripts/tomatic_uploadcases.py b/scripts/tomatic_uploadcases.py old mode 100644 new mode 100755 index b44c19744..143d60036 --- a/scripts/tomatic_uploadcases.py +++ b/scripts/tomatic_uploadcases.py @@ -29,8 +29,12 @@ 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)) + if claims.is_atc_case(case): + atc_case_id = claims.create_atc_case(case, crm_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/api.py b/tomatic/api.py index d7332e54e..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') @@ -366,12 +367,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) @@ -442,16 +443,26 @@ 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/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.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') @@ -489,16 +500,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 +519,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 diff --git a/tomatic/callregistry.py b/tomatic/callregistry.py index 693bda1e6..fa9ac0ea3 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') @@ -41,11 +40,17 @@ def updateCall(self, extension, fields): calls.dump(self.path) - def annotateInfoRequest(self, data): - self._appendToExtensionDailyInfo('info_cases', data) - - def annotateClaim(self, data): - self._appendToExtensionDailyInfo('atc_cases', data) + def annotateCall(self, fields): + from . import persons + extension = persons.extension(fields.user) or fields.user + self.updateCall(extension, ns( + data = fields.date, + telefon = fields.phone, + partner = fields.partner, + contracte = fields.contract, + motius = fields.reason, + )) + self._appendToExtensionDailyInfo('cases', fields) def _appendToExtensionDailyInfo(self, prefix, info, date=datetime.today()): path = self.path.parent / prefix / '{:%Y%m%d}.yaml'.format(date) @@ -53,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)) @@ -91,8 +96,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/callregistry_test.py b/tomatic/callregistry_test.py new file mode 100644 index 000000000..4a7cd55e0 --- /dev/null +++ b/tomatic/callregistry_test.py @@ -0,0 +1,222 @@ +import unittest +from pathlib import Path +from yamlns import namespace as ns +from datetime import date +from .callregistry import CallRegistry +from .persons import persons + +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.assertEqual(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 + """) + + 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), + ]) + + diff --git a/tomatic/claims.py b/tomatic/claims.py index 6f941c9a4..d1a9252c9 100644 --- a/tomatic/claims.py +++ b/tomatic/claims.py @@ -1,96 +1,126 @@ # -*- 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 +from consolemsg import warn + +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] + +def resolutionCode(case): + return dict( + unsolved = '', + fair = '01', + unfair = '02', + irresolvable = '03', + ).get(case.resolution, 'bad') + + +PHONE_CHANNEL = 2 +TIME_TRACKER_COMERCIALIZADORA = 1 +CLAIMANT = '01' # Titular de PS/ Usuario efectivo (Tabla 83) +CRM_CASE_SECTION_NAME = 'CONSULTA' +SECTION_TO_BE_SPECIFIED = 'ASSIGNAR USUARI' + + +def unknownState(erp): + if hasattr(unknownState,'cached'): + return unknownState.cached + unknownState.cached = erp.ResCountryState.search([ + ('code', '=', '00') + ])[0] + return unknownState.cached -PHONE = 2 -COMERCIALIZADORA = 1 -RECLAMANTE = '01' +class Claims(object): -def partnerId(erp, partner): - partner_model = erp.ResPartner - return partner_model.browse([('ref', '=', partner)])[0].id - - -def partnerAddress(erp, partner_id): - partner_address_model = erp.ResPartnerAddress - return partner_address_model.read( - [('partner_id', '=', partner_id)], - ['id', 'state_id', 'email'] - )[0] - - -def contractId(erp, contract): - contract_model = erp.GiscedataPolissa - return contract_model.browse([("name", "=", contract)])[0].id - - -def cupsId(erp, cups): - cups_model = erp.GiscedataCupsPs - return cups_model.browse([('name', '=', cups)])[0].id - + def __init__(self, erp): + self.erp = erp -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: + 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.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.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.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.search('res.partner.address', [ + ('email', '=', email), + ]) + user_ids = self.erp.search('res.users', [ + ('address_id', 'in', address_ids), + ]) + if user_ids: return user_ids[0] + # No match found return None + def _claimSubtypeByDescription(self, section_description): + return self.erp.search('giscedata.subtipus.reclamacio', [ + ('desc', '=', section_description) + ])[0] -def resultat(erp, procedente, improcedente): - if procedente: - return '01' - if improcedente: - return '02' - return '03' - - -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 - return sections_model.search([('name', 'ilike', section)])[0] - -defaultSection = 'ASSIGNAR USUARI' - -class Claims(object): - - def __init__(self, erp): - self.erp = erp + def _crmSectionID(self, section): + return self.erp.search('crm.case.section', [ + ('name', 'ilike', section) + ])[0] def get_claims(self): claims_model = self.erp.GiscedataSubtipusReclamacio 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 - 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, @@ -101,73 +131,168 @@ def get_claims(self): return claims - def create_atc_case(self, case): - ''' - Expected case: - - namespace( - person: - - date: D-M-YYYY H:M:S - person: person - reason: '[´section.name´] ´claim.name´. ´claim.desc´' - partner: partner number - contract: contract number - procedente: '' - improcedente: '' - solved: '' - user: section.name - cups: cups number - observations: comments - - ... - ... - ) - ''' - 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() + 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 [ + 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): + CallAnnotation(**case) + partner_id = self._partnerId(case.partner) + partner_address = self._partnerAddress(partner_id) + + category_description = case.reason.split('.',1)[-1].strip() + categ_ids = self.erp.search('crm.case.categ', [ + ('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 = self._crmSectionID(case.get('claimsection', 'HelpDesk')) + data_crm = { 'section_id': crm_section_id, - 'name': sectionName(self.erp, claim_section_id), - 'canal_id': PHONE, - 'polissa_id': contractId(self.erp, case.contract), + 'name': category_description, + 'canal_id': PHONE_CHANNEL, + 'categ_id': categ_id, + 'polissa_id': self._contractId(case.contract), 'partner_id': partner_id, - 'partner_address_id': partner_address.get('id'), - 'state': 'done' if case.solved else 'open', - 'user_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), } - 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.observations + '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 + def create_atc_case(self, case): + ''' + Expected case: + 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 null + resolution: fair + claimsection: section.name + notes: comments + ''' + CallAnnotation(**case) + + crm_case_id = self.create_crm_case(case) + + partner_id = self._partnerId(case.partner) + partner_address = self._partnerAddress(partner_id) + claim_subtype_id = self._claimSubtypeByDescription( + case.reason.split('.',1)[-1].strip() + ) + 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 = { - 'provincia': partner_address.get('state_id')[0], + 'provincia': state_id, 'total_cups': 1, - '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 "", + 'cups_id': contract['cups'][0] if contract else None, + 'subtipus_id': claim_subtype_id, + 'reclamante': CLAIMANT, + 'resultat': resolutionCode(case), 'date': case.date, - 'email_from': partner_address.get('email'), - 'time_tracking_id': COMERCIALIZADORA + 'email_from': partner_address.get('email') if partner_address else False, + 'time_tracking_id': TIME_TRACKER_COMERCIALIZADORA, + 'crm_id': crm_case_id, } - # user_id = userId(self.erp, self.emails, case.person) - # if user_id: - # data_crm['create_uid'] = user_id - data_atc['crm_id'] = crm_id case = self.erp.GiscedataAtc.create(data_atc) return case.id + 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 24d320f9b..6e6203f74 100644 --- a/tomatic/claims_test.py +++ b/tomatic/claims_test.py @@ -1,17 +1,28 @@ # -*- encoding: utf-8 -*- import unittest +import b2btest import os from consolemsg import error from erppeek_wst import ClientWST from yamlns import namespace as ns from xmlrpc import client as xmlrpclib from .claims import Claims +from .persons import persons + try: import dbconfig 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( @@ -21,67 +32,408 @@ class Claims_Test(unittest.TestCase): 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('testpersons.yaml') + persons.path.write_text("""\ + erpusers: + marc: Marc + """, encoding='utf8') + if not dbconfig: return if not dbconfig.erppeek: return self.erp = ClientWST(**dbconfig.erppeek) self.erp.begin() - self.data_atc = dbconfig.data_atc def tearDown(self): + persons.path.unlink() + persons(self.old_persons or False) 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 - def test_getAllClaims(self): + from yamlns.testutils import assertNsEqual + + 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', + 'partner_address_id', + 'state', + 'user_id', + ])) + + fkname(result, "section_id") + fkname(result, "categ_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') + anonymize(result, 'user_id') + + if deep: + result.atc_id = self.atcCase([ + ('crm_id', '=', case_id), + ]) + if result.atc_id: + del result.atc_id.id + + return result + + def atcCase(self, atc_case_id, deep=False): + """Retrieves checkeable erp fields for an AtcCase""" + result = self.erp.GiscedataAtc.read(atc_case_id, [ + 'provincia', + 'total_cups', + 'cups_id', + 'subtipus_id', + 'reclamante', + 'resultat', + 'date', + 'email_from', + 'time_tracking_id', + 'state', + 'crm_id', + ]) + if not result: return False + result = ns(result[0]) + + fkname(result, "cups_id") + fkname(result, "subtipus_id") + fkname(result, "time_tracking_id") + fkname(result, "provincia") + 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, 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, deep=deep) + self.assertNsEqual(result, expected) + + def assertAtcCase(self, case_id, expected): + """Asserts that the AtcCase checkable erp fields do have + the proper values""" + if not expected: + self.assertFalse(case_id) + return + self.assertTrue(case_id) + result = self.atcCase([case_id]) + self.assertNsEqual(result, expected) + + 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' + phone: '555444333' + user: albert + reason: '[RECLAMACIONS] 003. INCIDENCIA EN EQUIPOS DE MEDIDA' + partner: S001975 + contract: '0013117' + resolution: fair + claimsection: RECLAMACIONS + notes: User annotated text + """) + base.update(**kwds) + return base + + def info_base(self, **kwds): + base = ns.loads("""\ + date: '2021-11-11T15:13:39.998Z' + phone: '555444333' + user: albert + reason: "[COBRAMENTS] Informació sobre el tall de subministrament" + partner: S001975 + contract: '0013117' + notes: User annotated text + """) + 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_categories(self): + claims = Claims(self.erp) + categories = claims.categories() + self.assertB2BEqual(ns(categories=categories).dump()) + + def test_createAtcCase_procedente(self): + case = self.claim_base() + + claims = Claims(self.erp) + case_id = claims.create_atc_case(case) + self.assertAtcCase(case_id, """ + cups_id: ...M0F + date: '2021-11-11 15:13:39.998' + email_from: ...oop + id: {} + provincia: Barcelona + reclamante: '01' + resultat: '01' + subtipus_id: '003' + time_tracking_id: Comercialitzadora + state: open + total_cups: 1 + """.format(case_id)) + + def test_createAtcCase_improcedente(self): + case = self.claim_base( + resolution='unfair', + ) + + claims = Claims(self.erp) + case_id = claims.create_atc_case(case) + 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 + state: open + total_cups: 1 + """.format(case_id)) + + def test_createAtcCase_noSolution(self): + case = self.claim_base( + resolution='irresolvable', + ) + + claims = Claims(self.erp) + case_id = claims.create_atc_case(case) + 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 + state: open + total_cups: 1 + """.format(case_id)) + + def test_createAtcCase_unsolved(self): + case = self.claim_base( + resolution='unsolved', + ) + + claims = Claims(self.erp) + case_id = claims.create_atc_case(case) + self.assertAtcCase(case_id, """ + cups_id: ...M0F + date: '2021-11-11 15:13:39.998' + email_from: ...oop + id: {} + provincia: Barcelona + reclamante: '01' + resultat: '' # <--------- THIS CHANGES + subtipus_id: '003' + time_tracking_id: Comercialitzadora + state: open + total_cups: 1 + """.format(case_id)) + + def test_createCrmCase(self): + 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: '[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 + """.format(case_id)) + + def test_createCrmCase_erpuserInPersons(self): + 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í + partner_id: ...osé + polissa_id: '0013117' + section_id: Atenció al Client / RECLAMACIONS + state: open + user_id: ...lló # <--THIS CHANGES + """.format(case_id)) + + def test_createCrmCase_noContract(self): + 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: '[COBRAMENTS] Informació sobre el tall de subministrament' + partner_address_id: ...spí + partner_id: ...osé + polissa_id: False # <--------- THIS CHANGES + section_id: HelpDesk + state: open + user_id: false + """.format(case_id)) + + def test_createCrmCase_noPartner(self): + case = self.info_base( + contract = '', + partner = '', + ) + claims = Claims(self.erp) + case_id = claims.create_crm_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: False + partner_id: False # <--------- THIS CHANGES + polissa_id: False # <--------- THIS CHANGES + section_id: HelpDesk + state: open + 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_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) + 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_withInfo_doesNotCreateAtc(self): + case = self.info_base() claims = Claims(self.erp) - reclamacions = claims.get_claims() - Reclamacio = self.erp.GiscedataSubtipusReclamacio - 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_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) - - 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) + 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 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 df6700b56..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 \ No newline at end of file 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()) diff --git a/tomatic/static/components/callinfo.js b/tomatic/static/components/callinfo.js index b5c90097b..7b32a50e8 100644 --- a/tomatic/static/components/callinfo.js +++ b/tomatic/static/components/callinfo.js @@ -12,9 +12,11 @@ var styleCallinfo = require('./callinfo_style.styl'); var CallInfo = {}; var websock = null; +CallInfo.topics = []; // Call topics +CallInfo.sections = []; // Teams to assign a call 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 @@ -24,30 +26,97 @@ 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 - 'extra': "", // annotated comments for the call - 'log_call_reasons': [], + 'topic': "", // annotated topic for the call + 'notes': "", // annotated comments for the call }; -CallInfo.call_reasons = { - 'general': [], - 'infos': [], - 'extras': [] -} -CallInfo.extras_dict = {}; + CallInfo.savingAnnotation = false; +CallInfo.annotation = {}; + +CallInfo.resetAnnotation = function() { + var tag = CallInfo.reasonTag() + CallInfo.annotation = { + resolution: 'unsolved', + tag: tag, + } +}; +CallInfo.noSection = "ASSIGNAR USUARI"; +CallInfo.helpdeskSection = "CONSULTA"; +CallInfo.hasNoSection = function() { + return CallInfo.annotation.tag === CallInfo.noSection; +}; +CallInfo.reasonTag = function() { + var topic = CallInfo.call.topic; + var matches = topic.match(/\[(.*?)\]/); + if (matches) { + return matches[1].trim(); + } + 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.notes = ""; + CallInfo.call.date = ""; + } + }, function(error) { + console.debug('Info POST apicall failed: ', error); + }); + CallInfo.call.topic = ""; +} + +CallInfo.annotationIsClaim = function() { + return CallInfo.reasonTag() !== CallInfo.helpdeskSection; +} + +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.topic, + "notes": CallInfo.call.notes, + "claimsection": ( + !isClaim ? "" : ( + claim.tag ? claim.tag : ( + CallInfo.helpdeskSection + ))), + "resolution": isClaim ? claim.resolution:'', + }); +} CallInfo.clear = function() { CallInfo.call.phone = ""; - CallInfo.call.log_call_reasons = []; - CallInfo.call.reason = ""; - CallInfo.call.extra = ""; - CallInfo.call.proc = false; - CallInfo.call.improc = false; + CallInfo.call.topic = ""; + CallInfo.call.notes = ""; CallInfo.currentPerson = 0; CallInfo.currentContract = 0; CallInfo.savingAnnotation = false; - CallInfo.file_info = {}; + CallInfo.searchResults = {}; } @@ -55,9 +124,8 @@ 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; } @@ -74,10 +142,15 @@ 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(); } +CallInfo.selectableSections = function() { + return CallInfo.sections.map(function(section) { + return section.name; + }); +}; function formatContractNumber(number) { // some contract numbers get converted to int and lose their padding @@ -99,18 +172,45 @@ function contractNumbers(info) { } -CallInfo.getExtras = function (extras) { - return extras.map(function(extra) { - return CallInfo.extras_dict[extra]; - }); +CallInfo.filteredTopics = function(filter) { + 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) { + 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; }; @@ -131,29 +231,30 @@ CallInfo.selectContract = function(idx) { var retrieveInfo = function () { + var encodedValue = encodeURIComponent(CallInfo.search.trim()); m.request({ method: 'GET', - url: '/api/info/'+CallInfo.search_by+"/"+CallInfo.search, + url: '/api/info/'+CallInfo.search_by+"/"+encodedValue, extract: deyamlize, }).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', @@ -177,47 +278,32 @@ var retrieveInfo = 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.extras_dict = response.info.dict; - CallInfo.call_reasons.extras = Object.keys(response.info.dict); - } - }, function(error) { - console.debug('Info GET apicall failed: ', error); - }); -}; - - -CallInfo.getInfos = function() { +CallInfo.getTopics = function() { m.request({ method: 'GET', - url: '/api/getInfos', + url: '/api/call/annotate/topics', 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; - } + 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); }); -}; - +} +// TODO: updateTopics instead CallInfo.updateClaims = function() { CallInfo.updatingClaims = true; m.request({ @@ -231,13 +317,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({ @@ -251,7 +338,7 @@ CallInfo.updateCrmCategories = function() { } else{ CallInfo.updatingCrmCategories = false; - CallInfo.getInfos(); + CallInfo.getTopics(); } }, function(error) { console.debug('Info GET apicall failed: ', error); @@ -311,13 +398,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 = {} } } @@ -353,9 +440,7 @@ CallInfo.onMessageReceived = function(event) { } console.debug("Message received from WebSockets and type not recognized."); } - -CallInfo.getClaims(); -CallInfo.getInfos(); +CallInfo.getTopics(); CallInfo.getLogPerson() Login.onLogin.push(CallInfo.sendIdentification); diff --git a/tomatic/static/components/callinfo_style.styl b/tomatic/static/components/callinfo_style.styl index 3045d52d8..7cc931b21 100644 --- a/tomatic/static/components/callinfo_style.styl +++ b/tomatic/static/components/callinfo_style.styl @@ -357,20 +357,24 @@ 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.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 .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 @@ -421,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: #d9bbbb - 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/callinfopage.js b/tomatic/static/components/callinfopage.js index d88faeea8..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()) }}, ] }); @@ -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), ]) ))) ), diff --git a/tomatic/static/components/login.js b/tomatic/static/components/login.js index 7b2d250a1..bb0d5573a 100644 --- a/tomatic/static/components/login.js +++ b/tomatic/static/components/login.js @@ -36,23 +36,14 @@ 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=/" - var info = { - iden: "", - ext: -1 - } Login.onLogout.map(function(callback) { - callback(info); + callback(); }) } @@ -61,21 +52,18 @@ Date.prototype.addHours = function(h) { return this; } - -var getMyExt = function() { - var cookie_value = getCookie(tomaticCookie); - if (cookie_value === ":") return ""; - return cookie_value.split(":")[1].toString(); +Login.myName = function() { + var cookie = whoAreYou(); + var user = cookie.split(":")[0]; + return user; } -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){ diff --git a/tomatic/static/components/questionnaire.js b/tomatic/static/components/questionnaire.js index 9059bee0f..a31bf8482 100644 --- a/tomatic/static/components/questionnaire.js +++ b/tomatic/static/components/questionnaire.js @@ -23,145 +23,20 @@ var Login = require('./login'); var Questionnaire = {}; -var call_reasons = CallInfo.call_reasons; -var reason_filter = ""; +var topicFilter = ""; -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', - 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.proc = false; - CallInfo.call.improc = false; - } - }, 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({ - "date": isodate, - "phone": CallInfo.call.phone, - "person": user, - "reason": CallInfo.call.reason, - "extra": CallInfo.call.extra, - }); -} - - -var llistaMotius = function() { - const all=true; - function contains(value) { - var contains = value.toLowerCase().includes(reason_filter.toLowerCase()); - return contains; - } - var list_reasons = [].concat( - call_reasons.infos, - all ? 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; - } - - var disabled = (CallInfo.savingAnnotation || CallInfo.call.date === "" ); +var topicList = function() { + var topics = CallInfo.filteredTopics(topicFilter) return m(".motius", m(List, { compact: true, indentedBorder: true, - tiles: filtered.map(function(reason) { + compact: true, + tiles: topics.map(function(topic) { return m(ListTile, { className: ( - CallInfo.call.reason === reason + CallInfo.call.topic === topic ? "llista-motius-selected" : "llista-motius-unselected" ), @@ -169,14 +44,14 @@ var llistaMotius = 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, }); }), @@ -191,19 +66,15 @@ var clipboardIcon = function(){ } Questionnaire.annotationButton = function() { - var partner = CallInfo.selectedPartner(); - var contract = CallInfo.selectedContract(); return m("", m(IconButton, { - icon: m("i.far.fa-clipboard.icon-clipboard"), icon: clipboardIcon(), wash: true, compact: true, title: "Anota la trucada fent servir aquest contracte", events: { onclick: function() { - console.log("VEURE QÜESTIONARI INFOS") - Questionnaire.openCaseAnnotationDialog(contract, partner); + Questionnaire.openCaseAnnotationDialog(); }, }, disabled: ( @@ -215,108 +86,71 @@ 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 getTag = function(reason) { - var matches = reason.match(/\[(.*?)\]/); - if (matches) { - return matches[1].trim(); - } - return ""; - } - - var esReclamacio = function(type) { - const info = "INFO"; - return (type != info); - } - - 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 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", { id: "select-user", class: ".select-user", - disabled: section !== "ASSIGNAR USUARI", - default: section, - onchange: function() { - reclamacio.tag = document.getElementById("select-user").value; + disabled: !selectable, + default: defaultSection, + oninput: function(ev) { + CallInfo.annotation.tag = ev.target.value; }, }, - options.map(function(option) { - return - m("option", { + m("option", { + value: defaultSection, + selected: !sections.includes(defaultSection) + }, defaultSection), + selectable && sections.map(function(option) { + return m("option", { "value": option, - "selected": section === option + "selected": CallInfo.annotation.tag === option }, option); - }), - m("option", { - "value": section, - "selected": !options.includes(section) - }, section) + }) ) ]); } - var preguntarResolt = function(reclamacio) { + var resolutionChoser = function() { return m(".case-resolution", [ m("p", "Resolució:"), m(RadioGroup, { name: 'resolution', onChange: function(state) { - reclamacio.solved = state.value != 'unsolved'; - reclamacio.proc = state.value == 'procedent'; - reclamacio.improc = state.value == 'improcedent'; + CallInfo.annotation.resolution = state.value; }, - checkedValue: ( - !reclamacio.solved ? 'unsolved' : - reclamacio.proc ? 'procedent' : - reclamacio.improc ? 'improcedent' : - 'nogestionable' - ), + checkedValue: CallInfo.annotation.resolution, 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: 'irresolvable', }], }), ]) } - var buttons = function(reclamacio) { + var buttons = function() { return [ m(Button, { label: "Cancel·lar", @@ -331,36 +165,31 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { label: "Desa", events: { onclick: function() { - enviar(reclamacio); + CallInfo.saveCallLog(); Dialog.hide({ id: 'fillReclama' }); Dialog.hide({ id: 'settingsDialog' }); }, }, - disabled: (reclamacio.tag === "ASSIGNAR USUARI"), + disabled: CallInfo.hasNoSection(), contained: true, raised: true, }) ]; } - var emplenaReclamacio = function(tag) { - var reclamacio = { - "proc": false, - "improc": false, - "solved": false, - "tag": tag - } + var emplenaReclamacio = function() { + CallInfo.resetAnnotation(); Dialog.show(function() { return { className: 'dialog-reclama', title: 'Reclamació:', backdrop: true, body: [ - seleccionaUsuari(reclamacio, tag), + sectionSelector(), m("br"), - preguntarResolt(reclamacio), + resolutionChoser(), ], - footerButtons: buttons(reclamacio), + footerButtons: buttons(), };},{id:'fillReclama'}); } @@ -402,15 +231,15 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { m(".filter", m(Textfield, { className: "textfield-filter", - label: "Escriure per filtrar", - value: reason_filter, + label: "Escriu per a filtrar", + value: topicFilter, dense: true, onChange: function(params) { - reason_filter = params.value + topicFilter = params.value } })), ]), - llistaMotius(), + topicList(), m(".final-motius", [ m(Textfield, { className: "textfield-comentaris", @@ -420,9 +249,9 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { 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 === "", }), @@ -441,15 +270,16 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { }), m(Button, { className: "save", - label: CallInfo.savingAnnotation?"Desant":"Desa", + label: CallInfo.savingAnnotation?"Desant":( + CallInfo.annotationIsClaim()?"Continua": + "Desa"), events: { onclick: function() { - var tag = getTag(CallInfo.call.reason); - if (esReclamacio(tag)) { - emplenaReclamacio(tag); + if (CallInfo.annotationIsClaim()) { + emplenaReclamacio(); } else { - enviar(""); + CallInfo.saveCallLog(); Dialog.hide({id:'settingsDialog'}); } }, @@ -457,8 +287,8 @@ Questionnaire.openCaseAnnotationDialog = function(contract, partner) { border: 'true', disabled: ( CallInfo.savingAnnotation || - CallInfo.call.reason === "" || - CallInfo.call.extra === "" || + CallInfo.call.topic === "" || + CallInfo.call.notes === "" || CallInfo.call.date === "" || Login.myName() === "" ), 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 000000000..8d546261d Binary files /dev/null and b/tomatic/static/ketchup.png differ 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 000000000..878e5a294 Binary files /dev/null and b/tomatic/static/pebrotic.jpg differ diff --git a/tomatic/static/style.styl b/tomatic/static/style.styl index 538d0deb4..8cd72175d 100644 --- a/tomatic/static/style.styl +++ b/tomatic/static/style.styl @@ -341,3 +341,16 @@ select.pe-textfield__input { pointer-events: auto; } } + +#tomatic.main.main.main { + .variant-pebrotic { + .tmt-header { + background-image: url(./pebrotic.jpg) + } + } + .variant-ketchup { + .tmt-header { + background-image: url(./ketchup.png) + } + } +}