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
+
+
+