diff --git a/project_report/__init__.py b/project_report/__init__.py new file mode 100755 index 0000000..4d39843 --- /dev/null +++ b/project_report/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +from . import models +from . import wizard +from . import report + + + diff --git a/project_report/__manifest__.py b/project_report/__manifest__.py new file mode 100755 index 0000000..45c3274 --- /dev/null +++ b/project_report/__manifest__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +{ + 'name': 'Project Report', + 'summary': 'Projects Report', + 'description': "", + 'version': '1.0', + 'license': 'LGPL-3', + 'category': 'Services/Project', + 'author': "SunArc Technologies", + 'website': "http://www.sunarctechnologies.com", + 'depends': [ + 'cor_custom' + ], + 'data': [ + 'security/ir.model.access.csv', + 'views/project_view.xml', + 'wizard/project_create_expenses_views.xml', + 'report/project_budget_hrs_analysis_views.xml', + 'report/project_budget_amt_analysis_views.xml', + ], + 'auto_install': False, + 'installable': True, +} \ No newline at end of file diff --git a/project_report/models/__init__.py b/project_report/models/__init__.py new file mode 100755 index 0000000..3134359 --- /dev/null +++ b/project_report/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import project + diff --git a/project_report/models/project.py b/project_report/models/project.py new file mode 100755 index 0000000..f8fba07 --- /dev/null +++ b/project_report/models/project.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models, _ + + +class Project(models.Model): + _inherit = 'project.project' + + budgeted_hours = fields.Float(string='Budgeted Hours') + budgeted_revenue = fields.Float(string='Budgeted Revenue') + + diff --git a/project_report/report/__init__.py b/project_report/report/__init__.py new file mode 100755 index 0000000..bda40bb --- /dev/null +++ b/project_report/report/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import project_budget_hrs_analysis +from . import project_budget_amt_analysis diff --git a/project_report/report/project_budget_amt_analysis.py b/project_report/report/project_budget_amt_analysis.py new file mode 100755 index 0000000..a60b852 --- /dev/null +++ b/project_report/report/project_budget_amt_analysis.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models, tools + + +class BudgetAmtAnalysis(models.Model): + + _name = "project.budget.amt.report" + _description = "Project budget amount analysis report" + _order = 'project_id' + _auto = False + + #analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', readonly=True) + project_id = fields.Many2one('project.project', string='Project', readonly=True) + partner_id = fields.Many2one('res.partner', string='Client', readonly=True) + employee_id = fields.Many2one('hr.employee', string='Employee', readonly=True) + budgeted_revenue = fields.Float("Budgeted Revenue", digits=(16, 2), readonly=True, group_operator="sum") + actual_revenue = fields.Float("Actual Revenue", digits=(16, 2), readonly=True, group_operator="sum") + + def init(self): + '''Create the view''' + tools.drop_view_if_exists(self._cr, self._table) + self._cr.execute(""" + CREATE OR REPLACE VIEW %s AS ( + SELECT min(AAL.id) as id, + PRO.id AS project_id, + PRO.partner_id AS partner_id, + AAL.employee_id AS employee_id, + PRO.budgeted_hours AS budgeted_revenue, + sum(AAL.amount) AS actual_revenue + FROM project_project PRO + LEFT JOIN account_analytic_account AA ON PRO.analytic_account_id = AA.id + LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id + WHERE AAL.amount < 0.0 AND AAL.project_id IS NOT NULL AND PRO.active = 't' AND PRO.allow_timesheets = 't' + group by Pro.id, PRO.partner_id, AAL.employee_id, Pro.budgeted_hours, AAL.unit_amount + )""" % (self._table,)) + + diff --git a/project_report/report/project_budget_amt_analysis_views.xml b/project_report/report/project_budget_amt_analysis_views.xml new file mode 100755 index 0000000..44c17c0 --- /dev/null +++ b/project_report/report/project_budget_amt_analysis_views.xml @@ -0,0 +1,63 @@ + + + + + project.budget.amt.report.pivot + project.budget.amt.report + + + + + + + + + + + project.budget.amt.report.graph + project.budget.amt.report + + + + + + + + + + + + project.budget.amt.report.search + project.budget.amt.report + + + + + + + + + + + + + + + + Projects Revenue Acutal Vs Budget + project.budget.amt.report + pivot,graph + + { + 'group_by_no_leaf':1, + 'group_by':[], + } + + + + + diff --git a/project_report/report/project_budget_hrs_analysis.py b/project_report/report/project_budget_hrs_analysis.py new file mode 100755 index 0000000..df7e2d5 --- /dev/null +++ b/project_report/report/project_budget_hrs_analysis.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models, tools + + +class BudgetHrsAnalysis(models.Model): + + _name = "project.budget.hrs.report" + _description = "Project budget hours analysis report" + _order = 'project_id' + _auto = False + + #analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', readonly=True) + project_id = fields.Many2one('project.project', string='Project', readonly=True) + partner_id = fields.Many2one('res.partner', string='Client', readonly=True) + employee_id = fields.Many2one('hr.employee', string='Employee', readonly=True) + budgeted_hours = fields.Float("Budgeted Hours", digits=(16, 2), readonly=True, group_operator="sum") + actual_hours = fields.Float("Actual Hours", digits=(16, 2), readonly=True, group_operator="sum") + + def init(self): + '''Create the view''' + tools.drop_view_if_exists(self._cr, self._table) + self._cr.execute(""" + CREATE OR REPLACE VIEW %s AS ( + SELECT min(AAL.id) as id, + PRO.id AS project_id, + PRO.partner_id AS partner_id, + AAL.employee_id AS employee_id, + PRO.budgeted_hours AS budgeted_hours, + sum(AAL.unit_amount) AS actual_hours + FROM project_project PRO + LEFT JOIN account_analytic_account AA ON PRO.analytic_account_id = AA.id + LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id + WHERE AAL.amount < 0.0 AND AAL.project_id IS NOT NULL AND PRO.active = 't' AND PRO.allow_timesheets = 't' + group by Pro.id, PRO.partner_id, AAL.employee_id, Pro.budgeted_hours, AAL.unit_amount + )""" % (self._table,)) + + diff --git a/project_report/report/project_budget_hrs_analysis_views.xml b/project_report/report/project_budget_hrs_analysis_views.xml new file mode 100755 index 0000000..ae29b1e --- /dev/null +++ b/project_report/report/project_budget_hrs_analysis_views.xml @@ -0,0 +1,64 @@ + + + + + project.budget.hrs.report.pivot + project.budget.hrs.report + + + + + + + + + + + project.budget.hrs.report.graph + project.budget.hrs.report + + + + + + + + + + + + project.budget.hrs.report.search + project.budget.hrs.report + + + + + + + + + + + + + + + + Projects Hours Acutal Vs Budget + + project.budget.hrs.report + pivot,graph + + { + 'group_by_no_leaf':1, + 'group_by':[], + } + + + + + diff --git a/project_report/security/ir.model.access.csv b/project_report/security/ir.model.access.csv new file mode 100755 index 0000000..aa845cf --- /dev/null +++ b/project_report/security/ir.model.access.csv @@ -0,0 +1,6 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_project_budget_hrs_report_manager,project.budget.hrs.report,model_project_budget_hrs_report,project.group_project_manager,1,1,1,1 +access_project_budget_amt_report_manager,project.budget.amt.report,model_project_budget_amt_report,project.group_project_manager,1,1,1,1 +access_project_create_expense_manager,access_project_create_expense_project_manager,model_project_create_expense,project.group_project_manager,1,1,1,1 + + diff --git a/project_report/views/project_view.xml b/project_report/views/project_view.xml new file mode 100755 index 0000000..1ac1b86 --- /dev/null +++ b/project_report/views/project_view.xml @@ -0,0 +1,24 @@ + + + + + + Project Budget + project.project + + +
+ +
+ + + + +
+
+ + + +
diff --git a/project_report/wizard/__init__.py b/project_report/wizard/__init__.py new file mode 100755 index 0000000..f44b1f1 --- /dev/null +++ b/project_report/wizard/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- + +from . import project_create_expenses + + + diff --git a/project_report/wizard/project_create_expenses.py b/project_report/wizard/project_create_expenses.py new file mode 100644 index 0000000..1add259 --- /dev/null +++ b/project_report/wizard/project_create_expenses.py @@ -0,0 +1,86 @@ +# -*- 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 +from odoo.exceptions import ValidationError + + +class ProjectCreateExpense(models.TransientModel): + _name = 'project.create.expense' + _description = "Create Expense from project" + + @api.model + def default_get(self, fields): + result = super(ProjectCreateExpense, self).default_get(fields) + active_model = self._context.get('active_model') + if active_model != 'project.project': + raise UserError(_('You can only apply this action from a project.')) + active_id = self._context.get('active_id') + if 'project_id' in fields and active_id: + result['project_id'] = active_id + if 'analytic_id' in fields and active_id: + project = self.env['project.project'].browse(active_id) + if not project.analytic_account_id: + raise UserError(_("Please define Analytic Account.")) + result['analytic_id'] = project.analytic_account_id.id + return result + + project_id = fields.Many2one('project.project', "Project", required=True) + analytic_id = fields.Many2one('account.analytic.account', "Analytic Account", required=True) + description = fields.Char(string="Description", required=True) + ispercentage = fields.Boolean(string="Is Percentage", default=False) + #sale_order_id = fields.Many2one('sale.order', string="Sales Order", domain="['|', '|', ('partner_id', '=', partner_id), ('partner_id', 'child_of', commercial_partner_id), ('partner_id', 'parent_of', partner_id)]") + expenses_from = fields.Selection([ + ('invoiced', 'Invoiced'), + ('to_invoice', 'To Invoice') + ], "Expenses from", default='to_invoice') + per_from_amt = fields.Float(string="Of", digits=(6, 2)) + percentage_rate = fields.Float(string="Percentage (%)") + expenses_amt = fields.Float(string="Amount", digits=(6, 2), required=True) + + @api.onchange('ispercentage', 'expenses_from', 'percentage_rate', 'per_from_amt') + def onchange_ispercentage(self): + expense_amount = 0 + active_id = self._context.get('active_id') + if self.ispercentage and self.expenses_from: + profit = dict.fromkeys(['invoiced', 'to_invoice'], 0.0) + profitability_raw_data = self.env['project.profitability.report'].read_group([('project_id', '=', active_id)], + ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced'], ['project_id']) + print("profitability_raw_dataprofitability_raw_data", profitability_raw_data) + 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) + if self.expenses_from == 'invoiced': + self.per_from_amt = profit['invoiced'] + if self.expenses_from == 'to_invoice': + self.per_from_amt = profit['to_invoice'] + if self.percentage_rate > 0.0 and self.per_from_amt > 0.0: + expense_amount = self.per_from_amt * (self.percentage_rate / 100) + self.expenses_amt = expense_amount + + + def action_create_project_expense(self): + """ Private implementation of generating the sales order """ + expenses_amt = self.expenses_amt + if self.expenses_amt < 0: + expenses_amt = self.expense_amount * -1 + if self.expenses_amt == 0: + raise ValidationError(_("Amount shoud not be zero")) + analytic = self.env['account.analytic.line'].create({ + 'project_id': False, + 'partner_id': False, + 'account_id': self.project_id.analytic_account_id.id, + 'name': self.description, + 'date': fields.Date.context_today(self), + 'user_id': self.env.uid, + 'product_uom_id': False, + #'amount': self.expenses_amt * -1, + 'amount': expenses_amt + }) + action = self.env["ir.actions.actions"]._for_xml_id("analytic.account_analytic_line_action") + action['context'] = {'default_account_id': self.analytic_id.id} + action['domain'] = [('account_id', '=', self.analytic_id.id)] + return action + + diff --git a/project_report/wizard/project_create_expenses_views.xml b/project_report/wizard/project_create_expenses_views.xml new file mode 100644 index 0000000..c437702 --- /dev/null +++ b/project_report/wizard/project_create_expenses_views.xml @@ -0,0 +1,42 @@ + + + + + project.create.expense.view.form + project.create.expense + +
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + Create Expense + project.create.expense + form + + new + + +
diff --git a/project_report/wizard/project_create_invoice.py b/project_report/wizard/project_create_invoice.py new file mode 100644 index 0000000..0584981 --- /dev/null +++ b/project_report/wizard/project_create_invoice.py @@ -0,0 +1,58 @@ +# -*- 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 + + +class ProjectCreateExpenses(models.TransientModel): + _name = 'project.create.expenses' + _description = "Create Expenses from project" + + @api.model + def default_get(self, fields): + result = super(ProjectCreateInvoice, self).default_get(fields) + + active_model = self._context.get('active_model') + if active_model != 'project.project': + raise UserError(_('You can only apply this action from a project.')) + + active_id = self._context.get('active_id') + if 'project_id' in fields and active_id: + result['project_id'] = active_id + return result + + project_id = fields.Many2one('project.project', "Project", help="Project to make billable", required=True) + _candidate_orders = fields.Many2many('sale.order', compute='_compute_candidate_orders') + sale_order_id = fields.Many2one( + 'sale.order', string="Choose the Sales Order to invoice", required=True, + domain="[('id', 'in', _candidate_orders)]" + ) + amount_to_invoice = fields.Monetary("Amount to invoice", compute='_compute_amount_to_invoice', currency_field='currency_id', help="Total amount to invoice on the sales order, including all items (services, storables, expenses, ...)") + currency_id = fields.Many2one(related='sale_order_id.currency_id', readonly=True) + + @api.depends('project_id.tasks.sale_line_id.order_id.invoice_status') + def _compute_candidate_orders(self): + for p in self: + p._candidate_orders = p.project_id\ + .mapped('tasks.sale_line_id.order_id')\ + .filtered(lambda so: so.invoice_status == 'to invoice') + + @api.depends('sale_order_id') + def _compute_amount_to_invoice(self): + for wizard in self: + amount_untaxed = 0.0 + amount_tax = 0.0 + for line in wizard.sale_order_id.order_line.filtered(lambda sol: sol.invoice_status == 'to invoice'): + amount_untaxed += line.price_reduce * line.qty_to_invoice + amount_tax += line.price_tax + wizard.amount_to_invoice = amount_untaxed + amount_tax + + def action_create_invoice(self): + if not self.sale_order_id and self.sale_order_id.invoice_status != 'to invoice': + raise UserError(_("The selected Sales Order should contain something to invoice.")) + action = self.env["ir.actions.actions"]._for_xml_id("sale.action_view_sale_advance_payment_inv") + action['context'] = { + 'active_ids': self.sale_order_id.ids + } + return action diff --git a/project_report/wizard/project_create_invoice_views.xml b/project_report/wizard/project_create_invoice_views.xml new file mode 100644 index 0000000..867fb06 --- /dev/null +++ b/project_report/wizard/project_create_invoice_views.xml @@ -0,0 +1,30 @@ + + + + + project.create.expense.view.form + project.create.expense + +
+ + + + + +
+
+
+
+
+ + + Create Expense + project.create.expense + form + + new + + +