diff --git a/oca_dependencies.txt b/oca_dependencies.txt new file mode 100644 index 0000000..3c22dc8 --- /dev/null +++ b/oca_dependencies.txt @@ -0,0 +1,3 @@ +# list the OCA project dependencies, one per line +# add a github url if you need a forked version +project diff --git a/project_billing_utils/README.rst b/project_billing_utils/README.rst new file mode 100644 index 0000000..efb4425 --- /dev/null +++ b/project_billing_utils/README.rst @@ -0,0 +1,63 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :alt: License: AGPL-3 + +Analytic and project wizard for service companies +================================================= + + +Improve the view of analytic and timesheet lines for the project manager +Add a wizard to manage project and invoicing: + + * Associate Analytic Lines to invoice (from an invoice or from analytic line directly) + * Dissociate Analytic Lines from an invoice + * Get all invoices from Project (with recursion in child account) + * Get Analytic Lines from an invoice for controlling + * Create a blank invoice from project (with related infos) + + +For further information, please visit: + +* https://www.odoo.com/forum/help-1 + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/139/11.0 + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed feedback +`here `_. + + +Credits +======= + +Contributors +------------ + +* Yannick Vaucher +* Daniel Reis +* Joël Grand-Guillaume +* Leonardo Pistone +* Alexandre Fayolle +* Vincent Renaville +* Damien Crier +* Serpent Consulting Services Pvt. Ltd. + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit http://odoo-community.org. diff --git a/project_billing_utils/__init__.py b/project_billing_utils/__init__.py new file mode 100644 index 0000000..f987065 --- /dev/null +++ b/project_billing_utils/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import models +from . import wizard diff --git a/project_billing_utils/__manifest__.py b/project_billing_utils/__manifest__.py new file mode 100644 index 0000000..b02acb8 --- /dev/null +++ b/project_billing_utils/__manifest__.py @@ -0,0 +1,27 @@ +############################################################################## +# +# Author: Joël Grand-Guillaume +# Copyright 2010 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +############################################################################## + +{ + 'name': 'Analytic and project wizard for service companies', + 'version': '11.0.1.0.0', + 'category': 'Generic Modules/Projects & Services', + 'author': "Camptocamp,Odoo Community Association (OCA)", + 'website': 'https://github.com/OCA/project-reporting', + 'depends': ['hr_timesheet', 'sale_timesheet'], + 'license': 'AGPL-3', + 'data': [ + 'views/invoice_view.xml', + 'views/project_view.xml', + 'wizard/associate_aal_view.xml', + 'wizard/dissociate_aal_view.xml', + 'wizard/open_invoices_view.xml', + 'wizard/blank_invoice_view.xml', + ], + 'installable': True, + 'auto_install': False, + 'application': False, +} diff --git a/project_billing_utils/models/__init__.py b/project_billing_utils/models/__init__.py new file mode 100644 index 0000000..e03b05e --- /dev/null +++ b/project_billing_utils/models/__init__.py @@ -0,0 +1,5 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import analytic +from . import invoice +from . import project diff --git a/project_billing_utils/models/analytic.py b/project_billing_utils/models/analytic.py new file mode 100644 index 0000000..d538fcf --- /dev/null +++ b/project_billing_utils/models/analytic.py @@ -0,0 +1,32 @@ +############################################################################## +# +# Author: Leonardo Pistone +# Copyright 2014 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +############################################################################## + +"""Changes to allow the dissociate analytic lines wizard to work.""" + +from odoo import models, api + + +class AccountAnalyticLine(models.Model): + + """Hack the analytic line to optionally skip the invoice check.""" + + _inherit = 'account.analytic.line' + + @api.multi + def write(self, vals): + """Put a key in the vals, since we have no context. Return super.""" + if self.env.context.get('skip_invoice_check'): + vals['_x_vals_skip_invoice_check'] = True + return super(AccountAnalyticLine, self).write(vals) + + @api.multi + def _check_inv(self, vals): + """Optionally skip invoice check. Return boolean.""" + if '_x_vals_skip_invoice_check' in vals: + del vals['_x_vals_skip_invoice_check'] + return True + return super(AccountAnalyticLine, self)._check_inv(vals) diff --git a/project_billing_utils/models/invoice.py b/project_billing_utils/models/invoice.py new file mode 100644 index 0000000..dcf04cc --- /dev/null +++ b/project_billing_utils/models/invoice.py @@ -0,0 +1,54 @@ +############################################################################## +# +# Author: Joël Grand-Guillaume +# Copyright 2010 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +############################################################################## + +from odoo import models, api + + +class AccountInvoice(models.Model): + _inherit = 'account.invoice' + + @api.multi + def name_get(self): + if 'special_search' not in self.env.context: + return super(AccountInvoice, self).name_get() + else: + if not self: + return [] + # We will return value + rest = [] + for r in self: + partner_name = r.partner_id.name_get() + if partner_name: + partner_name = partner_name[0][1] + rest.append( + (r['id'], + ('%s - %s - %s' % (r.number or '', + partner_name or '', r.name or '')) + ) + ) + # We will + return rest + + @api.model + def name_search(self, name, args=None, operator='ilike', limit=100): + if 'special_search' not in self.env.context: + return super(AccountInvoice, self).name_search( + name, args=args, operator=operator, limit=limit) + invoices = self.env['account.invoice'] + if not args: + args = [] + if name: + invoices = self.search( + [('number', operator, name)] + args, limit=limit) + if not invoices: + invoices = self.search( + [('commercial_partner_id.name', operator, name)] + args, + limit=limit) + if not invoices: + invoices = self.search( + [('partner_id.name', operator, name)] + args, limit=limit) + return invoices.name_get() diff --git a/project_billing_utils/models/project.py b/project_billing_utils/models/project.py new file mode 100644 index 0000000..fcd4823 --- /dev/null +++ b/project_billing_utils/models/project.py @@ -0,0 +1,30 @@ +############################################################################## +# +# Author: Joël Grand-Guillaume +# Copyright 2010 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +############################################################################## + +from odoo import models, api, _ +from odoo.exceptions import UserError + + +class ProjectProject(models.Model): + _inherit = 'project.project' + _description = 'Project' + + @api.multi + def unlink(self): + # We will check if the account have no analytic line linked + account_line_obj = self.env['account.analytic.line'] + for project in self: + account_lines = account_line_obj.search( + [('account_id', '=', project.analytic_account_id.id)]) + # If we found line linked with account we raise an error + if account_lines: + raise UserError( + _('You cannot delete project %s as there are analytic ' + 'lines linked to it') % project.name) + else: + super(ProjectProject, project).unlink() + return True diff --git a/project_billing_utils/views/invoice_view.xml b/project_billing_utils/views/invoice_view.xml new file mode 100644 index 0000000..9856117 --- /dev/null +++ b/project_billing_utils/views/invoice_view.xml @@ -0,0 +1,21 @@ + + + + + + account.analytic.line.project.invoice + account.analytic.line + + + + + + + + + diff --git a/project_billing_utils/views/project_view.xml b/project_billing_utils/views/project_view.xml new file mode 100644 index 0000000..f915a7b --- /dev/null +++ b/project_billing_utils/views/project_view.xml @@ -0,0 +1,16 @@ + + + + + + project.project.form + project.project + + + + 1 + + + + + diff --git a/project_billing_utils/wizard/__init__.py b/project_billing_utils/wizard/__init__.py new file mode 100644 index 0000000..5ef6aad --- /dev/null +++ b/project_billing_utils/wizard/__init__.py @@ -0,0 +1,6 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import associate_aal +from . import dissociate_aal +from . import open_invoices +from . import blank_invoice diff --git a/project_billing_utils/wizard/associate_aal.py b/project_billing_utils/wizard/associate_aal.py new file mode 100644 index 0000000..dafea8b --- /dev/null +++ b/project_billing_utils/wizard/associate_aal.py @@ -0,0 +1,32 @@ +############################################################################## +# +# Author: Joël Grand-Guillaume +# Copyright 2010 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +############################################################################## + +from odoo import models, api, fields, _ + + +class AssociateInvoice(models.TransientModel): + _name = 'associate.aal.to.invoice' + _description = 'Associate Analytic Lines' + invoice_id = fields.Many2one('account.invoice', string='Invoice', + required=True) + + @api.multi + def associate_aal(self): + aal_obj = self.env[self.env.context['active_model']] + aal_ids = self.env.context.get('active_ids', False) + aal_rs = aal_obj.browse(aal_ids) + aal_rs.write({'timesheet_invoice_id': self.invoice_id.id}) + return { + 'domain': "[('id','in', [%s])]" % (self.invoice_id.id,), + 'name': _('Associated invoice'), + 'view_type': 'form', + 'view_mode': 'tree,form', + 'res_model': 'account.invoice', + 'view_id': False, + 'context': self.env.context, + 'type': 'ir.actions.act_window', + } diff --git a/project_billing_utils/wizard/associate_aal_view.xml b/project_billing_utils/wizard/associate_aal_view.xml new file mode 100644 index 0000000..aa47d70 --- /dev/null +++ b/project_billing_utils/wizard/associate_aal_view.xml @@ -0,0 +1,31 @@ + + + + + Associate To Invoice + associate.aal.to.invoice + +
+ + + + +
+
+
+
+
+ + + +
diff --git a/project_billing_utils/wizard/blank_invoice.py b/project_billing_utils/wizard/blank_invoice.py new file mode 100644 index 0000000..1481c29 --- /dev/null +++ b/project_billing_utils/wizard/blank_invoice.py @@ -0,0 +1,71 @@ +############################################################################## +# +# Author: Joël Grand-Guillaume +# Copyright 2010 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +############################################################################## + +from odoo import models, api, fields, _ +from odoo.tools.safe_eval import safe_eval +from odoo import exceptions + +import time + + +class CreateInvoicesFromProject(models.TransientModel): + _name = 'create.invoice.from.project' + _description = 'Create Invoices' + + @api.model + def _prepare_invoice(self, project): + """ + Prepare the values used for the invoice creation. + Override and properly use the `super` chain + to customize the invoice creation + """ + if not project.partner_id: + raise exceptions.Warning( + _('UserError'), + _('The Partner is missing on the project:\n%s') % project.name) + + partner = project.partner_id + + date_due = False + if partner.property_payment_term_id: + pterm_list = partner.property_payment_term_id.compute( + value=1, + date_ref=fields.Date.today()) + if pterm_list: + pterm_list = [line[0] for line in pterm_list] + pterm_list.sort() + date_due = pterm_list[-1] + + return { + 'name': '%s - %s' % (time.strftime('%D'), project.name), + 'type': 'out_invoice', + 'date_due': date_due, + 'partner_id': partner.id, + 'payment_term': partner.property_payment_term_id.id or False, + 'account_id': partner.property_account_receivable_id.id, + 'currency_id': project.currency_id.id, + } + + @api.multi + def create_invoices(self): + assert self.env.context.get('active_ids') is not None, \ + "create_invoices needs active_ids in context" + + project_obj = self.env['project.project'] + invoice_obj = self.env['account.invoice'] + for project in project_obj.browse(self.env.context['active_ids'],): + values = self._prepare_invoice(project) + last_invoice = invoice_obj.create(values) + invoice_obj += last_invoice + + xml_id = 'action_invoice_tree1' + view = self.env.ref('account.%s' % xml_id) + result = view.read()[0] + invoice_domain = safe_eval(result.get('domain', [])) + invoice_domain.append(('id', 'in', invoice_obj.ids)) + result['domain'] = invoice_domain + return result diff --git a/project_billing_utils/wizard/blank_invoice_view.xml b/project_billing_utils/wizard/blank_invoice_view.xml new file mode 100644 index 0000000..2fc429a --- /dev/null +++ b/project_billing_utils/wizard/blank_invoice_view.xml @@ -0,0 +1,29 @@ + + + + + Create blank invoice + create.invoice.from.project + +
+ + + +
+
+
+
+
+ + + +
diff --git a/project_billing_utils/wizard/dissociate_aal.py b/project_billing_utils/wizard/dissociate_aal.py new file mode 100644 index 0000000..88aa283 --- /dev/null +++ b/project_billing_utils/wizard/dissociate_aal.py @@ -0,0 +1,32 @@ +############################################################################## +# +# Author: Joël Grand-Guillaume, Leonardo Pistone +# Copyright 2010-2014 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +############################################################################## + +"""Introduce a wizard to dissociate an Analytic Line from an Invoice.""" +from odoo import models, api + + +class DissociateInvoice(models.TransientModel): + + """Wizard to dissociate an Analytic Line from an Invoice.""" + + _name = 'dissociate.aal.to.invoice' + _description = 'Dissociate Analytic Lines' + + @api.multi + def dissociate_aal(self): + """Dissociate invoice from the line and return {}. + + This is necessary because the module hr_timesheet_invoice introduces + a check that we want to avoid. + + """ + aal_obj = self.env[self.env.context['active_model']] + ctx = self.env.context.copy() + aals = aal_obj.browse(self.env.context['active_ids']) + ctx['skip_invoice_check'] = True + aals.with_context(ctx).write({'timesheet_invoice_id': False}) + return {} diff --git a/project_billing_utils/wizard/dissociate_aal_view.xml b/project_billing_utils/wizard/dissociate_aal_view.xml new file mode 100644 index 0000000..2b448c8 --- /dev/null +++ b/project_billing_utils/wizard/dissociate_aal_view.xml @@ -0,0 +1,30 @@ + + + + + Dissociate To Invoice + dissociate.aal.to.invoice + +
+ + + +
+
+
+
+
+ + + +
diff --git a/project_billing_utils/wizard/open_invoices.py b/project_billing_utils/wizard/open_invoices.py new file mode 100644 index 0000000..1144925 --- /dev/null +++ b/project_billing_utils/wizard/open_invoices.py @@ -0,0 +1,47 @@ +############################################################################## +# +# Author: Joël Grand-Guillaume +# Copyright 2010 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +############################################################################## + +from odoo import models, api +from odoo.tools.safe_eval import safe_eval + + +class OpenInvoicesFromProject(models.TransientModel): + _name = 'open.invoice.from.project' + _description = 'Open Invoices' + + @api.multi + def open_invoices(self): + aa_obj = self.env['project.project'] + + active_ids = self.env.context.get('active_ids', False) + aa_rs = aa_obj.browse(active_ids).mapped('analytic_account_id') + + # Use a SQL request because we can't do that so easily with the ORM + query = """ + SELECT inv.id from account_invoice inv + LEFT JOIN account_invoice_line l ON (inv.id=l.invoice_id) + WHERE l.account_analytic_id IN %s + """ + self.env.cr.execute(query, (tuple(aa_rs.ids),)) + + inv_ids = self.env.cr.fetchall() + line_ids = [] + for line in inv_ids: + line_ids.append(line[0]) + inv_type = self.env.context.get('inv_type', 'out_invoice') + + if 'out_invoice' in inv_type: + xml_id = 'action_invoice_tree1' + else: + xml_id = 'action_invoice_tree2' + + result = self.env.ref('account.%s' % xml_id) + result = result.read()[0] + invoice_domain = safe_eval(result.get('domain', [])) + invoice_domain.append(('id', 'in', line_ids)) + result['domain'] = invoice_domain + return result diff --git a/project_billing_utils/wizard/open_invoices_view.xml b/project_billing_utils/wizard/open_invoices_view.xml new file mode 100644 index 0000000..7f69631 --- /dev/null +++ b/project_billing_utils/wizard/open_invoices_view.xml @@ -0,0 +1,38 @@ + + + + + Open Related Invoices + open.invoice.from.project + +
+ + + +
+
+
+
+
+ + + + + +