From d0291482ab969b4c2c5a7bd61a059130a7ee8148 Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Wed, 9 Dec 2020 18:35:10 +0530 Subject: [PATCH 001/111] add sub-project feater --- sub_project/__init__.py | 1 + sub_project/__manifest__.py | 16 +++++++++++ .../__pycache__/__init__.cpython-36.pyc | Bin 0 -> 177 bytes sub_project/models/__init__.py | 1 + .../__pycache__/__init__.cpython-36.pyc | Bin 0 -> 189 bytes .../__pycache__/sub_project.cpython-36.pyc | Bin 0 -> 576 bytes sub_project/models/sub_project.py | 9 ++++++ sub_project/views/sub_project.xml | 26 ++++++++++++++++++ 8 files changed, 53 insertions(+) create mode 100644 sub_project/__init__.py create mode 100644 sub_project/__manifest__.py create mode 100644 sub_project/__pycache__/__init__.cpython-36.pyc create mode 100644 sub_project/models/__init__.py create mode 100644 sub_project/models/__pycache__/__init__.cpython-36.pyc create mode 100644 sub_project/models/__pycache__/sub_project.cpython-36.pyc create mode 100644 sub_project/models/sub_project.py create mode 100644 sub_project/views/sub_project.xml diff --git a/sub_project/__init__.py b/sub_project/__init__.py new file mode 100644 index 0000000..9a7e03e --- /dev/null +++ b/sub_project/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/sub_project/__manifest__.py b/sub_project/__manifest__.py new file mode 100644 index 0000000..8e6f221 --- /dev/null +++ b/sub_project/__manifest__.py @@ -0,0 +1,16 @@ +{ + 'name': 'sub_project', + 'version': '', + 'summary': 'Sub Project of sub project', + 'description': 'Sub Project of sub project', + 'category': '', + 'author': '', + 'website': '', + 'license': '', + 'depends': ['base', 'project'], + 'data': ['views/sub_project.xml'], + 'demo': [''], + 'installable': True, + 'auto_install': False, + +} \ No newline at end of file diff --git a/sub_project/__pycache__/__init__.cpython-36.pyc b/sub_project/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..07b288ba513bbdbcae4b3f6573f1d6f974934859 GIT binary patch literal 177 zcmXr!<>l(tI~6a&z`*brh~a<<$Z`PUVgVqL!jQt4!;s4u#mLBz!W7J)$^4QLD6GkN zi!C=lB{iqmPm}2uLlH>NN`@j9AO$9V+3V*Ql<4QDEG6 y{p9>2U68DPacNR~K~a8IYI2Ewe0*kJW=VX!UP0w84jZ8Pr8%i~AnS^Om;nHspDXGB literal 0 HcmV?d00001 diff --git a/sub_project/models/__init__.py b/sub_project/models/__init__.py new file mode 100644 index 0000000..d30a3e9 --- /dev/null +++ b/sub_project/models/__init__.py @@ -0,0 +1 @@ +from . import sub_project \ No newline at end of file diff --git a/sub_project/models/__pycache__/__init__.cpython-36.pyc b/sub_project/models/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..db5f8344afea4897ccdc78d41178a459529ffb1a GIT binary patch literal 189 zcmXr!<>l(tI~6aLY6a0WML6DXu)~RfH0ZZV-4t zUJ(#ZX8wZE0Q@ORf-f775@JYJ7bbitcnR8sl>}bu+;QviTjAJxIHDL3^#D@>9`$)J z3xO;!?O7B71|Nbqk8VL|YxGP!ZSN;a&UoI;2lWz+R_lD*l80-yf2$Aw?I)SyqFM6^ zHlc1*As)v`Y$C=)R&mBm%$SwjmJaVQ_S|OWiiubup13L+>j$oLRn$!(g(CJArgtr6 z$umLwg=V_FXDdx6y354NsT7>{>rTL$c}J>mPj=?}HkEagN|5s8yjzBOs~cGjGXN=c znoBiwY`T#;UAA`9HL71!V3WEI>&Ti2``Gtl+5hxf@3f~}jC literal 0 HcmV?d00001 diff --git a/sub_project/models/sub_project.py b/sub_project/models/sub_project.py new file mode 100644 index 0000000..39eaf43 --- /dev/null +++ b/sub_project/models/sub_project.py @@ -0,0 +1,9 @@ +from odoo import api, fields, models + + +class SubProject(models.Model): + _inherit = "project.project" + _description = "Sub Project" + + is_sub_project = fields.Boolean("Is Sub Project") + parent_project = fields.Many2one('project.project', string='Parent Project') \ No newline at end of file diff --git a/sub_project/views/sub_project.xml b/sub_project/views/sub_project.xml new file mode 100644 index 0000000..51a8219 --- /dev/null +++ b/sub_project/views/sub_project.xml @@ -0,0 +1,26 @@ + + + + + + project.project.form.inherit + project.project + + + + + + + + + + + + + + + + + + \ No newline at end of file From d1ab7228d880467277181ce423af0e84f1e8d150 Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Wed, 9 Dec 2020 18:35:37 +0530 Subject: [PATCH 002/111] add sub-project feater --- sub_project/__pycache__/__init__.cpython-36.pyc | Bin 177 -> 0 bytes .../models/__pycache__/__init__.cpython-36.pyc | Bin 189 -> 0 bytes .../models/__pycache__/sub_project.cpython-36.pyc | Bin 576 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 sub_project/__pycache__/__init__.cpython-36.pyc delete mode 100644 sub_project/models/__pycache__/__init__.cpython-36.pyc delete mode 100644 sub_project/models/__pycache__/sub_project.cpython-36.pyc diff --git a/sub_project/__pycache__/__init__.cpython-36.pyc b/sub_project/__pycache__/__init__.cpython-36.pyc deleted file mode 100644 index 07b288ba513bbdbcae4b3f6573f1d6f974934859..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 177 zcmXr!<>l(tI~6a&z`*brh~a<<$Z`PUVgVqL!jQt4!;s4u#mLBz!W7J)$^4QLD6GkN zi!C=lB{iqmPm}2uLlH>NN`@j9AO$9V+3V*Ql<4QDEG6 y{p9>2U68DPacNR~K~a8IYI2Ewe0*kJW=VX!UP0w84jZ8Pr8%i~AnS^Om;nHspDXGB diff --git a/sub_project/models/__pycache__/__init__.cpython-36.pyc b/sub_project/models/__pycache__/__init__.cpython-36.pyc deleted file mode 100644 index db5f8344afea4897ccdc78d41178a459529ffb1a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 189 zcmXr!<>l(tI~6aLY6a0WML6DXu)~RfH0ZZV-4t zUJ(#ZX8wZE0Q@ORf-f775@JYJ7bbitcnR8sl>}bu+;QviTjAJxIHDL3^#D@>9`$)J z3xO;!?O7B71|Nbqk8VL|YxGP!ZSN;a&UoI;2lWz+R_lD*l80-yf2$Aw?I)SyqFM6^ zHlc1*As)v`Y$C=)R&mBm%$SwjmJaVQ_S|OWiiubup13L+>j$oLRn$!(g(CJArgtr6 z$umLwg=V_FXDdx6y354NsT7>{>rTL$c}J>mPj=?}HkEagN|5s8yjzBOs~cGjGXN=c znoBiwY`T#;UAA`9HL71!V3WEI>&Ti2``Gtl+5hxf@3f~}jC From da0039b4826f8ed87a3b1637f8ff38d08f59ebd6 Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Wed, 9 Dec 2020 18:37:46 +0530 Subject: [PATCH 003/111] create custom module for project screen --- cor_custom/__init__.py | 4 ++ cor_custom/__manifest__.py | 35 +++++++++++++++ cor_custom/controllers/__init__.py | 3 ++ cor_custom/controllers/controllers.py | 21 +++++++++ cor_custom/demo/demo.xml | 30 +++++++++++++ cor_custom/models/__init__.py | 3 ++ cor_custom/models/models.py | 18 ++++++++ cor_custom/security/ir.model.access.csv | 2 + cor_custom/views/templates.xml | 24 ++++++++++ cor_custom/views/views.xml | 60 +++++++++++++++++++++++++ 10 files changed, 200 insertions(+) create mode 100644 cor_custom/__init__.py create mode 100644 cor_custom/__manifest__.py create mode 100644 cor_custom/controllers/__init__.py create mode 100644 cor_custom/controllers/controllers.py create mode 100644 cor_custom/demo/demo.xml create mode 100644 cor_custom/models/__init__.py create mode 100644 cor_custom/models/models.py create mode 100644 cor_custom/security/ir.model.access.csv create mode 100644 cor_custom/views/templates.xml create mode 100644 cor_custom/views/views.xml diff --git a/cor_custom/__init__.py b/cor_custom/__init__.py new file mode 100644 index 0000000..511a0ca --- /dev/null +++ b/cor_custom/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import controllers +from . import models \ No newline at end of file diff --git a/cor_custom/__manifest__.py b/cor_custom/__manifest__.py new file mode 100644 index 0000000..517a6b0 --- /dev/null +++ b/cor_custom/__manifest__.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +{ + 'name': "cor_custom", + + 'summary': """ + Short (1 phrase/line) summary of the module's purpose, used as + subtitle on modules listing or apps.openerp.com""", + + 'description': """ + Long description of module's purpose + """, + + 'author': "My Company", + 'website': "http://www.yourcompany.com", + + # Categories can be used to filter modules in modules listing + # Check https://github.com/odoo/odoo/blob/14.0/odoo/addons/base/data/ir_module_category_data.xml + # for the full list + 'category': 'Uncategorized', + 'version': '0.1', + + # any module necessary for this one to work correctly + 'depends': ['base'], + + # always loaded + 'data': [ + # 'security/ir.model.access.csv', + 'views/views.xml', + 'views/templates.xml', + ], + # only loaded in demonstration mode + 'demo': [ + 'demo/demo.xml', + ], +} diff --git a/cor_custom/controllers/__init__.py b/cor_custom/controllers/__init__.py new file mode 100644 index 0000000..457bae2 --- /dev/null +++ b/cor_custom/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import controllers \ No newline at end of file diff --git a/cor_custom/controllers/controllers.py b/cor_custom/controllers/controllers.py new file mode 100644 index 0000000..a8a213a --- /dev/null +++ b/cor_custom/controllers/controllers.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# from odoo import http + + +# class CorCustom(http.Controller): +# @http.route('/cor_custom/cor_custom/', auth='public') +# def index(self, **kw): +# return "Hello, world" + +# @http.route('/cor_custom/cor_custom/objects/', auth='public') +# def list(self, **kw): +# return http.request.render('cor_custom.listing', { +# 'root': '/cor_custom/cor_custom', +# 'objects': http.request.env['cor_custom.cor_custom'].search([]), +# }) + +# @http.route('/cor_custom/cor_custom/objects//', auth='public') +# def object(self, obj, **kw): +# return http.request.render('cor_custom.object', { +# 'object': obj +# }) diff --git a/cor_custom/demo/demo.xml b/cor_custom/demo/demo.xml new file mode 100644 index 0000000..1d8cbab --- /dev/null +++ b/cor_custom/demo/demo.xml @@ -0,0 +1,30 @@ + + + + + \ No newline at end of file diff --git a/cor_custom/models/__init__.py b/cor_custom/models/__init__.py new file mode 100644 index 0000000..5305644 --- /dev/null +++ b/cor_custom/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models \ No newline at end of file diff --git a/cor_custom/models/models.py b/cor_custom/models/models.py new file mode 100644 index 0000000..c89ae76 --- /dev/null +++ b/cor_custom/models/models.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- + +# from odoo import models, fields, api + + +# class cor_custom(models.Model): +# _name = 'cor_custom.cor_custom' +# _description = 'cor_custom.cor_custom' + +# name = fields.Char() +# value = fields.Integer() +# value2 = fields.Float(compute="_value_pc", store=True) +# description = fields.Text() +# +# @api.depends('value') +# def _value_pc(self): +# for record in self: +# record.value2 = float(record.value) / 100 diff --git a/cor_custom/security/ir.model.access.csv b/cor_custom/security/ir.model.access.csv new file mode 100644 index 0000000..fdc1832 --- /dev/null +++ b/cor_custom/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_cor_custom_cor_custom,cor_custom.cor_custom,model_cor_custom_cor_custom,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/cor_custom/views/templates.xml b/cor_custom/views/templates.xml new file mode 100644 index 0000000..cea6b39 --- /dev/null +++ b/cor_custom/views/templates.xml @@ -0,0 +1,24 @@ + + + + + \ No newline at end of file diff --git a/cor_custom/views/views.xml b/cor_custom/views/views.xml new file mode 100644 index 0000000..de75043 --- /dev/null +++ b/cor_custom/views/views.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file From bd33e2a81c341ae2b5823e279bd4330e6f96afd2 Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Thu, 10 Dec 2020 11:45:38 +0530 Subject: [PATCH 004/111] inherit employee price one2many field --- cor_custom/__manifest__.py | 3 +- .../__pycache__/__init__.cpython-36.pyc | Bin 0 -> 210 bytes .../__pycache__/__init__.cpython-36.pyc | Bin 0 -> 193 bytes .../__pycache__/controllers.cpython-36.pyc | Bin 0 -> 160 bytes cor_custom/models/__init__.py | 3 +- .../__pycache__/__init__.cpython-36.pyc | Bin 0 -> 213 bytes .../models/__pycache__/models.cpython-36.pyc | Bin 0 -> 150 bytes .../models/__pycache__/project.cpython-36.pyc | Bin 0 -> 787 bytes cor_custom/models/project.py | 17 ++++++++ cor_custom/views/project_view.xml | 39 ++++++++++++++++++ 10 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 cor_custom/__pycache__/__init__.cpython-36.pyc create mode 100644 cor_custom/controllers/__pycache__/__init__.cpython-36.pyc create mode 100644 cor_custom/controllers/__pycache__/controllers.cpython-36.pyc create mode 100644 cor_custom/models/__pycache__/__init__.cpython-36.pyc create mode 100644 cor_custom/models/__pycache__/models.cpython-36.pyc create mode 100644 cor_custom/models/__pycache__/project.cpython-36.pyc create mode 100755 cor_custom/models/project.py create mode 100755 cor_custom/views/project_view.xml diff --git a/cor_custom/__manifest__.py b/cor_custom/__manifest__.py index 517a6b0..2e73341 100644 --- a/cor_custom/__manifest__.py +++ b/cor_custom/__manifest__.py @@ -20,11 +20,12 @@ 'version': '0.1', # any module necessary for this one to work correctly - 'depends': ['base'], + 'depends': ['base', 'sale_timesheet'], # always loaded 'data': [ # 'security/ir.model.access.csv', + 'views/project_view.xml', 'views/views.xml', 'views/templates.xml', ], diff --git a/cor_custom/__pycache__/__init__.cpython-36.pyc b/cor_custom/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a2ca7f5dad7e0aa4741977cd979df0d8af3279e3 GIT binary patch literal 210 zcmXr!<>lIZ>O#Cb0|UcjAcg}bAj<)Wi)DaB3PTEG4nr-mKMKJ-{%)tzr zEH4>>N;Da7aVO{Jl@#UYlIZ>O#B`0|UcjAcg}bAj<)Wiv@s03PTEG4nr1ps*(6 zE$-y}ypp2)oSf96Vn0o$TMR`YjVl?7Sb!9m_~oUaUr?f-pOT+%Xac5mlS_+B@^f_) zQ&RHtiuIH8i*!MlIZ>O%Z$CI*Jb3`l?x$aVnYViq8g!Vt`$$>_I|p$H_5Ab$Dk=NFXd=cnZ7 z8=8PA-Q?2ZlKfoV#FUi$ykh<2{32bDEQk>gkpeRFN{aGxa#D*x@^H3ZLFFwDo80`A O(wtN~kTu0X%m4uV7%UI~ literal 0 HcmV?d00001 diff --git a/cor_custom/models/__init__.py b/cor_custom/models/__init__.py index 5305644..a0c0dd3 100644 --- a/cor_custom/models/__init__.py +++ b/cor_custom/models/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- -from . import models \ No newline at end of file +from . import models +from . import project \ No newline at end of file diff --git a/cor_custom/models/__pycache__/__init__.cpython-36.pyc b/cor_custom/models/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cdcfb0db09223f6b949dc3701e4d87fe649d7fd9 GIT binary patch literal 213 zcmXr!<>i{X=VH7w0|UcjAcg}bAj<)Wi)DaB3PTEG4nr-mKMKJ-{%)tzr zEH4>>N;Da7vE}Bcq~;U@S?mQx`B|ySC4QRBw-|~*MinswiIogRtROas_~ojfUr?f- zpOT+%Xac5mlS_+B@^f_)Q&RHtiuIH8i*!M&ryk0@&Ee;!? QU};XO9ms-WkX1a405-=pr~m)} literal 0 HcmV?d00001 diff --git a/cor_custom/models/__pycache__/models.cpython-36.pyc b/cor_custom/models/__pycache__/models.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..18206e2f506784bae4310bafab77e46a0f53af8b GIT binary patch literal 150 zcmXr!<>lIZ>O%YjMh1q*3`hXTXK(=GViq8g!Vt`$$>_I|p$H_5AbvUP=NFXd=cnZ7 z8=8PA-Q?2ZlKfoV#FUi$ykh<2{32bDEQk>gk8S%5Z)i#=Unctph|&I+LJh22pS}WB84AOi|C1|&$;uWZv@x|yU5F^cx_nV#hJUh?l^XccW-_wN26XK_R|UbN zNDR(U-BUE0WgJ*0fM-$%RS06ovq(>>803ya0O38ulOu?*&BUL6XgAuH{iQXPF37>Q z=-)P7ZEm!_$h+Go-5yF@UhAYTTb<|!IB5`HjLD~$wk)(i9#K_7fioF|yg)rib&t{9 zv5MJ8WGjBhlxQOuJTRms*4LOa$Wqz~27b|(jdmNYdsP_M`^BFw9d%uDA2?lKePQ+( z(|^55P1mOejCuBAaI0eLdef|O0Moj(Fm^>K1!@na`{AbJ?{+u-M47hO=;xlpk@_$oI{#9wcocdU)HgB|2eyS9**wz%!l=`&I>jxxO<^G{8y|xeE)h6%l z5yqD0>f@2Aozoc+JTNo*B)+5r@t6lrb&*lIC literal 0 HcmV?d00001 diff --git a/cor_custom/models/project.py b/cor_custom/models/project.py new file mode 100755 index 0000000..5692ac8 --- /dev/null +++ b/cor_custom/models/project.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models, _ + + +class InheritProjectProductEmployeeMap(models.Model): + _inherit = 'project.sale.line.employee.map' + + employee_price = fields.Float("Employee Price") + + @api.onchange('employee_id') + def _onchange_employee_price(self): + if self.employee_id: + self.employee_price = self.employee_id.timesheet_cost + else: + self.employee_price = 0.0 + diff --git a/cor_custom/views/project_view.xml b/cor_custom/views/project_view.xml new file mode 100755 index 0000000..ef4c630 --- /dev/null +++ b/cor_custom/views/project_view.xml @@ -0,0 +1,39 @@ + + + + + project.project.form.inherit + project.project + + + + + + + + +
+
+ + + +
+ + + + + + + + + + + + +
+
+
+
+
From 9c406454d3c84d54771e64c88cddbc0ac912e381 Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Fri, 11 Dec 2020 12:47:59 +0530 Subject: [PATCH 005/111] add employee unit hours in project overview screen --- cor_custom/__manifest__.py | 1 + cor_custom/models/__init__.py | 3 +- .../__pycache__/__init__.cpython-36.pyc | Bin 213 -> 252 bytes .../project_overview.cpython-36.pyc | Bin 0 -> 19446 bytes cor_custom/models/project_overview.py | 564 ++++++++++++++++++ cor_custom/views/hr_timesheet_templates.xml | 469 +++++++++++++++ cor_custom/views/project_view.xml | 2 +- 7 files changed, 1037 insertions(+), 2 deletions(-) create mode 100644 cor_custom/models/__pycache__/project_overview.cpython-36.pyc create mode 100755 cor_custom/models/project_overview.py create mode 100755 cor_custom/views/hr_timesheet_templates.xml diff --git a/cor_custom/__manifest__.py b/cor_custom/__manifest__.py index 2e73341..dc068eb 100644 --- a/cor_custom/__manifest__.py +++ b/cor_custom/__manifest__.py @@ -26,6 +26,7 @@ 'data': [ # 'security/ir.model.access.csv', 'views/project_view.xml', + 'views/hr_timesheet_templates.xml', 'views/views.xml', 'views/templates.xml', ], diff --git a/cor_custom/models/__init__.py b/cor_custom/models/__init__.py index a0c0dd3..6495c5e 100644 --- a/cor_custom/models/__init__.py +++ b/cor_custom/models/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- from . import models -from . import project \ No newline at end of file +from . import project +from . import project_overview \ No newline at end of file diff --git a/cor_custom/models/__pycache__/__init__.cpython-36.pyc b/cor_custom/models/__pycache__/__init__.cpython-36.pyc index cdcfb0db09223f6b949dc3701e4d87fe649d7fd9..f0c84adacb3bde1266e44924eebcce52f623c443 100644 GIT binary patch delta 173 zcmcc0_=mCHn3tF9ufXN_BnAeC$3P4ROhA?c5Ep9zi4=wu#vF!R#wbQc5St0eW{P40 zvzdWx<|t+$nJJAd9`AC_gJTxdg}*fHLFr%TkNVGE>X_ kWHecBF%*I9Dq;o^EI?u~b!^p!301(q8L;wH) delta 111 zcmeyvc$Lw`n3tDp>Yj`7&I}9;kAWBtn1Cz?ATE{x5-AKRj5!Rsj8Tk?AT|?_%@oB1 pWHSddXtGQUkdpAzWWL2v1kzN*3?x=E6tRNXtP^KPvVlZ-7y(dS5>NmD diff --git a/cor_custom/models/__pycache__/project_overview.cpython-36.pyc b/cor_custom/models/__pycache__/project_overview.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4a5e6486d3ba3b49b688fa8cf33906ee2a060285 GIT binary patch literal 19446 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', 0), + ('project_id', '=', False)], ['amount'], [])[0].get('amount', 0) + + # profitability, using profitability SQL report + profit = dict.fromkeys(['invoiced', 'to_invoice', 'cost', 'expense_cost', 'expense_amount_untaxed_invoiced', 'total'], 0.0) + profitability_raw_data = self.env['project.profitability.report'].read_group([('project_id', 'in', self.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_invoiced'], ['project_id']) + for data in profitability_raw_data: + profit['invoiced'] += data.get('amount_untaxed_invoiced', 0.0) + profit['to_invoice'] += data.get('amount_untaxed_to_invoice', 0.0) + profit['cost'] += data.get('timesheet_cost', 0.0) + profit['expense_cost'] += data.get('expense_cost', 0.0) + profit['expense_amount_untaxed_invoiced'] += data.get('expense_amount_untaxed_invoiced', 0.0) + profit['other_revenues'] = other_revenues or 0 + profit['total'] = sum([profit[item] for item in profit.keys()]) + dashboard_values['profit'] = profit + + values['dashboard'] = dashboard_values + + # + # Time Repartition (per employee per billable types) + # + user_ids = self.env['project.task'].sudo().read_group([('project_id', 'in', self.ids), ('user_id', '!=', False)], ['user_id'], ['user_id']) + user_ids = [user_id['user_id'][0] for user_id in user_ids] + employee_ids = self.env['res.users'].sudo().search_read([('id', 'in', user_ids)], ['employee_ids']) + # flatten the list of list + employee_ids = list(itertools.chain.from_iterable([employee_id['employee_ids'] for employee_id in employee_ids])) + + aal_employee_ids = self.env['account.analytic.line'].read_group([('project_id', 'in', self.ids), ('employee_id', '!=', False)], ['employee_id'], ['employee_id']) + employee_ids.extend(list(map(lambda x: x['employee_id'][0], aal_employee_ids))) + + # Retrieve the employees for which the current user can see theirs timesheets + employee_domain = expression.AND([[('company_id', 'in', self.env.companies.ids)], self.env['account.analytic.line']._domain_employee_id()]) + employees = self.env['hr.employee'].sudo().browse(employee_ids).filtered_domain(employee_domain) + repartition_domain = [('project_id', 'in', self.ids), ('employee_id', '!=', False), ('timesheet_invoice_type', '!=', False)] # force billable type + # repartition data, without timesheet on cancelled so + repartition_data = self.env['account.analytic.line'].read_group(repartition_domain + ['|', ('so_line', '=', False), ('so_line.state', '!=', 'cancel')], ['employee_id', 'timesheet_invoice_type', 'unit_amount'], ['employee_id', 'timesheet_invoice_type'], lazy=False) + # read timesheet on cancelled so + cancelled_so_timesheet = self.env['account.analytic.line'].read_group(repartition_domain + [('so_line.state', '=', 'cancel')], ['employee_id', 'unit_amount'], ['employee_id'], lazy=False) + repartition_data += [{**canceled, 'timesheet_invoice_type': 'canceled'} for canceled in cancelled_so_timesheet] + + # set repartition per type per employee + repartition_employee = {} + for employee in employees: + repartition_employee[employee.id] = dict( + employee_id=employee.id, + employee_name=employee.name, + employee_price=employee.timesheet_cost, + non_billable_project=0.0, + non_billable=0.0, + billable_time=0.0, + non_billable_timesheet=0.0, + billable_fixed=0.0, + canceled=0.0, + total=0.0, + ) + for data in repartition_data: + employee_id = data['employee_id'][0] + repartition_employee.setdefault(employee_id, dict( + employee_id=data['employee_id'][0], + employee_name=data['employee_id'][1], + employee_price=data['employee_id'][1], + non_billable_project=0.0, + non_billable=0.0, + billable_time=0.0, + non_billable_timesheet=0.0, + billable_fixed=0.0, + canceled=0.0, + total=0.0, + ))[data['timesheet_invoice_type']] = float_round(data.get('unit_amount', 0.0), precision_rounding=hour_rounding) + repartition_employee[employee_id]['__domain_' + data['timesheet_invoice_type']] = data['__domain'] + # compute total + for employee_id, vals in repartition_employee.items(): + repartition_employee[employee_id]['total'] = sum([vals[inv_type] for inv_type in [*billable_types, 'canceled']]) + if is_uom_day: + # convert all times from hours to days + for time_type in ['non_billable_project', 'non_billable', 'billable_time', 'non_billable_timesheet', 'billable_fixed', 'canceled', 'total']: + if repartition_employee[employee_id][time_type]: + repartition_employee[employee_id][time_type] = round(uom_hour._compute_quantity(repartition_employee[employee_id][time_type], company_uom, raise_if_failure=False), 2) + hours_per_employee = [repartition_employee[employee_id]['total'] for employee_id in repartition_employee] + values['repartition_employee_max'] = (max(hours_per_employee) if hours_per_employee else 1) or 1 + values['repartition_employee'] = repartition_employee + + # + # Table grouped by SO / SOL / Employees + # + timesheet_forecast_table_rows = self._table_get_line_values(employees) + if timesheet_forecast_table_rows: + values['timesheet_forecast_table'] = timesheet_forecast_table_rows + return values + + def _table_get_line_values(self, employees=None): + """ return the header and the rows informations of the table """ + if not self: + return False + + uom_hour = self.env.ref('uom.product_uom_hour') + company_uom = self.env.company.timesheet_encode_uom_id + is_uom_day = company_uom and company_uom == self.env.ref('uom.product_uom_day') + + # build SQL query and fetch raw data + query, query_params = self._table_rows_sql_query() + self.env.cr.execute(query, query_params) + raw_data = self.env.cr.dictfetchall() + rows_employee = self._table_rows_get_employee_lines(raw_data) + default_row_vals = self._table_row_default() + + empty_line_ids, empty_order_ids = self._table_get_empty_so_lines() + + # extract row labels + sale_line_ids = set() + sale_order_ids = set() + for key_tuple, row in rows_employee.items(): + if row[0]['sale_line_id']: + sale_line_ids.add(row[0]['sale_line_id']) + if row[0]['sale_order_id']: + sale_order_ids.add(row[0]['sale_order_id']) + + sale_orders = self.env['sale.order'].sudo().browse(sale_order_ids | empty_order_ids) + sale_order_lines = self.env['sale.order.line'].sudo().browse(sale_line_ids | empty_line_ids) + map_so_names = {so.id: so.name for so in sale_orders} + map_so_cancel = {so.id: so.state == 'cancel' for so in sale_orders} + map_sol = {sol.id: sol for sol in sale_order_lines} + map_sol_names = {sol.id: sol.name.split('\n')[0] if sol.name else _('No Sales Order Line') for sol in sale_order_lines} + map_sol_so = {sol.id: sol.order_id.id for sol in sale_order_lines} + + rows_sale_line = {} # (so, sol) -> [INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted] + for sale_line_id in empty_line_ids: # add service SO line having no timesheet + sale_line_row_key = (map_sol_so.get(sale_line_id), sale_line_id) + sale_line = map_sol.get(sale_line_id) + is_milestone = sale_line.product_id.invoice_policy == 'delivery' and sale_line.product_id.service_type == 'manual' if sale_line else False + rows_sale_line[sale_line_row_key] = [{'label': map_sol_names.get(sale_line_id, _('No Sales Order Line')), 'res_id': sale_line_id, 'res_model': 'sale.order.line', 'type': 'sale_order_line', 'is_milestone': is_milestone}] + default_row_vals[:] + if not is_milestone: + rows_sale_line[sale_line_row_key][-2] = sale_line.product_uom._compute_quantity(sale_line.product_uom_qty, uom_hour, raise_if_failure=False) if sale_line else 0.0 + + rows_sale_line_all_data = {} + if not employees: + employees = self.env['hr.employee'].sudo().search(self.env['account.analytic.line']._domain_employee_id()) + for row_key, row_employee in rows_employee.items(): + sale_order_id, sale_line_id, employee_id = row_key + # sale line row + sale_line_row_key = (sale_order_id, sale_line_id) + if sale_line_row_key not in rows_sale_line: + sale_line = map_sol.get(sale_line_id, self.env['sale.order.line']) + is_milestone = sale_line.product_id.invoice_policy == 'delivery' and sale_line.product_id.service_type == 'manual' if sale_line else False + rows_sale_line[sale_line_row_key] = [{'label': map_sol_names.get(sale_line.id) if sale_line else _('No Sales Order Line'), 'res_id': sale_line_id, 'res_model': 'sale.order.line', 'type': 'sale_order_line', 'is_milestone': is_milestone}] + default_row_vals[:] # INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted + if not is_milestone: + rows_sale_line[sale_line_row_key][-2] = sale_line.product_uom._compute_quantity(sale_line.product_uom_qty, uom_hour, raise_if_failure=False) if sale_line else 0.0 + + if sale_line_row_key not in rows_sale_line_all_data: + rows_sale_line_all_data[sale_line_row_key] = [0] * len(row_employee) + for index in range(1, len(row_employee)): + if employee_id in employees.ids: + rows_sale_line[sale_line_row_key][index] += row_employee[index] + rows_sale_line_all_data[sale_line_row_key][index] += row_employee[index] + if not rows_sale_line[sale_line_row_key][0].get('is_milestone'): + rows_sale_line[sale_line_row_key][-1] = rows_sale_line[sale_line_row_key][-2] - rows_sale_line_all_data[sale_line_row_key][5] + else: + rows_sale_line[sale_line_row_key][-1] = 0 + + rows_sale_order = {} # so -> [INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted] + for row_key, row_sale_line in rows_sale_line.items(): + sale_order_id = row_key[0] + # sale order row + if sale_order_id not in rows_sale_order: + rows_sale_order[sale_order_id] = [{'label': map_so_names.get(sale_order_id, _('No Sales Order')), 'canceled': map_so_cancel.get(sale_order_id, False), 'res_id': sale_order_id, 'res_model': 'sale.order', 'type': 'sale_order'}] + default_row_vals[:] # INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted + + for index in range(1, len(row_sale_line)): + rows_sale_order[sale_order_id][index] += row_sale_line[index] + + # group rows SO, SOL and their related employee rows. + timesheet_forecast_table_rows = [] + for sale_order_id, sale_order_row in rows_sale_order.items(): + timesheet_forecast_table_rows.append(sale_order_row) + for sale_line_row_key, sale_line_row in rows_sale_line.items(): + if sale_order_id == sale_line_row_key[0]: + sale_order_row[0]['has_children'] = True + timesheet_forecast_table_rows.append(sale_line_row) + for employee_row_key, employee_row in rows_employee.items(): + if sale_order_id == employee_row_key[0] and sale_line_row_key[1] == employee_row_key[1] and employee_row_key[2] in employees.ids: + sale_line_row[0]['has_children'] = True + timesheet_forecast_table_rows.append(employee_row) + + if is_uom_day: + # convert all values from hours to days + for row in timesheet_forecast_table_rows: + for index in range(1, len(row)): + row[index] = round(uom_hour._compute_quantity(row[index], company_uom, raise_if_failure=False), 2) + # complete table data + return { + 'header': self._table_header(), + 'rows': timesheet_forecast_table_rows + } + def _table_header(self): + initial_date = fields.Date.from_string(fields.Date.today()) + ts_months = sorted([fields.Date.to_string(initial_date - relativedelta(months=i, day=1)) for i in range(0, DEFAULT_MONTH_RANGE)]) # M1, M2, M3 + + def _to_short_month_name(date): + month_index = fields.Date.from_string(date).month + return babel.dates.get_month_names('abbreviated', locale=get_lang(self.env).code)[month_index] + + header_names = [_('Sales Order'), _('Before')] + [_to_short_month_name(date) for date in ts_months] + [_('Total'), _('Sold'), _('Remaining')] + + result = [] + for name in header_names: + result.append({ + 'label': name, + 'tooltip': '', + }) + # add tooltip for reminaing + result[-1]['tooltip'] = _('What is still to deliver based on sold hours and hours already done. Equals to sold hours - done hours.') + return result + + def _table_row_default(self): + lenght = len(self._table_header()) + return [0.0] * (lenght - 1) # before, M1, M2, M3, Done, Sold, Remaining + + def _table_rows_sql_query(self): + initial_date = fields.Date.from_string(fields.Date.today()) + ts_months = sorted([fields.Date.to_string(initial_date - relativedelta(months=i, day=1)) for i in range(0, DEFAULT_MONTH_RANGE)]) # M1, M2, M3 + # build query + query = """ + SELECT + 'timesheet' AS type, + date_trunc('month', date)::date AS month_date, + E.id AS employee_id, + S.order_id AS sale_order_id, + A.so_line AS sale_line_id, + SUM(A.unit_amount) AS number_hours + FROM account_analytic_line A + JOIN hr_employee E ON E.id = A.employee_id + LEFT JOIN sale_order_line S ON S.id = A.so_line + WHERE A.project_id IS NOT NULL + AND A.project_id IN %s + AND A.date < %s + GROUP BY date_trunc('month', date)::date, S.order_id, A.so_line, E.id + """ + + last_ts_month = fields.Date.to_string(fields.Date.from_string(ts_months[-1]) + relativedelta(months=1)) + query_params = (tuple(self.ids), last_ts_month) + return query, query_params + + def _table_rows_get_employee_lines(self, data_from_db): + initial_date = fields.Date.today() + ts_months = sorted([initial_date - relativedelta(months=i, day=1) for i in range(0, DEFAULT_MONTH_RANGE)]) # M1, M2, M3 + default_row_vals = self._table_row_default() + + # extract employee names + employee_ids = set() + for data in data_from_db: + employee_ids.add(data['employee_id']) + map_empl_names = {empl.id: empl.name for empl in self.env['hr.employee'].sudo().browse(employee_ids)} + + # extract rows data for employee, sol and so rows + rows_employee = {} # (so, sol, employee) -> [INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted] + for data in data_from_db: + sale_line_id = data['sale_line_id'] + sale_order_id = data['sale_order_id'] + # employee row + row_key = (data['sale_order_id'], sale_line_id, data['employee_id']) + if row_key not in rows_employee: + meta_vals = { + 'label': map_empl_names.get(row_key[2]), + 'sale_line_id': sale_line_id, + 'sale_order_id': sale_order_id, + 'res_id': row_key[2], + 'res_model': 'hr.employee', + 'type': 'hr_employee' + } + rows_employee[row_key] = [meta_vals] + default_row_vals[:] # INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted + + index = False + if data['type'] == 'timesheet': + if data['month_date'] in ts_months: + index = ts_months.index(data['month_date']) + 2 + elif data['month_date'] < ts_months[0]: + index = 1 + rows_employee[row_key][index] += data['number_hours'] + rows_employee[row_key][5] += data['number_hours'] + return rows_employee + + def _table_get_empty_so_lines(self): + """ get the Sale Order Lines having no timesheet but having generated a task or a project """ + so_lines = self.sudo().mapped('sale_line_id.order_id.order_line').filtered(lambda sol: sol.is_service and not sol.is_expense and not sol.is_downpayment) + # include the service SO line of SO sharing the same project + sale_order = self.env['sale.order'].search([('project_id', 'in', self.ids)]) + return set(so_lines.ids) | set(sale_order.mapped('order_line').filtered(lambda sol: sol.is_service and not sol.is_expense).ids), set(so_lines.mapped('order_id').ids) | set(sale_order.ids) + + # -------------------------------------------------- + # Actions: Stat buttons, ... + # -------------------------------------------------- + + def _plan_prepare_actions(self, values): + actions = [] + if len(self) == 1: + task_order_line_ids = [] + # retrieve all the sale order line that we will need later below + if self.env.user.has_group('sales_team.group_sale_salesman') or self.env.user.has_group('sales_team.group_sale_salesman_all_leads'): + task_order_line_ids = self.env['project.task'].read_group([('project_id', '=', self.id), ('sale_line_id', '!=', False)], ['sale_line_id'], ['sale_line_id']) + task_order_line_ids = [ol['sale_line_id'][0] for ol in task_order_line_ids] + + if self.env.user.has_group('sales_team.group_sale_salesman'): + if self.bill_type == 'customer_project' and self.allow_billable and not self.sale_order_id: + actions.append({ + 'label': _("Create a Sales Order"), + 'type': 'action', + 'action_id': 'sale_timesheet.project_project_action_multi_create_sale_order', + 'context': json.dumps({'active_id': self.id, 'active_model': 'project.project'}), + }) + if self.env.user.has_group('sales_team.group_sale_salesman_all_leads'): + to_invoice_amount = values['dashboard']['profit'].get('to_invoice', False) # plan project only takes services SO line with timesheet into account + + sale_order_ids = self.env['sale.order.line'].read_group([('id', 'in', task_order_line_ids)], ['order_id'], ['order_id']) + sale_order_ids = [s['order_id'][0] for s in sale_order_ids] + sale_order_ids = self.env['sale.order'].search_read([('id', 'in', sale_order_ids), ('invoice_status', '=', 'to invoice')], ['id']) + sale_order_ids = list(map(lambda x: x['id'], sale_order_ids)) + + if to_invoice_amount and sale_order_ids: + if len(sale_order_ids) == 1: + actions.append({ + 'label': _("Create Invoice"), + 'type': 'action', + 'action_id': 'sale.action_view_sale_advance_payment_inv', + 'context': json.dumps({'active_ids': sale_order_ids, 'active_model': 'project.project'}), + }) + else: + actions.append({ + 'label': _("Create Invoice"), + 'type': 'action', + 'action_id': 'sale_timesheet.project_project_action_multi_create_invoice', + 'context': json.dumps({'active_id': self.id, 'active_model': 'project.project'}), + }) + return actions + + def _plan_get_stat_button(self): + stat_buttons = [] + num_projects = len(self) + if num_projects == 1: + action_data = _to_action_data('project.project', res_id=self.id, + views=[[self.env.ref('project.edit_project').id, 'form']]) + else: + action_data = _to_action_data(action=self.env.ref('project.open_view_project_all_config').sudo(), + domain=[('id', 'in', self.ids)]) + + stat_buttons.append({ + 'name': _('Project') if num_projects == 1 else _('Projects'), + 'count': num_projects, + 'icon': 'fa fa-puzzle-piece', + 'action': action_data + }) + + # if only one project, add it in the context as default value + tasks_domain = [('project_id', 'in', self.ids)] + tasks_context = self.env.context.copy() + tasks_context.pop('search_default_name', False) + late_tasks_domain = [('project_id', 'in', self.ids), ('date_deadline', '<', fields.Date.to_string(fields.Date.today())), ('date_end', '=', False)] + overtime_tasks_domain = [('project_id', 'in', self.ids), ('overtime', '>', 0), ('planned_hours', '>', 0)] + + # filter out all the projects that have no tasks + task_projects_ids = self.env['project.task'].read_group([('project_id', 'in', self.ids)], ['project_id'], ['project_id']) + task_projects_ids = [p['project_id'][0] for p in task_projects_ids] + + if len(task_projects_ids) == 1: + tasks_context = {**tasks_context, 'default_project_id': task_projects_ids[0]} + stat_buttons.append({ + 'name': _('Tasks'), + 'count': sum(self.mapped('task_count')), + 'icon': 'fa fa-tasks', + 'action': _to_action_data( + action=self.env.ref('project.action_view_task').sudo(), + domain=tasks_domain, + context=tasks_context + ) + }) + stat_buttons.append({ + 'name': [_("Tasks"), _("Late")], + 'count': self.env['project.task'].search_count(late_tasks_domain), + 'icon': 'fa fa-tasks', + 'action': _to_action_data( + action=self.env.ref('project.action_view_task').sudo(), + domain=late_tasks_domain, + context=tasks_context, + ), + }) + stat_buttons.append({ + 'name': [_("Tasks"), _("in Overtime")], + 'count': self.env['project.task'].search_count(overtime_tasks_domain), + 'icon': 'fa fa-tasks', + 'action': _to_action_data( + action=self.env.ref('project.action_view_task').sudo(), + domain=overtime_tasks_domain, + context=tasks_context, + ), + }) + + if self.env.user.has_group('sales_team.group_sale_salesman_all_leads'): + # read all the sale orders linked to the projects' tasks + task_so_ids = self.env['project.task'].search_read([ + ('project_id', 'in', self.ids), ('sale_order_id', '!=', False) + ], ['sale_order_id']) + task_so_ids = [o['sale_order_id'][0] for o in task_so_ids] + + sale_orders = self.mapped('sale_line_id.order_id') | self.env['sale.order'].browse(task_so_ids) + if sale_orders: + stat_buttons.append({ + 'name': _('Sales Orders'), + 'count': len(sale_orders), + 'icon': 'fa fa-dollar', + 'action': _to_action_data( + action=self.env.ref('sale.action_orders').sudo(), + domain=[('id', 'in', sale_orders.ids)], + context={'create': False, 'edit': False, 'delete': False} + ) + }) + + invoice_ids = self.env['sale.order'].search_read([('id', 'in', sale_orders.ids)], ['invoice_ids']) + invoice_ids = list(itertools.chain(*[i['invoice_ids'] for i in invoice_ids])) + invoice_ids = self.env['account.move'].search_read([('id', 'in', invoice_ids), ('move_type', '=', 'out_invoice')], ['id']) + invoice_ids = list(map(lambda x: x['id'], invoice_ids)) + + if invoice_ids: + stat_buttons.append({ + 'name': _('Invoices'), + 'count': len(invoice_ids), + 'icon': 'fa fa-pencil-square-o', + 'action': _to_action_data( + action=self.env.ref('account.action_move_out_invoice_type').sudo(), + domain=[('id', 'in', invoice_ids), ('move_type', '=', 'out_invoice')], + context={'create': False, 'delete': False} + ) + }) + + ts_tree = self.env.ref('hr_timesheet.hr_timesheet_line_tree') + ts_form = self.env.ref('hr_timesheet.hr_timesheet_line_form') + if self.env.company.timesheet_encode_uom_id == self.env.ref('uom.product_uom_day'): + timesheet_label = [_('Days'), _('Recorded')] + else: + timesheet_label = [_('Hours'), _('Recorded')] + + stat_buttons.append({ + 'name': timesheet_label, + 'count': sum(self.mapped('total_timesheet_time')), + 'icon': 'fa fa-calendar', + 'action': _to_action_data( + 'account.analytic.line', + domain=[('project_id', 'in', self.ids)], + views=[(ts_tree.id, 'list'), (ts_form.id, 'form')], + ) + }) + + return stat_buttons + + +def _to_action_data(model=None, *, action=None, views=None, res_id=None, domain=None, context=None): + # pass in either action or (model, views) + if action: + assert model is None and views is None + act = clean_action(action.read()[0], env=action.env) + model = act['res_model'] + views = act['views'] + # FIXME: search-view-id, possibly help? + descr = { + 'data-model': model, + 'data-views': json.dumps(views), + } + if context is not None: # otherwise copy action's? + descr['data-context'] = json.dumps(context) + if res_id: + descr['data-res-id'] = res_id + elif domain: + descr['data-domain'] = json.dumps(domain) + return descr diff --git a/cor_custom/views/hr_timesheet_templates.xml b/cor_custom/views/hr_timesheet_templates.xml new file mode 100755 index 0000000..8ecb7e3 --- /dev/null +++ b/cor_custom/views/hr_timesheet_templates.xml @@ -0,0 +1,469 @@ + + + + + Timesheet Plan + qweb + project.project + + + +
+
+
+
+
+ + + +
+

Recorded Days and Profitability

+

Recorded Hours and Profitability

+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Days recorded + Hours recorded + +
+ + + + ( %) + + Billed on Timesheets +
+ + + + ( %) + + Billed at a Fixed price +
+ + + + ( %) + + No task found +
+ + + + ( %) + + + + Non Billable Tasks + + +
+ + + + ( %) + + + + Non Billable Timesheets + + +
+ + + + ( %) + + Cancelled +
+ + + + + Total
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Profitability +
+ + + Invoiced +
+ + + To invoice +
+ + Other Revenues
+ + + Timesheet costs +
+ + + Other costs +
+ + + Re-invoiced costs +
+ + + + Total
+
+
+
+
+ +
+

Time by people

+
+ +
+ +

There are no timesheets for now.

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
EmployeeUnit PriceDays SpentHours Spent + +
+ + + + + + + + + + + + + +
+
+ + + + Billed on Timesheets + billable_time + + + Billed at a Fixed price + billable_fixed + + + No task found + non_billable_project + + + Non billable timesheets + non_billable_timesheet + + + Non billable tasks + non_billable + + + Cancelled + canceled + +
+
+
+
+
+
+ +
+

Timesheets

+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Timesheets

+ +
+ + + + + + + + Cancelled + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+
+
+
+
+ +
diff --git a/cor_custom/views/project_view.xml b/cor_custom/views/project_view.xml index ef4c630..9133928 100755 --- a/cor_custom/views/project_view.xml +++ b/cor_custom/views/project_view.xml @@ -29,7 +29,7 @@ - + From 67d617318c213f72860abcb79179016b1a58bb3e Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Fri, 11 Dec 2020 17:10:40 +0530 Subject: [PATCH 006/111] change unit price to hourly rate --- cor_custom/views/hr_timesheet_templates.xml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cor_custom/views/hr_timesheet_templates.xml b/cor_custom/views/hr_timesheet_templates.xml index 8ecb7e3..f02a7d7 100755 --- a/cor_custom/views/hr_timesheet_templates.xml +++ b/cor_custom/views/hr_timesheet_templates.xml @@ -263,7 +263,7 @@ Employee - Unit Price + Hourly Rate Days Spent Hours Spent @@ -306,9 +306,7 @@ - - From 855346a8e5f99c1cb85566f98775765474b35406 Mon Sep 17 00:00:00 2001 From: projectsodoo Date: Mon, 14 Dec 2020 14:27:45 +0530 Subject: [PATCH 007/111] Crm lead lead Id field added --- cor_custom/__manifest__.py | 3 ++- cor_custom/__pycache__/__init__.cpython-36.pyc | Bin 210 -> 0 bytes .../__pycache__/__init__.cpython-36.pyc | Bin 193 -> 0 bytes .../__pycache__/controllers.cpython-36.pyc | Bin 160 -> 0 bytes cor_custom/models/__init__.py | 1 + .../models/__pycache__/__init__.cpython-36.pyc | Bin 252 -> 0 bytes .../models/__pycache__/models.cpython-36.pyc | Bin 150 -> 0 bytes .../models/__pycache__/project.cpython-36.pyc | Bin 787 -> 0 bytes .../__pycache__/project_overview.cpython-36.pyc | Bin 19446 -> 0 bytes cor_custom/models/crm_lead.py | 10 ++++++++++ cor_custom/views/crm_view.xml | 15 +++++++++++++++ 11 files changed, 28 insertions(+), 1 deletion(-) delete mode 100644 cor_custom/__pycache__/__init__.cpython-36.pyc delete mode 100644 cor_custom/controllers/__pycache__/__init__.cpython-36.pyc delete mode 100644 cor_custom/controllers/__pycache__/controllers.cpython-36.pyc delete mode 100644 cor_custom/models/__pycache__/__init__.cpython-36.pyc delete mode 100644 cor_custom/models/__pycache__/models.cpython-36.pyc delete mode 100644 cor_custom/models/__pycache__/project.cpython-36.pyc delete mode 100644 cor_custom/models/__pycache__/project_overview.cpython-36.pyc create mode 100644 cor_custom/models/crm_lead.py create mode 100755 cor_custom/views/crm_view.xml diff --git a/cor_custom/__manifest__.py b/cor_custom/__manifest__.py index dc068eb..48ec7f2 100644 --- a/cor_custom/__manifest__.py +++ b/cor_custom/__manifest__.py @@ -20,11 +20,12 @@ 'version': '0.1', # any module necessary for this one to work correctly - 'depends': ['base', 'sale_timesheet'], + 'depends': ['base', 'crm', 'sale_timesheet'], # always loaded 'data': [ # 'security/ir.model.access.csv', + 'views/crm_view.xml', 'views/project_view.xml', 'views/hr_timesheet_templates.xml', 'views/views.xml', diff --git a/cor_custom/__pycache__/__init__.cpython-36.pyc b/cor_custom/__pycache__/__init__.cpython-36.pyc deleted file mode 100644 index a2ca7f5dad7e0aa4741977cd979df0d8af3279e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 210 zcmXr!<>lIZ>O#Cb0|UcjAcg}bAj<)Wi)DaB3PTEG4nr-mKMKJ-{%)tzr zEH4>>N;Da7aVO{Jl@#UYlIZ>O#B`0|UcjAcg}bAj<)Wiv@s03PTEG4nr1ps*(6 zE$-y}ypp2)oSf96Vn0o$TMR`YjVl?7Sb!9m_~oUaUr?f-pOT+%Xac5mlS_+B@^f_) zQ&RHtiuIH8i*!MlIZ>O%Z$CI*Jb3`l?x$aVnYViq8g!Vt`$$>_I|p$H_5Ab$Dk=NFXd=cnZ7 z8=8PA-Q?2ZlKfoV#FUi$ykh<2{32bDEQk>gkpeRFN{aGxa#D*x@^H3ZLFFwDo80`A O(wtN~kTu0X%m4uV7%UI~ diff --git a/cor_custom/models/__init__.py b/cor_custom/models/__init__.py index 6495c5e..284dfb7 100644 --- a/cor_custom/models/__init__.py +++ b/cor_custom/models/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from . import crm_lead from . import models from . import project from . import project_overview \ No newline at end of file diff --git a/cor_custom/models/__pycache__/__init__.cpython-36.pyc b/cor_custom/models/__pycache__/__init__.cpython-36.pyc deleted file mode 100644 index f0c84adacb3bde1266e44924eebcce52f623c443..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 252 zcmX}lzY4-I5C-t1t$&af`WSYoL39*xadmO)612%sX?rDUD?XCXlIZ>O%YjMh1q*3`hXTXK(=GViq8g!Vt`$$>_I|p$H_5AbvUP=NFXd=cnZ7 z8=8PA-Q?2ZlKfoV#FUi$ykh<2{32bDEQk>gk8S%5Z)i#=Unctph|&I+LJh22pS}WB84AOi|C1|&$;uWZv@x|yU5F^cx_nV#hJUh?l^XccW-_wN26XK_R|UbN zNDR(U-BUE0WgJ*0fM-$%RS06ovq(>>803ya0O38ulOu?*&BUL6XgAuH{iQXPF37>Q z=-)P7ZEm!_$h+Go-5yF@UhAYTTb<|!IB5`HjLD~$wk)(i9#K_7fioF|yg)rib&t{9 zv5MJ8WGjBhlxQOuJTRms*4LOa$Wqz~27b|(jdmNYdsP_M`^BFw9d%uDA2?lKePQ+( z(|^55P1mOejCuBAaI0eLdef|O0Moj(Fm^>K1!@na`{AbJ?{+u-M47hO=;xlpk@_$oI{#9wcocdU)HgB|2eyS9**wz%!l=`&I>jxxO<^G{8y|xeE)h6%l z5yqD0>f@2Aozoc+JTNo*B)+5r@t6lrb&*lIC diff --git a/cor_custom/models/__pycache__/project_overview.cpython-36.pyc b/cor_custom/models/__pycache__/project_overview.cpython-36.pyc deleted file mode 100644 index 4a5e6486d3ba3b49b688fa8cf33906ee2a060285..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19446 zcmb_^eQ;b?me+f)Kh)}OwWQX!tjKM}v8}{bGRaJmaU5@)#7;7s$l2H#&x&Nb+PY72 zw_DxveNT?uyxv(xhRIZIrpy9MHW+IEm>pQ4hAnDBDySlIa%x1C@E%s9Y7FnZ z8dqh!3u;15;$2izY8vm6Z(C_=t%UK;hC^@acEerwJEPksvJ$=5W0*1kiE%#Ovj}`^ z&uZ9#9XNYVHx*b72Qj;w4pNO&kZz>+tUcSd5X%IqJv&G#`&=?yRja>gJ$U2s_O}0c zTeaI?`m(&vtnYY!d+SV1sdmeIe7&vDFqe_4$$H$7!{bqt)%I;yv)`5NJGumXO%ZW< z1eRTL@^%SfXYtJl+lP;S-DYw8E#Vo%Bgo&*S{Bd^u|me0vx34iK+8ES^LQlc zb+w@0P^%bYG3j$GK!0j<)4^l!rMe@2UM;GlcdcOLWe_R|G(Tf?OS|9a_q%_kI~t6t z(K)L-2Ixi`NzP<2q@Kc?nENuuI|Miv39Qwk8zGi~y#yna$qRy%3 zLCrIJSn*`M{zBs+bzbJzYAl)+p0sy#&)Oujo|TlJiD0V)pz_ACU=}%ZeLY@LSFwWU zliFj!JXYA->g$^+@Pp^D%}j%G@CIb#>V*%|-6I2KuL<9H4y6p_rp!WcL|u>(FL1=q zSluIa>pS-D)4?3*WTpF%p9>ypEa7<=&+&xE7*6xz2l@AH=AX0frEmTFYZmD1h@mg6 z+~=?=K*8O`c=eJB%sP1AZafk!qV1$q6kEcYGiit&><#sWBY+A<)k||0dS67jmrd#n zIMBV!ty#eFE%b1LboB1+cbx9xcO1}|gYEkwX$)NS8-KxxXw0+#Is-?^=A+87)m#TV@Q7u=|kAG*H$}|JMArs1!`yAuOeQ( z(caOWiT$anb|);h+O6vKdb3%(-o&2W|AnnvB#m$+&ae9QEjOGVD8wZ1hU@xaDJiy5 z-*r`(kM~z>=T5z~oYgbf7{lE9j@E8#{Z2TR)U8Il{UoYv)cqP-s{421)S~A@O1r+} z``FOKeBF~V5&DeZ_G`^eG%J70=`dGs-EP;{*}dPcMjv5%z3ut>Bnm+=Ov@-T8bXQ} zW=)UbbPtxOQq{k+?LKghJ%8!Jh}W(*>n(SMfVdCr=fmuJt+nnpJ5#mwbx4o?O089E z-tp_}D=Zflc3O46TH9h41eP*{rS&?6P9tm9MZ_G_wYuk4>l@XLTD`fW-H#mrs2@WB zBuV?OdSD0PP}F@QWUyQs&lNo0B!W#aymyNbF(9MQS%22Kl?EZ$`YB}TIRwj2=&U>u zrXiTsB*6sp$a;`J*Q|T~dV6d8{MYd9vGJ_cdE(7j;5kH$TRGQkuh*L1c|_vMA1hSU zrxAQiVJOT;s1a$-pyGjZ{&~$zeTu;{gGX7m2(jF40U4a9@M!#HYE^;68VD+$P1RY5 zD2p`SBbOCOq;2hobNj0G(ZS5Vtb|xfz3CSaN^Sp!tE<|*?Y1Dc9>DwrpdP?HHXu93 zd=JCRjy}urrR%=Cg$X&s5pzN`QLoP-xQQpixnzgKs^;Ce-mYoYDaPaUYu?RpXvYIc zb#(w3A%j`8BV;fZlYvSA0vMcHgL*>`0+&e@cm{#;dq8!Dp)aklYY(efVXC&ao9#O; z2v;4%%jZ$#>ja|@g0RrnTn}$O^t{)DM8AP}GI){^NmY6*A`(DL1)F!~khGjymC}79 zwnKY&Uk73CT(h=yUDeKaP~XF2bymJ8*_(=Co(reex&v%<3ODphJRGe0bhFmElfX+@ zshJ%*S<0^4zv@Km*3l~{r=MZahpJwJ8Ad`ucTq5+pyj*_I~<9@sJ3e0*rnuiTZ1?1 z1vY(z!9xrd860IGeUeRvxoXUPp=}xNTpbt-`OgHNpI{Y`Hw#xKa-i3F`v&rTHfPw+ z;a&C9xLI%m?2dpBV2lKVz9vOg_3KpSt1&9}_5C+{eTTD$m9K8qcJ((~ry<0y3rA5Y+|zr^541p9Np8s{cOSuZa70^=7LkkC=L!Z;gaaaqlA zHyout^h4+<&iU;A_Q7fkDJ&0t0a96B4pVOHb~qHRgs>2A@Z|g{?QVoKJ&a-Fhb=}n zaOCF1^n&d$Oo5xk#sQjP`nG&*TlIB6%x&oQ)=l@07pAG;dWD+3ULfp67<68}%ld!aiq~oBmlgk>&!{ml(}^uCLsU+D_9CGu%*s2@oX=GD3Y- zv0g#?s2Y>W@{;EI)Jv?Ji={x!PQWEha@Gs}5#{rG4Lh%|()5sn-F8lyIu|ZCWIdo_e#aYKeZ4>LzNVOW-NRWXbNmhhpZVs((gV3OW5OcOjnWC=1PeT{)^2jhs5n|uEh zfo13IBAzijYnM`v?cl43cy4IS9&`AOluX&4M{3%cMmU+8#@96J1O`SlUvU3{oaw?6|j-8f&)2|69 z*<$gkz37zfCG(SQ)6lZ>oOWq_b(Y>!J*xY+x({-ymv-`au50&>m3=z|fF&Z9%*$t;V7fhNj7#-$e zF#DBYzA@e?tI?azJ-hwqH z$6FMP%tjb7#uJom%+c1}weQ)x--_qvp8c+m*?FkD7(Db&s(Z9M+pPpgqtVR;GdG9s zIrbF zZapfqwX}OLn2FZNU6}=#khmtgM==LW7*|?5K^atY6qtKRsAhs+FrzyNhiDo;Ks`&r zQH=eGXzUNO7RJ7au_GrL*TcQ>!Z1~aNhWpcZ(oBF)jhuZ_k*PfsviKVquob>km>bqQA+_F#>FwP&QR`&)6l$GJYJG$j zU@}JvLTTqC#AlFV<|?f}Lc1ph+C9Z~0r^LOJbg<^_$p?HCaPC=F9pXj=JVar-KQfw zj7E4k-aY0Yh2efW7$pXH-oy2zV31^r7^vpOK$R7aQ^E1VakX^cfnIg0bGD*ge@C|} zP>?G(ARf88Qfn#s1$NQzG_;MX%O$#q>f+X+*x- z8eQVEljrA(e0D~Aak0Q0&>uwYq9{`b6MeTt9l1f|+J8=rC9&E-MNkBnHcB(_gu|AVYQvGl!xswZ&Wdo%GLG&Pn6+yHK2j_9L zM)gChKO&Ft zX+Na;FEil~bo?IDKTXjTLZPB5kcu&dF!m-u8iYYD7Zc(a>hjH#W~y?S38N-N;jjqX=vEy_f~C~Oskr_M zy4}PhfBI>>2CNjHV!zbqQBeOi1Rv96(J2k$9-0W3r9id}b2tl&<={6MqZY6KDuSzF z@kY(7uHUFP6%4*WS=R6i2sGy6O{jN7p|h^TA$QkZhsGBcxwJN1ANpCd87}tMpmr2X;lfcM)A#bLv0^pX zr%a(ZjL?zsm1?K!>wuu7aJHHPcSOV0@38~X1XE471sxae3D7rLAxqu2r8MU^9EspI7+zjB zy5Bw4{w9N8Vj${X#^9|euPV;)YfvO}iIy}X_>D0I#m{J*R6VHrd16cgT1b(gL8CR{ zJ6;W5#HN${Qjb`q{4mt)s=u?1>34G41JqJr|hUj8D>_G1Ux{mA3zSlqnepJ4XvugKre+m|@cPxKM zW%q1^P~J8jIZe*7P9P_5Gt7s#1H!z71(d?6Y1e^3nA%+kC_+w((@YND7H~qkkF5%7 zS%5SCAyvY*1uqCFBlHHK^Gta)83ZK+YNxcvc*hZlp?;xjo58$q`6-^?0Oh>^iKT#X8QQ>acOZ_~9Bt zp=2S44iy*`W4UE{pWI?NNT0Zpobbp-JTNHkxH2f7;+Vk%7ywjk%nB7~1$^SPK8Lhn z<$M4+CNj^5h1&J&I9{m(IQm{?5-3voE@0&7nP)f1I!cfBlDev~0&j&|Bcq-0J^^Y8b(P$E!C2nbZ@#Newes zc`zELue6&G6W(&^OGPs)N#uK|8V=Et>DRYAo7ZmC{7T)ccs@>nD}K8YNhg)-HP2O* zcB=w;PgTTDC6ay=YtlA)rvfJ{ccpR>w$`S{+I@x3NT#W|Vq}W{Nw8&;AMQ=GrV}5} za(Seig}@Dx(EpHW6E9!9bm8sSuU6lRT69U%6<+ydNPOvIi#;tAJ9FERmw2 z-e?=3oCIsB2#JC$n9T%n{~6T`5y+DGNBCSM_NX9OU|kDNG5xGC!!!(6rPP?+nL3mJ z237+p_a!_Y8}(_dv5mPw*)VT_Kh0W`=5VSfl9?s0{{d=ff`R#wF=esaZ{F~G^KT4% ze+NaLB&3Egi*{#zA6(pHqCGnSZh(nWS~hqoVG{bsOzmTS`aiLRt!0XP zTF^P9K1;;0D2}(kbP#QQQst*|zuJeWpRe@&UAg%B#h0!QBr*47V#7UIxp1XI>HPG8 zLS#))GxA=9i`o)#HVwprnsH=m8t{4p&q8E#b^bw0{ z2Mb?ViKN}6Y$V1VEPCbbH%?tx>GyMBoz~9Qb!g|pA_j(g>8;CeRN}*ywiuU)CEUcOpcef#y-2Z|C(9PaEZyjpq0`;=lbpXc`Hzw*}Qx8JP1 z`13OBpFs(y0eCWNJ?KvhPLdvl#e(!@p$L;L>|?U}kGNX?7X$KF{eVHTg1~|)mWy@~ z@mBr6S>hif2uI*@i-XRH)#`u346d(utqCXnEhIe6WyTYE;iThf93Wr=edse6Tw=%{ zK{amTmxVzC3HAmUk(fqN*(pR?2m{9ZGv z1gQ}QbG?iB7z}a_jI*|;Z|#Ulfq8^Juf^7MTtcN_CWO&jA6MgO1FfN6QOvj*RZh&f zlCCE9r{CIzVUNCIlY`882uK%kZUGUIQ75}Lq*Dkav2?mmHtl1|_7~*BKi9{F&mKk; z2Q~FiWyhj#CEFZXJNLyacw{RV6V4a6^`*&3_wTqgpMB$x1#)8U8!)|y9%__Enq@*b zk0iPzT;;xpw7w$0!Oz69a!Kqfkb4r}yL@k-5H0|}zCKQ@e-qW0OMT4XFg`%pLjOGm zR3VKGX3)r?L$)(wB$OgpTCFM~y1H)o#6M#_9KjQZEz#XA$9%CJqOoKvDr+l!L__)K03NJeqW%oH~e4Lv|~I>rA3l z8cOPd-8u2aNG%~IG9b!LBG7Fk!m}UVqo4zS19nQ>@u1v8w|+S5<19oR_VJaW%P?{r zKMSj2269psOBphg)XSNAOn=dm5;oP&q@1LSdPE*-nZ1l|b~)2{TL%=bh%59N zD=FQgm>EkM)l#GkF5HKz?9!KL2lcnY1jvL5`c69c6l{?u_&RXVXMyt}hV(bcQ^Zf$EtoR*ByP*QPV&^h(l`C_TW?KR~M zI*^(bK^HP%!s6PTtn-1ra~5Sj!e35EwKN45UrAQ;2tre5cH;-=?$lgRQ>?dU>Zbqi*JdUah| zGT0R!ao|!N3`oOl`8I9cVKIuzkqfj?*`#^YVG6;!5QX3eCl})MYsAnQ2J}uja6~Cq zePK|-Iew~{_;t2^U?%Lyv9iZTI1#_vHxYe&vUga)SSgk~XVr{@eSu4^;7{7CUo$XumN~;KX1c3# zT~MaHrf$<^9tRq-$Iw|ElLsgCX!1Ng2^czOe+v4DumO%Ol#g4h;)FSiw{h>BLu`56 z7;$)X6{hKG77p|HOBS%MBh?(^4AXhd_Q9E1P zo}4l17W3K+=%5WLkvoVu=xOR(;RLt(z7s=`qZf|%{NJM+3HqP2+Pf~2}W`5PjJ_A$Z7S{J~TdXZ9XXoS=b36ex)}ZaQMszaOP?)`67fN5B7ZsQgD${ zbj~t&C5Lfa5$5&AP}E)%W-D0uaoC(wu<;|c2+a)lA-UgRFG_04&XQ^j)47YfV57im zx^xHYmEwh6AC6p&l7yqQ_hCnQ8Mm;qeWfm{OGu&X816`7?-}dmVH73vV6VbWL3IQ> zX-@X4954aPd9>f0hVHk3TW)X)p|dz3!k91yIK}wmXt}K5_=D0%6Kr5-A~u3FY951w zM>NL1dO5YkvycR?3Ano*)&d;iKD7n7*9^1(G4~J{hW53E_C9g!|z7wCicD*`WQQi0~#5-9qaK6u&MYiNzoK6~HU4=qZ66);eaV$HLN zk4PN%3!}8>=<0|bJD{6$=q;z7=j?S$tue&U_ov8;DMgg^GeTJ}U?pDY%?~UZMXbaZ zHyk)CVnuE^fbFGzI)hB}wMeGfe_Or7t6sdvzTzrem5M&XG<}C)1E;0rCvsBiXmQ`9b#Gf$xjP398j-ZWi#o$^MlvQay;z0>J5-80*DcfIFB zB7Wewse|@R0~OYth#bM=V{jfrUkXQP&V!k~rl?dru+PD#gDE_Yr+@|L5cqdcGkaj4 zmkl}`AtP+TF<5M)gOXXo4}TIVjNl!z_iyiK?A-f*hm8GjA&lBXpR@g;LRdK-eT;XA zIhm{2Yj7J+wj9A-r(m$l6t6QI&szUZ%`9xAz4@62U}5@o;I31sw2f%77=iqszQIAt zHFp}fK*TW6AT;typ}phBm+%r$Q1Oo^8h*mA(Z~VO@G~egx_p(3WJ0eq6)kLBFR3@r zcsPyJ?wNMycs!Jd2xTmNqmXfdVbEl)pGDAl7-qP{qO#KeVLV=Z?Yf^^h)nG8>$W(y z#|4($TP}W$LgA8X=2c#62%}pQNyh?0r(_0&i+S73VQRa*-Di*dF|+@Ofk?_OV`4*r>lA-g1b2FH3sfH=YzquT6y9Yq zk%wuZ)3+E~|Khi~$~YT1WC5xyf8QHoU9^FAVh4WD})0^Q8%Nhlt8d$YdvEgRwJG!&*ry3{&_KfGnMlw8q^QxH-Y= zzKow~xhwflZ;+NlJ`a9|R}C|;jcM}xfK3bUa5{Oe<}+gAh~h>J6MO%eXianqQVRYR zXL#{dKFqBs{7_Kof(P}N4&WEHZfiY^*5UIBY030Pc+4f%u z*EHih@O{Dc@xC9BlEW-5Y=z*4Y5s + + + + crm.lead.cor.inherit1 + crm.lead + + + + + + + + + From 12cd2f546e4f8c6482a70bcb81776515edbaddd1 Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Mon, 14 Dec 2020 16:21:06 +0530 Subject: [PATCH 008/111] change sale_timesheet qweb file in xpath --- .../__pycache__/__init__.cpython-36.pyc | Bin 0 -> 210 bytes .../__pycache__/__init__.cpython-36.pyc | Bin 0 -> 193 bytes .../__pycache__/controllers.cpython-36.pyc | Bin 0 -> 160 bytes .../__pycache__/__init__.cpython-36.pyc | Bin 0 -> 283 bytes .../__pycache__/crm_lead.cpython-36.pyc | Bin 0 -> 453 bytes .../models/__pycache__/models.cpython-36.pyc | Bin 0 -> 150 bytes .../models/__pycache__/project.cpython-36.pyc | Bin 0 -> 787 bytes .../project_overview.cpython-36.pyc | Bin 0 -> 5801 bytes cor_custom/models/project_overview.py | 387 +------------- cor_custom/views/hr_timesheet_templates.xml | 470 +----------------- 10 files changed, 13 insertions(+), 844 deletions(-) create mode 100644 cor_custom/__pycache__/__init__.cpython-36.pyc create mode 100644 cor_custom/controllers/__pycache__/__init__.cpython-36.pyc create mode 100644 cor_custom/controllers/__pycache__/controllers.cpython-36.pyc create mode 100644 cor_custom/models/__pycache__/__init__.cpython-36.pyc create mode 100644 cor_custom/models/__pycache__/crm_lead.cpython-36.pyc create mode 100644 cor_custom/models/__pycache__/models.cpython-36.pyc create mode 100644 cor_custom/models/__pycache__/project.cpython-36.pyc create mode 100644 cor_custom/models/__pycache__/project_overview.cpython-36.pyc diff --git a/cor_custom/__pycache__/__init__.cpython-36.pyc b/cor_custom/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a2ca7f5dad7e0aa4741977cd979df0d8af3279e3 GIT binary patch literal 210 zcmXr!<>lIZ>O#Cb0|UcjAcg}bAj<)Wi)DaB3PTEG4nr-mKMKJ-{%)tzr zEH4>>N;Da7aVO{Jl@#UYlIZ>O#B`0|UcjAcg}bAj<)Wiv@s03PTEG4nr1ps*(6 zE$-y}ypp2)oSf96Vn0o$TMR`YjVl?7Sb!9m_~oUaUr?f-pOT+%Xac5mlS_+B@^f_) zQ&RHtiuIH8i*!MlIZ>O%Z$CI*Jb3`l?x$aVnYViq8g!Vt`$$>_I|p$H_5Ab$Dk=NFXd=cnZ7 z8=8PA-Q?2ZlKfoV#FUi$ykh<2{32bDEQk>gkpeRFN{aGxa#D*x@^H3ZLFFwDo80`A O(wtN~kTu0X%m4uV7%UI~ literal 0 HcmV?d00001 diff --git a/cor_custom/models/__pycache__/__init__.cpython-36.pyc b/cor_custom/models/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2651ed4b9d326251a1530c7cc35f2a44881552a4 GIT binary patch literal 283 zcmX}mu?oU45C-6+X=@P$U!y|}V#gvbt}bp}f;1T_jaQOZ@sWHYp{tXx;N)Eu6YkGP zj&NDe=hGs6s2gMKiS=IrYK!G_49zrenB^tsJrqqCBNgPQuVp+lv=j8At~^(Z_I{9`50ryX@3A5oJLjv literal 0 HcmV?d00001 diff --git a/cor_custom/models/__pycache__/crm_lead.cpython-36.pyc b/cor_custom/models/__pycache__/crm_lead.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..87c5da1763fe7ebb7d4a42485ab24ba48dfbcfa4 GIT binary patch literal 453 zcmYLFJ5R$f5cVq%Rqe*ez#61BLNHdS5+Hx??}MxJjQZGhwrcNIS>jWr+M>})`H@ng?5;*UR_cCN)IUtB zs&=hy)mawGdU3WctV~c;wdR%BaftTr>moPXE;nGz(Mf+z%bn{?Gc5p2>+;grDY6LE zK;^@%dFWH29hM literal 0 HcmV?d00001 diff --git a/cor_custom/models/__pycache__/models.cpython-36.pyc b/cor_custom/models/__pycache__/models.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..18206e2f506784bae4310bafab77e46a0f53af8b GIT binary patch literal 150 zcmXr!<>lIZ>O%YjMh1q*3`hXTXK(=GViq8g!Vt`$$>_I|p$H_5AbvUP=NFXd=cnZ7 z8=8PA-Q?2ZlKfoV#FUi$ykh<2{32bDEQk>gk8S%5Z)i#=Unctph|&I+LJh22pS}WB84AOi|C1|&$;uWZv@x|yU5F^cx_nV#hJUh?l^XccW-_wN26XK_R|UbN zNDR(U-BUE0WgJ*0fM-$%RS06ovq(>>803ya0O38ulOu?*&BUL6XgAuH{iQXPF37>Q z=-)P7ZEm!_$h+Go-5yF@UhAYTTb<|!IB5`HjLD~$wk)(i9#K_7fioF|yg)rib&t{9 zv5MJ8WGjBhlxQOuJTRms*4LOa$Wqz~27b|(jdmNYdsP_M`^BFw9d%uDA2?lKePQ+( z(|^55P1mOejCuBAaI0eLdef|O0Moj(Fm^>K1!@na`{AbJ?{+u-M47hO=;xlpk@_$oI{#9wcocdU)HgB|2eyS9**wz%!l=`&I>jxxO<^G{8y|xeE)h6%l z5yqD0>f@2Aozoc+JTNo*B)+5r@t6lrb&*lIC literal 0 HcmV?d00001 diff --git a/cor_custom/models/__pycache__/project_overview.cpython-36.pyc b/cor_custom/models/__pycache__/project_overview.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8831012054ea2adb64caf4359f7bed0a926cd9c8 GIT binary patch literal 5801 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 [INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted] - for sale_line_id in empty_line_ids: # add service SO line having no timesheet - sale_line_row_key = (map_sol_so.get(sale_line_id), sale_line_id) - sale_line = map_sol.get(sale_line_id) - is_milestone = sale_line.product_id.invoice_policy == 'delivery' and sale_line.product_id.service_type == 'manual' if sale_line else False - rows_sale_line[sale_line_row_key] = [{'label': map_sol_names.get(sale_line_id, _('No Sales Order Line')), 'res_id': sale_line_id, 'res_model': 'sale.order.line', 'type': 'sale_order_line', 'is_milestone': is_milestone}] + default_row_vals[:] - if not is_milestone: - rows_sale_line[sale_line_row_key][-2] = sale_line.product_uom._compute_quantity(sale_line.product_uom_qty, uom_hour, raise_if_failure=False) if sale_line else 0.0 - - rows_sale_line_all_data = {} - if not employees: - employees = self.env['hr.employee'].sudo().search(self.env['account.analytic.line']._domain_employee_id()) - for row_key, row_employee in rows_employee.items(): - sale_order_id, sale_line_id, employee_id = row_key - # sale line row - sale_line_row_key = (sale_order_id, sale_line_id) - if sale_line_row_key not in rows_sale_line: - sale_line = map_sol.get(sale_line_id, self.env['sale.order.line']) - is_milestone = sale_line.product_id.invoice_policy == 'delivery' and sale_line.product_id.service_type == 'manual' if sale_line else False - rows_sale_line[sale_line_row_key] = [{'label': map_sol_names.get(sale_line.id) if sale_line else _('No Sales Order Line'), 'res_id': sale_line_id, 'res_model': 'sale.order.line', 'type': 'sale_order_line', 'is_milestone': is_milestone}] + default_row_vals[:] # INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted - if not is_milestone: - rows_sale_line[sale_line_row_key][-2] = sale_line.product_uom._compute_quantity(sale_line.product_uom_qty, uom_hour, raise_if_failure=False) if sale_line else 0.0 - - if sale_line_row_key not in rows_sale_line_all_data: - rows_sale_line_all_data[sale_line_row_key] = [0] * len(row_employee) - for index in range(1, len(row_employee)): - if employee_id in employees.ids: - rows_sale_line[sale_line_row_key][index] += row_employee[index] - rows_sale_line_all_data[sale_line_row_key][index] += row_employee[index] - if not rows_sale_line[sale_line_row_key][0].get('is_milestone'): - rows_sale_line[sale_line_row_key][-1] = rows_sale_line[sale_line_row_key][-2] - rows_sale_line_all_data[sale_line_row_key][5] - else: - rows_sale_line[sale_line_row_key][-1] = 0 - - rows_sale_order = {} # so -> [INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted] - for row_key, row_sale_line in rows_sale_line.items(): - sale_order_id = row_key[0] - # sale order row - if sale_order_id not in rows_sale_order: - rows_sale_order[sale_order_id] = [{'label': map_so_names.get(sale_order_id, _('No Sales Order')), 'canceled': map_so_cancel.get(sale_order_id, False), 'res_id': sale_order_id, 'res_model': 'sale.order', 'type': 'sale_order'}] + default_row_vals[:] # INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted - - for index in range(1, len(row_sale_line)): - rows_sale_order[sale_order_id][index] += row_sale_line[index] - - # group rows SO, SOL and their related employee rows. - timesheet_forecast_table_rows = [] - for sale_order_id, sale_order_row in rows_sale_order.items(): - timesheet_forecast_table_rows.append(sale_order_row) - for sale_line_row_key, sale_line_row in rows_sale_line.items(): - if sale_order_id == sale_line_row_key[0]: - sale_order_row[0]['has_children'] = True - timesheet_forecast_table_rows.append(sale_line_row) - for employee_row_key, employee_row in rows_employee.items(): - if sale_order_id == employee_row_key[0] and sale_line_row_key[1] == employee_row_key[1] and employee_row_key[2] in employees.ids: - sale_line_row[0]['has_children'] = True - timesheet_forecast_table_rows.append(employee_row) - - if is_uom_day: - # convert all values from hours to days - for row in timesheet_forecast_table_rows: - for index in range(1, len(row)): - row[index] = round(uom_hour._compute_quantity(row[index], company_uom, raise_if_failure=False), 2) - # complete table data - return { - 'header': self._table_header(), - 'rows': timesheet_forecast_table_rows - } - def _table_header(self): - initial_date = fields.Date.from_string(fields.Date.today()) - ts_months = sorted([fields.Date.to_string(initial_date - relativedelta(months=i, day=1)) for i in range(0, DEFAULT_MONTH_RANGE)]) # M1, M2, M3 - - def _to_short_month_name(date): - month_index = fields.Date.from_string(date).month - return babel.dates.get_month_names('abbreviated', locale=get_lang(self.env).code)[month_index] - - header_names = [_('Sales Order'), _('Before')] + [_to_short_month_name(date) for date in ts_months] + [_('Total'), _('Sold'), _('Remaining')] - - result = [] - for name in header_names: - result.append({ - 'label': name, - 'tooltip': '', - }) - # add tooltip for reminaing - result[-1]['tooltip'] = _('What is still to deliver based on sold hours and hours already done. Equals to sold hours - done hours.') - return result - - def _table_row_default(self): - lenght = len(self._table_header()) - return [0.0] * (lenght - 1) # before, M1, M2, M3, Done, Sold, Remaining - - def _table_rows_sql_query(self): - initial_date = fields.Date.from_string(fields.Date.today()) - ts_months = sorted([fields.Date.to_string(initial_date - relativedelta(months=i, day=1)) for i in range(0, DEFAULT_MONTH_RANGE)]) # M1, M2, M3 - # build query - query = """ - SELECT - 'timesheet' AS type, - date_trunc('month', date)::date AS month_date, - E.id AS employee_id, - S.order_id AS sale_order_id, - A.so_line AS sale_line_id, - SUM(A.unit_amount) AS number_hours - FROM account_analytic_line A - JOIN hr_employee E ON E.id = A.employee_id - LEFT JOIN sale_order_line S ON S.id = A.so_line - WHERE A.project_id IS NOT NULL - AND A.project_id IN %s - AND A.date < %s - GROUP BY date_trunc('month', date)::date, S.order_id, A.so_line, E.id - """ - - last_ts_month = fields.Date.to_string(fields.Date.from_string(ts_months[-1]) + relativedelta(months=1)) - query_params = (tuple(self.ids), last_ts_month) - return query, query_params - - def _table_rows_get_employee_lines(self, data_from_db): - initial_date = fields.Date.today() - ts_months = sorted([initial_date - relativedelta(months=i, day=1) for i in range(0, DEFAULT_MONTH_RANGE)]) # M1, M2, M3 - default_row_vals = self._table_row_default() - - # extract employee names - employee_ids = set() - for data in data_from_db: - employee_ids.add(data['employee_id']) - map_empl_names = {empl.id: empl.name for empl in self.env['hr.employee'].sudo().browse(employee_ids)} - - # extract rows data for employee, sol and so rows - rows_employee = {} # (so, sol, employee) -> [INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted] - for data in data_from_db: - sale_line_id = data['sale_line_id'] - sale_order_id = data['sale_order_id'] - # employee row - row_key = (data['sale_order_id'], sale_line_id, data['employee_id']) - if row_key not in rows_employee: - meta_vals = { - 'label': map_empl_names.get(row_key[2]), - 'sale_line_id': sale_line_id, - 'sale_order_id': sale_order_id, - 'res_id': row_key[2], - 'res_model': 'hr.employee', - 'type': 'hr_employee' - } - rows_employee[row_key] = [meta_vals] + default_row_vals[:] # INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted - - index = False - if data['type'] == 'timesheet': - if data['month_date'] in ts_months: - index = ts_months.index(data['month_date']) + 2 - elif data['month_date'] < ts_months[0]: - index = 1 - rows_employee[row_key][index] += data['number_hours'] - rows_employee[row_key][5] += data['number_hours'] - return rows_employee - - def _table_get_empty_so_lines(self): - """ get the Sale Order Lines having no timesheet but having generated a task or a project """ - so_lines = self.sudo().mapped('sale_line_id.order_id.order_line').filtered(lambda sol: sol.is_service and not sol.is_expense and not sol.is_downpayment) - # include the service SO line of SO sharing the same project - sale_order = self.env['sale.order'].search([('project_id', 'in', self.ids)]) - return set(so_lines.ids) | set(sale_order.mapped('order_line').filtered(lambda sol: sol.is_service and not sol.is_expense).ids), set(so_lines.mapped('order_id').ids) | set(sale_order.ids) - - # -------------------------------------------------- - # Actions: Stat buttons, ... - # -------------------------------------------------- - - def _plan_prepare_actions(self, values): - actions = [] - if len(self) == 1: - task_order_line_ids = [] - # retrieve all the sale order line that we will need later below - if self.env.user.has_group('sales_team.group_sale_salesman') or self.env.user.has_group('sales_team.group_sale_salesman_all_leads'): - task_order_line_ids = self.env['project.task'].read_group([('project_id', '=', self.id), ('sale_line_id', '!=', False)], ['sale_line_id'], ['sale_line_id']) - task_order_line_ids = [ol['sale_line_id'][0] for ol in task_order_line_ids] - - if self.env.user.has_group('sales_team.group_sale_salesman'): - if self.bill_type == 'customer_project' and self.allow_billable and not self.sale_order_id: - actions.append({ - 'label': _("Create a Sales Order"), - 'type': 'action', - 'action_id': 'sale_timesheet.project_project_action_multi_create_sale_order', - 'context': json.dumps({'active_id': self.id, 'active_model': 'project.project'}), - }) - if self.env.user.has_group('sales_team.group_sale_salesman_all_leads'): - to_invoice_amount = values['dashboard']['profit'].get('to_invoice', False) # plan project only takes services SO line with timesheet into account - - sale_order_ids = self.env['sale.order.line'].read_group([('id', 'in', task_order_line_ids)], ['order_id'], ['order_id']) - sale_order_ids = [s['order_id'][0] for s in sale_order_ids] - sale_order_ids = self.env['sale.order'].search_read([('id', 'in', sale_order_ids), ('invoice_status', '=', 'to invoice')], ['id']) - sale_order_ids = list(map(lambda x: x['id'], sale_order_ids)) - - if to_invoice_amount and sale_order_ids: - if len(sale_order_ids) == 1: - actions.append({ - 'label': _("Create Invoice"), - 'type': 'action', - 'action_id': 'sale.action_view_sale_advance_payment_inv', - 'context': json.dumps({'active_ids': sale_order_ids, 'active_model': 'project.project'}), - }) - else: - actions.append({ - 'label': _("Create Invoice"), - 'type': 'action', - 'action_id': 'sale_timesheet.project_project_action_multi_create_invoice', - 'context': json.dumps({'active_id': self.id, 'active_model': 'project.project'}), - }) - return actions - - def _plan_get_stat_button(self): - stat_buttons = [] - num_projects = len(self) - if num_projects == 1: - action_data = _to_action_data('project.project', res_id=self.id, - views=[[self.env.ref('project.edit_project').id, 'form']]) - else: - action_data = _to_action_data(action=self.env.ref('project.open_view_project_all_config').sudo(), - domain=[('id', 'in', self.ids)]) - - stat_buttons.append({ - 'name': _('Project') if num_projects == 1 else _('Projects'), - 'count': num_projects, - 'icon': 'fa fa-puzzle-piece', - 'action': action_data - }) - - # if only one project, add it in the context as default value - tasks_domain = [('project_id', 'in', self.ids)] - tasks_context = self.env.context.copy() - tasks_context.pop('search_default_name', False) - late_tasks_domain = [('project_id', 'in', self.ids), ('date_deadline', '<', fields.Date.to_string(fields.Date.today())), ('date_end', '=', False)] - overtime_tasks_domain = [('project_id', 'in', self.ids), ('overtime', '>', 0), ('planned_hours', '>', 0)] - - # filter out all the projects that have no tasks - task_projects_ids = self.env['project.task'].read_group([('project_id', 'in', self.ids)], ['project_id'], ['project_id']) - task_projects_ids = [p['project_id'][0] for p in task_projects_ids] - - if len(task_projects_ids) == 1: - tasks_context = {**tasks_context, 'default_project_id': task_projects_ids[0]} - stat_buttons.append({ - 'name': _('Tasks'), - 'count': sum(self.mapped('task_count')), - 'icon': 'fa fa-tasks', - 'action': _to_action_data( - action=self.env.ref('project.action_view_task').sudo(), - domain=tasks_domain, - context=tasks_context - ) - }) - stat_buttons.append({ - 'name': [_("Tasks"), _("Late")], - 'count': self.env['project.task'].search_count(late_tasks_domain), - 'icon': 'fa fa-tasks', - 'action': _to_action_data( - action=self.env.ref('project.action_view_task').sudo(), - domain=late_tasks_domain, - context=tasks_context, - ), - }) - stat_buttons.append({ - 'name': [_("Tasks"), _("in Overtime")], - 'count': self.env['project.task'].search_count(overtime_tasks_domain), - 'icon': 'fa fa-tasks', - 'action': _to_action_data( - action=self.env.ref('project.action_view_task').sudo(), - domain=overtime_tasks_domain, - context=tasks_context, - ), - }) - - if self.env.user.has_group('sales_team.group_sale_salesman_all_leads'): - # read all the sale orders linked to the projects' tasks - task_so_ids = self.env['project.task'].search_read([ - ('project_id', 'in', self.ids), ('sale_order_id', '!=', False) - ], ['sale_order_id']) - task_so_ids = [o['sale_order_id'][0] for o in task_so_ids] - - sale_orders = self.mapped('sale_line_id.order_id') | self.env['sale.order'].browse(task_so_ids) - if sale_orders: - stat_buttons.append({ - 'name': _('Sales Orders'), - 'count': len(sale_orders), - 'icon': 'fa fa-dollar', - 'action': _to_action_data( - action=self.env.ref('sale.action_orders').sudo(), - domain=[('id', 'in', sale_orders.ids)], - context={'create': False, 'edit': False, 'delete': False} - ) - }) - - invoice_ids = self.env['sale.order'].search_read([('id', 'in', sale_orders.ids)], ['invoice_ids']) - invoice_ids = list(itertools.chain(*[i['invoice_ids'] for i in invoice_ids])) - invoice_ids = self.env['account.move'].search_read([('id', 'in', invoice_ids), ('move_type', '=', 'out_invoice')], ['id']) - invoice_ids = list(map(lambda x: x['id'], invoice_ids)) - - if invoice_ids: - stat_buttons.append({ - 'name': _('Invoices'), - 'count': len(invoice_ids), - 'icon': 'fa fa-pencil-square-o', - 'action': _to_action_data( - action=self.env.ref('account.action_move_out_invoice_type').sudo(), - domain=[('id', 'in', invoice_ids), ('move_type', '=', 'out_invoice')], - context={'create': False, 'delete': False} - ) - }) - - ts_tree = self.env.ref('hr_timesheet.hr_timesheet_line_tree') - ts_form = self.env.ref('hr_timesheet.hr_timesheet_line_form') - if self.env.company.timesheet_encode_uom_id == self.env.ref('uom.product_uom_day'): - timesheet_label = [_('Days'), _('Recorded')] - else: - timesheet_label = [_('Hours'), _('Recorded')] - - stat_buttons.append({ - 'name': timesheet_label, - 'count': sum(self.mapped('total_timesheet_time')), - 'icon': 'fa fa-calendar', - 'action': _to_action_data( - 'account.analytic.line', - domain=[('project_id', 'in', self.ids)], - views=[(ts_tree.id, 'list'), (ts_form.id, 'form')], - ) - }) - - return stat_buttons -def _to_action_data(model=None, *, action=None, views=None, res_id=None, domain=None, context=None): - # pass in either action or (model, views) - if action: - assert model is None and views is None - act = clean_action(action.read()[0], env=action.env) - model = act['res_model'] - views = act['views'] - # FIXME: search-view-id, possibly help? - descr = { - 'data-model': model, - 'data-views': json.dumps(views), - } - if context is not None: # otherwise copy action's? - descr['data-context'] = json.dumps(context) - if res_id: - descr['data-res-id'] = res_id - elif domain: - descr['data-domain'] = json.dumps(domain) - return descr + diff --git a/cor_custom/views/hr_timesheet_templates.xml b/cor_custom/views/hr_timesheet_templates.xml index f02a7d7..c997bba 100755 --- a/cor_custom/views/hr_timesheet_templates.xml +++ b/cor_custom/views/hr_timesheet_templates.xml @@ -1,467 +1,21 @@ - - + Timesheet Plan qweb project.project + - - -
-
-
-
-
- - - -
-

Recorded Days and Profitability

-

Recorded Hours and Profitability

-
- - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - Days recorded - Hours recorded - -
- - - - ( %) - - Billed on Timesheets -
- - - - ( %) - - Billed at a Fixed price -
- - - - ( %) - - No task found -
- - - - ( %) - - - - Non Billable Tasks - - -
- - - - ( %) - - - - Non Billable Timesheets - - -
- - - - ( %) - - Cancelled -
- - - - - Total
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Profitability -
- - - Invoiced -
- - - To invoice -
- - Other Revenues
- - - Timesheet costs -
- - - Other costs -
- - - Re-invoiced costs -
- - - - Total
-
-
-
-
- -
-

Time by people

-
- -
- -

There are no timesheets for now.

-
- -
- - - - - - - - - - - - - - - - - - - - - - - -
EmployeeHourly RateDays SpentHours Spent - -
- - - - - - - - - - - -
-
- - - - Billed on Timesheets - billable_time - - - Billed at a Fixed price - billable_fixed - - - No task found - non_billable_project - - - Non billable timesheets - non_billable_timesheet - - - Non billable tasks - non_billable - - - Cancelled - canceled - -
-
-
-
-
-
- -
-

Timesheets

-
- - -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Timesheets

- -
- - - - - - - - Cancelled - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
- -
-
-
-
-
+ + Hourly Rate + + + + + +
-
From 54e7317925d5c374f1f8101cb61933e7f9737ffe Mon Sep 17 00:00:00 2001 From: projectsodoo Date: Tue, 15 Dec 2020 15:06:09 +0530 Subject: [PATCH 009/111] crm lead extra fields added --- cor_custom/models/crm_lead.py | 4 ++++ cor_custom/views/crm_view.xml | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/cor_custom/models/crm_lead.py b/cor_custom/models/crm_lead.py index 41801e1..3440a59 100644 --- a/cor_custom/models/crm_lead.py +++ b/cor_custom/models/crm_lead.py @@ -8,3 +8,7 @@ class Lead(models.Model): _inherit = 'crm.lead' lead_no = fields.Char(string='Lead ID') + professional_support = fields.Char(string='Professional Support') + ref_summary_status = fields.Char(string='Referral Summary status') + project_scope = fields.Char(string='The scope of the project') + client_folder = fields.Char(string='Client Folder') diff --git a/cor_custom/views/crm_view.xml b/cor_custom/views/crm_view.xml index 643fe2b..ac52133 100755 --- a/cor_custom/views/crm_view.xml +++ b/cor_custom/views/crm_view.xml @@ -9,6 +9,12 @@ + + + + + +
From d6c40f510e9c122a481218f90744504d103f8544 Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Tue, 15 Dec 2020 16:57:16 +0530 Subject: [PATCH 010/111] all pyc files removed --- cor_custom/__pycache__/__init__.cpython-36.pyc | Bin 210 -> 0 bytes .../__pycache__/__init__.cpython-36.pyc | Bin 193 -> 0 bytes .../__pycache__/controllers.cpython-36.pyc | Bin 160 -> 0 bytes .../models/__pycache__/__init__.cpython-36.pyc | Bin 283 -> 0 bytes .../models/__pycache__/crm_lead.cpython-36.pyc | Bin 453 -> 0 bytes .../models/__pycache__/models.cpython-36.pyc | Bin 150 -> 0 bytes .../models/__pycache__/project.cpython-36.pyc | Bin 787 -> 0 bytes .../__pycache__/project_overview.cpython-36.pyc | Bin 5801 -> 0 bytes project_maintenance/views/maintenance_view.xml | 0 project_maintenance/views/project_view.xml | 0 10 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 cor_custom/__pycache__/__init__.cpython-36.pyc delete mode 100644 cor_custom/controllers/__pycache__/__init__.cpython-36.pyc delete mode 100644 cor_custom/controllers/__pycache__/controllers.cpython-36.pyc delete mode 100644 cor_custom/models/__pycache__/__init__.cpython-36.pyc delete mode 100644 cor_custom/models/__pycache__/crm_lead.cpython-36.pyc delete mode 100644 cor_custom/models/__pycache__/models.cpython-36.pyc delete mode 100644 cor_custom/models/__pycache__/project.cpython-36.pyc delete mode 100644 cor_custom/models/__pycache__/project_overview.cpython-36.pyc mode change 100644 => 100755 project_maintenance/views/maintenance_view.xml mode change 100644 => 100755 project_maintenance/views/project_view.xml diff --git a/cor_custom/__pycache__/__init__.cpython-36.pyc b/cor_custom/__pycache__/__init__.cpython-36.pyc deleted file mode 100644 index a2ca7f5dad7e0aa4741977cd979df0d8af3279e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 210 zcmXr!<>lIZ>O#Cb0|UcjAcg}bAj<)Wi)DaB3PTEG4nr-mKMKJ-{%)tzr zEH4>>N;Da7aVO{Jl@#UYlIZ>O#B`0|UcjAcg}bAj<)Wiv@s03PTEG4nr1ps*(6 zE$-y}ypp2)oSf96Vn0o$TMR`YjVl?7Sb!9m_~oUaUr?f-pOT+%Xac5mlS_+B@^f_) zQ&RHtiuIH8i*!MlIZ>O%Z$CI*Jb3`l?x$aVnYViq8g!Vt`$$>_I|p$H_5Ab$Dk=NFXd=cnZ7 z8=8PA-Q?2ZlKfoV#FUi$ykh<2{32bDEQk>gkpeRFN{aGxa#D*x@^H3ZLFFwDo80`A O(wtN~kTu0X%m4uV7%UI~ diff --git a/cor_custom/models/__pycache__/__init__.cpython-36.pyc b/cor_custom/models/__pycache__/__init__.cpython-36.pyc deleted file mode 100644 index 2651ed4b9d326251a1530c7cc35f2a44881552a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 283 zcmX}mu?oU45C-6+X=@P$U!y|}V#gvbt}bp}f;1T_jaQOZ@sWHYp{tXx;N)Eu6YkGP zj&NDe=hGs6s2gMKiS=IrYK!G_49zrenB^tsJrqqCBNgPQuVp+lv=j8At~^(Z_I{9`50ryX@3A5oJLjv diff --git a/cor_custom/models/__pycache__/crm_lead.cpython-36.pyc b/cor_custom/models/__pycache__/crm_lead.cpython-36.pyc deleted file mode 100644 index 87c5da1763fe7ebb7d4a42485ab24ba48dfbcfa4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 453 zcmYLFJ5R$f5cVq%Rqe*ez#61BLNHdS5+Hx??}MxJjQZGhwrcNIS>jWr+M>})`H@ng?5;*UR_cCN)IUtB zs&=hy)mawGdU3WctV~c;wdR%BaftTr>moPXE;nGz(Mf+z%bn{?Gc5p2>+;grDY6LE zK;^@%dFWH29hM diff --git a/cor_custom/models/__pycache__/models.cpython-36.pyc b/cor_custom/models/__pycache__/models.cpython-36.pyc deleted file mode 100644 index 18206e2f506784bae4310bafab77e46a0f53af8b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 150 zcmXr!<>lIZ>O%YjMh1q*3`hXTXK(=GViq8g!Vt`$$>_I|p$H_5AbvUP=NFXd=cnZ7 z8=8PA-Q?2ZlKfoV#FUi$ykh<2{32bDEQk>gk8S%5Z)i#=Unctph|&I+LJh22pS}WB84AOi|C1|&$;uWZv@x|yU5F^cx_nV#hJUh?l^XccW-_wN26XK_R|UbN zNDR(U-BUE0WgJ*0fM-$%RS06ovq(>>803ya0O38ulOu?*&BUL6XgAuH{iQXPF37>Q z=-)P7ZEm!_$h+Go-5yF@UhAYTTb<|!IB5`HjLD~$wk)(i9#K_7fioF|yg)rib&t{9 zv5MJ8WGjBhlxQOuJTRms*4LOa$Wqz~27b|(jdmNYdsP_M`^BFw9d%uDA2?lKePQ+( z(|^55P1mOejCuBAaI0eLdef|O0Moj(Fm^>K1!@na`{AbJ?{+u-M47hO=;xlpk@_$oI{#9wcocdU)HgB|2eyS9**wz%!l=`&I>jxxO<^G{8y|xeE)h6%l z5yqD0>f@2Aozoc+JTNo*B)+5r@t6lrb&*lIC diff --git a/cor_custom/models/__pycache__/project_overview.cpython-36.pyc b/cor_custom/models/__pycache__/project_overview.cpython-36.pyc deleted file mode 100644 index 8831012054ea2adb64caf4359f7bed0a926cd9c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5801 zcmb7IOLH5?5#9#?E?xvkg5XmWMa#0F2PnT2i=ri4mYvw59b1kPRGh6PW=So)z(TVN ziexQv2wky!P=z@pm4hp}IH}4%$PdUb$RVkkQ>t>(HAkKD_3SPll*)0)+S#6-o_MG0rz$#$txlJJCEZcjC)02__zcNLapxi1u! zv$L(4BfU{!>L*Gq{}_5GTQyY_x$Rnkvu88g4Xm0jGCPj#GGC~s$h19ReNg4>{VunC z-|;$_<#$}q3QX?xIt)`|*A7hA>g)n8G+o>3m{v0Y>0=#2nj|Ut(*Rt@mSSm^VH3OQekN2xl^HAx%FK?wI|c(caioMP zmis!>p9qXrHq5a6t~#f%!jX1z&I<8=@#_?u{95lDL%!^X?EPK6pAB-P5u065TKO=0 zq_zsIdPG|1f?}8p(`@dM*3XA2{-0nH+Lpq6sIw_H|43&GN3@a;4Ys(Tu%!(phVp4; zK?(D(!z~*)|HKmZ0b6Fb*%oXp3}lu=$YZ5l4Uc-H_lrS}t+3NyC}HuYLVAR`Ii)|j z|7V(i_J@8cEV0sp(l0~1QfrEIjdh#~Q^zX*eXQlwn3hxFRHR#2KB47Qq-8jXz5l@8 zgy-av!d5E^+1Sxop+6l?gToov6>$K>B0Hxe4#(>KpWju$A@W8Z>)4yjHk6>qR>NFH zfh4Vafvqhmk=_wk>AmC}%IrDmvuQei_96RdS7&E;)m`OCXXkKEQ|v4|2j9=od9xST zc~)Z=*hR!ahN9pr8HMZ;D9c0d%Q(8s>TI28L1{>TSN4AuR^ZuR^h*yF_JTY)@tpx}1N??yAv4{iW3>hWOzuA9_ z{S>?2=%0>who=|7lZzdOK3u@rp9#-^+6~VD{{!GBP^B7Mhn1e!rpm;6&A;IwT~9U~%?f6ul9F*zXHarSK{ za`ArCVM22{wTv)gj4zC4k8``zJP_pp1Cx1e%jt*;sCJyd0y`%-NU#MzKyBad1%cP` zMb7c1VPrx&@B+)-MZuMSwUjWN&YtHqNr%8QX6Yb2kTThC0QN7g_JK0{RSea0|+BwENVvsQh^>{Y9y1LaSd9DP{#0zXX&Vp4?vd zy1}}~JnzL<6lRtuY44DOJ*1S7~;zATHH4GEVpO-^$pi+TCTqdAldwwk%lW69#dl_ za(s7H^$t>Ds;w&r#XA%_ba5=Vv?#nRh z>QK?+fmk@PZ6uuLPAm;#ZgLn!QiVw`xMy>d+k18g_C9rhGtheM08z};3R|$>lI-Kk;Ml}wTDU_X3?zINwklQz;IOUuInAxh=2C9U;bBM zUnez2G!Xd_c0<1nHR%pD;Wq#ej3-+tRHeiMfDk{0P%^XyplbS-q#m(Y73%&80m9gD zt@bvvHht`;M>(v2*RYQ^iX2@Wt8)Nv9Om!wdSVWeN6NJh4+i>@u4z0{-;;;MR7d@t z7GHmjuY!)(Xc%$T5D}R%IIKPh;|Qwdq}igFs9|<2WdF&*w9AoVd68I?&-f$_B^u}l zP$Yt~tuV|aiDwhY9J7Ve*bJWk;}%egm&uu&gGAz=FyxQKE=oVo7B%X&=)#R^}`O-J%;`P9|B|_fK@~O?nhM>I=;!;$j05s*-6ZA20nXavglyAE`oLUWG zBwZy=C{9|yaJp2YNZsb3#~Eo0sh2{t6_tHa9@M5-f|yPE9oet?Nz$mu{>~g<6q6_B zBkbZna&jnux*jn*oJSlwzg*8gUu*`E?>I zTb4U|7``YaDIzf@n;?8KJ(?O~oy%i5(z+Vi>Ea;K0SZGoz8fFE+ z#za{yYcvNYJ*_SRo6@QnD|!{PD)t!aw3>-<+8S0C)kS?xBYqm7dG!)#=Hverby8hX z4b4cO2ehg!YG>7X{T%j8YnK2|>nTucYD)e`a-*?A91C~U6>VBwgS;H}zXZw4s*%o- zrt0DHx7Fz_eD*^DqkChTB4?Ute|s*$1(Q;O8?P8>DbeOR0sl4J5>*7MxFfY8k13^I zpX!~VrqY}+wONthwzh4zj*!CrNXv<+(ySLaE?Uu%FQJ@j8n-csRLl1|B1P>vr_YU? z*mCL%8U`Wjp1&6*j+~szWFvE1qclX++m7Gl1~FWf*r@ZbKd`s!O|KJh&vntS)#;}h z&l1_p&3A6y`1FH2=Isx+?!0Gyd}Hg~cSQO&eUp_2J Date: Tue, 15 Dec 2020 20:39:26 +0530 Subject: [PATCH 011/111] Employee rate budgeted qty and uom added --- cor_custom/models/project.py | 3 +++ cor_custom/views/project_view.xml | 2 ++ 2 files changed, 5 insertions(+) diff --git a/cor_custom/models/project.py b/cor_custom/models/project.py index 5692ac8..a60d5f1 100755 --- a/cor_custom/models/project.py +++ b/cor_custom/models/project.py @@ -7,6 +7,9 @@ class InheritProjectProductEmployeeMap(models.Model): _inherit = 'project.sale.line.employee.map' employee_price = fields.Float("Employee Price") + budgeted_qty = fields.Float(string='Budgeted qty', related='sale_line_id.product_uom_qty', readonly=True) + budgeted_uom = fields.Many2one('uom.uom', string='Budgeted UOM', related='sale_line_id.product_uom', readonly=True) + @api.onchange('employee_id') def _onchange_employee_price(self): diff --git a/cor_custom/views/project_view.xml b/cor_custom/views/project_view.xml index 9133928..1d3d46b 100755 --- a/cor_custom/views/project_view.xml +++ b/cor_custom/views/project_view.xml @@ -25,6 +25,8 @@ + + From 83f6ea35e686df7f5886e2084770de2b39515a81 Mon Sep 17 00:00:00 2001 From: projectsodoo Date: Wed, 16 Dec 2020 11:03:26 +0530 Subject: [PATCH 012/111] Analytic percentage field added and crm start, close date added --- cor_custom/__manifest__.py | 3 ++- cor_custom/models/__init__.py | 3 ++- cor_custom/models/analytic.py | 13 +++++++++++++ cor_custom/models/crm_lead.py | 2 ++ cor_custom/views/analytic_view.xml | 18 ++++++++++++++++++ cor_custom/views/crm_view.xml | 2 ++ cor_custom/views/project_view.xml | 13 +++++++++++++ 7 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 cor_custom/models/analytic.py create mode 100755 cor_custom/views/analytic_view.xml diff --git a/cor_custom/__manifest__.py b/cor_custom/__manifest__.py index 48ec7f2..57a12ab 100644 --- a/cor_custom/__manifest__.py +++ b/cor_custom/__manifest__.py @@ -20,7 +20,7 @@ 'version': '0.1', # any module necessary for this one to work correctly - 'depends': ['base', 'crm', 'sale_timesheet'], + 'depends': ['base', 'crm', 'sale_timesheet', 'analytic'], # always loaded 'data': [ @@ -28,6 +28,7 @@ 'views/crm_view.xml', 'views/project_view.xml', 'views/hr_timesheet_templates.xml', + 'views/analytic_view.xml', 'views/views.xml', 'views/templates.xml', ], diff --git a/cor_custom/models/__init__.py b/cor_custom/models/__init__.py index 284dfb7..8e0c0a0 100644 --- a/cor_custom/models/__init__.py +++ b/cor_custom/models/__init__.py @@ -3,4 +3,5 @@ from . import crm_lead from . import models from . import project -from . import project_overview \ No newline at end of file +from . import project_overview +from . import analytic \ No newline at end of file diff --git a/cor_custom/models/analytic.py b/cor_custom/models/analytic.py new file mode 100644 index 0000000..97c0605 --- /dev/null +++ b/cor_custom/models/analytic.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError, AccessError +from odoo.osv import expression + +class AccountAnalyticLine(models.Model): + _inherit = 'account.analytic.line' + + ispercentage = fields.Boolean(string="Is Percentage", default=False) + percentage_rate = fields.Float(string="Percentage (%)") + per_from_amt = fields.Float(string="Of", digits=(6, 2)) \ No newline at end of file diff --git a/cor_custom/models/crm_lead.py b/cor_custom/models/crm_lead.py index 3440a59..27ff63e 100644 --- a/cor_custom/models/crm_lead.py +++ b/cor_custom/models/crm_lead.py @@ -12,3 +12,5 @@ class Lead(models.Model): ref_summary_status = fields.Char(string='Referral Summary status') project_scope = fields.Char(string='The scope of the project') client_folder = fields.Char(string='Client Folder') + start_date = fields.Date(string='Start Date') + close_date = fields.Date(string='Close Date') diff --git a/cor_custom/views/analytic_view.xml b/cor_custom/views/analytic_view.xml new file mode 100755 index 0000000..dd0c981 --- /dev/null +++ b/cor_custom/views/analytic_view.xml @@ -0,0 +1,18 @@ + + + + + Percentage Analytic + account.analytic.line + + + + + + + + + + + + diff --git a/cor_custom/views/crm_view.xml b/cor_custom/views/crm_view.xml index ac52133..11fa30e 100755 --- a/cor_custom/views/crm_view.xml +++ b/cor_custom/views/crm_view.xml @@ -14,6 +14,8 @@ + + diff --git a/cor_custom/views/project_view.xml b/cor_custom/views/project_view.xml index 1d3d46b..4f88986 100755 --- a/cor_custom/views/project_view.xml +++ b/cor_custom/views/project_view.xml @@ -1,6 +1,19 @@ + + Project Analytic + project.project + + +
+ +
+
+
+ + project.project.form.inherit project.project From 45f90d2c4d1082d9ca04c313a58d7cc832ce0c83 Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Wed, 16 Dec 2020 11:42:16 +0530 Subject: [PATCH 013/111] sub-project visible and required when checkbox selected --- cor_custom/__init__.py | 0 cor_custom/__manifest__.py | 0 .../__pycache__/__init__.cpython-36.pyc | Bin 0 -> 210 bytes cor_custom/controllers/__init__.py | 0 .../__pycache__/__init__.cpython-36.pyc | Bin 0 -> 193 bytes .../__pycache__/controllers.cpython-36.pyc | Bin 0 -> 160 bytes cor_custom/controllers/controllers.py | 0 cor_custom/demo/demo.xml | 0 cor_custom/models/__init__.py | 0 .../__pycache__/__init__.cpython-36.pyc | Bin 0 -> 314 bytes .../__pycache__/analytic.cpython-36.pyc | Bin 0 -> 765 bytes .../__pycache__/crm_lead.cpython-36.pyc | Bin 0 -> 751 bytes .../models/__pycache__/models.cpython-36.pyc | Bin 0 -> 150 bytes .../models/__pycache__/project.cpython-36.pyc | Bin 0 -> 986 bytes .../project_overview.cpython-36.pyc | Bin 0 -> 5801 bytes cor_custom/models/analytic.py | 0 cor_custom/models/crm_lead.py | 0 cor_custom/models/models.py | 0 cor_custom/security/ir.model.access.csv | 0 cor_custom/views/project_view.xml | 45 +++++++++++++----- cor_custom/views/templates.xml | 0 cor_custom/views/views.xml | 0 22 files changed, 32 insertions(+), 13 deletions(-) mode change 100644 => 100755 cor_custom/__init__.py mode change 100644 => 100755 cor_custom/__manifest__.py create mode 100755 cor_custom/__pycache__/__init__.cpython-36.pyc mode change 100644 => 100755 cor_custom/controllers/__init__.py create mode 100755 cor_custom/controllers/__pycache__/__init__.cpython-36.pyc create mode 100755 cor_custom/controllers/__pycache__/controllers.cpython-36.pyc mode change 100644 => 100755 cor_custom/controllers/controllers.py mode change 100644 => 100755 cor_custom/demo/demo.xml mode change 100644 => 100755 cor_custom/models/__init__.py create mode 100755 cor_custom/models/__pycache__/__init__.cpython-36.pyc create mode 100755 cor_custom/models/__pycache__/analytic.cpython-36.pyc create mode 100755 cor_custom/models/__pycache__/crm_lead.cpython-36.pyc create mode 100755 cor_custom/models/__pycache__/models.cpython-36.pyc create mode 100755 cor_custom/models/__pycache__/project.cpython-36.pyc create mode 100755 cor_custom/models/__pycache__/project_overview.cpython-36.pyc mode change 100644 => 100755 cor_custom/models/analytic.py mode change 100644 => 100755 cor_custom/models/crm_lead.py mode change 100644 => 100755 cor_custom/models/models.py mode change 100644 => 100755 cor_custom/security/ir.model.access.csv mode change 100644 => 100755 cor_custom/views/templates.xml mode change 100644 => 100755 cor_custom/views/views.xml diff --git a/cor_custom/__init__.py b/cor_custom/__init__.py old mode 100644 new mode 100755 diff --git a/cor_custom/__manifest__.py b/cor_custom/__manifest__.py old mode 100644 new mode 100755 diff --git a/cor_custom/__pycache__/__init__.cpython-36.pyc b/cor_custom/__pycache__/__init__.cpython-36.pyc new file mode 100755 index 0000000000000000000000000000000000000000..a2ca7f5dad7e0aa4741977cd979df0d8af3279e3 GIT binary patch literal 210 zcmXr!<>lIZ>O#Cb0|UcjAcg}bAj<)Wi)DaB3PTEG4nr-mKMKJ-{%)tzr zEH4>>N;Da7aVO{Jl@#UYlIZ>O#B`0|UcjAcg}bAj<)Wiv@s03PTEG4nr1ps*(6 zE$-y}ypp2)oSf96Vn0o$TMR`YjVl?7Sb!9m_~oUaUr?f-pOT+%Xac5mlS_+B@^f_) zQ&RHtiuIH8i*!MlIZ>O%Z$CI*Jb3`l?x$aVnYViq8g!Vt`$$>_I|p$H_5Ab$Dk=NFXd=cnZ7 z8=8PA-Q?2ZlKfoV#FUi$ykh<2{32bDEQk>gkpeRFN{aGxa#D*x@^H3ZLFFwDo80`A O(wtN~kTu0X%m4uV7%UI~ literal 0 HcmV?d00001 diff --git a/cor_custom/controllers/controllers.py b/cor_custom/controllers/controllers.py old mode 100644 new mode 100755 diff --git a/cor_custom/demo/demo.xml b/cor_custom/demo/demo.xml old mode 100644 new mode 100755 diff --git a/cor_custom/models/__init__.py b/cor_custom/models/__init__.py old mode 100644 new mode 100755 diff --git a/cor_custom/models/__pycache__/__init__.cpython-36.pyc b/cor_custom/models/__pycache__/__init__.cpython-36.pyc new file mode 100755 index 0000000000000000000000000000000000000000..87a3f969537de035afe1b887d03838ac9b6a65ec GIT binary patch literal 314 zcmX}mJ#NDw6bEqdSF9?LdWbF_TvWAV6m`j(r5g*ffWZ~^vw$SZEpmYzpht?=Ou0g) zzGqhjy+1#JfcL&yJ-uA6>dY9sVf~K;^@$ZI3^UxZj!VwT2|AI<JbYQFtsKwyn+vAhE^W2|+=D5J5^12m%tKabsC)U)acAAGQxAX`#(e z;0N%Na7%S1Efvpu;SNiFJ8woFkDm9@WK!Mx{*(R22>n9;trYNc*zPqHfdp1aV~RDQ zL?<-SDNS+YiOO_Nb6wD4=o3}y5glRl5{Xn~-;l^;a$bE+=vd?*P*beI4>f5>__7Q6 zqLWIvkZU8Pasji3gwaPQ?Xk7ShVfC`O6P{A!J(2DOACEx`Zn79TidWtfRTt|fhZA} zCNe$G1QCfyuL;eeD?}#pYfQ@l7orvwC?he3Xp?Xot~0CNAN5>a`mTM`_44X~Z^#+H zVP`7RlUMHWy|k_DJwKBtkUMwYcKul>gk12I@>hF1{qVv4ropy?e3ZOftaAWs0;M4X z8FJB`b>5vqUjuC?QL949SkJX&EL4oakX9<6p9zdD9L+T zcW${27_t*(xTpI&idpU@t$|<*Yc%89|3LO2-X6=J-Zd^~)n?aFzhr(6ir zyLoHuG^!D>jWpl9<9s`+<)s~gR|(t2QsR{C;TjL@X;X(ZI@magA-#=HX)`EcKPoeM y(aNQV2X{8k%ob2+IY`ERvC%VRZ{y!HSnlF)(4DB4PdAf$7R_C>xr=Y%n*0STiOg;Q literal 0 HcmV?d00001 diff --git a/cor_custom/models/__pycache__/crm_lead.cpython-36.pyc b/cor_custom/models/__pycache__/crm_lead.cpython-36.pyc new file mode 100755 index 0000000000000000000000000000000000000000..ebeacfddccacfcdbbcac5cc1e5c187a7e9133286 GIT binary patch literal 751 zcmYk4&2H2%5XYU*P18+5XeBu2z-5uPD+EV`Ds2HFL4{Zj$QR3s9X6{ZPHpd@xwcmx zfd}A`@Rbvh-8glvtA_|Q~7_IE`9XJAHv zlY|C>&`<<4lJP$AwZ8^5MUIhY$O&?WoFX3~&yaKAW04`xk&loIk%P8l7r&ESj9I0- z)7I0>@5{I6%fO|^>ZboPESFmoSV7R2E^xO*^qu}od;E8)eppBn9l(lIZ>O%YjMh1q*3`hXTXK(=GViq8g!Vt`$$>_I|p$H_5AbvUP=NFXd=cnZ7 z8=8PA-Q?2ZlKfoV#FUi$ykh<2{32bDEQk>gkdm zf*}?%a>wuL3uTJ-%;<&6iNG>%Usp}3S4vH@W_`ces1lo%Dk+OvCF)k4RHz?qjgQYv zk*js|VhMBADmcDuSL-A4lH?@|NH7$ImrGqq)P9VG3v6qOdhYs0l^J@u(5MV_U9Q%B zG)oY6LVkKbU61by>GrpUVXmCxxabhY<*}Ct_6X4xTQGA-c)_oj^y&aQ3JfRW&E{y* zomiLwGH%osm9iH~wKCVXb)&yo8frM^&bO*O|IW+^&40g|=%$@$(E8z%j-2F6+v@5h z18mDqa&1mXON`v8CY#Mqwv%pFZY*`3Uu5-M$$!&P4oh!>c>#~S_0#_?Fu5zzEFQX0 z$~vo*lx`p;RAP^-k2%0=H>w?Uk7U#pfu*sB-1i3jfFFuZK8yR#r?GX>x=IXvLDNIzdQ?KyQ|sh+X;1AaXfa{vGU literal 0 HcmV?d00001 diff --git a/cor_custom/models/__pycache__/project_overview.cpython-36.pyc b/cor_custom/models/__pycache__/project_overview.cpython-36.pyc new file mode 100755 index 0000000000000000000000000000000000000000..8831012054ea2adb64caf4359f7bed0a926cd9c8 GIT binary patch literal 5801 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.project
-
-
+ + +
@@ -20,29 +24,44 @@ - + - -
-