diff --git a/analytic_partner_hr_timesheet_invoice/README.rst b/analytic_partner_hr_timesheet_invoice/README.rst index bf029fa4ce..36e5948c6a 100644 --- a/analytic_partner_hr_timesheet_invoice/README.rst +++ b/analytic_partner_hr_timesheet_invoice/README.rst @@ -14,63 +14,55 @@ Installation ============ This module is auto-installed by Odoo when *analytic_partner* and -*hr_timesheet_invoice* are installed. - -Configuration -============= - -You have to set the "Analytic Accounting" permission at user or db level for -seeing the analytic lines. +*analytic_partner_hr_timesheet* are installed. Usage ===== -Go to Accounting > Journal entries > Analytic Journal Items, and selecting some -of the lines, click on *More > Create Invoice*. Created invoice(s) will be -split by the other partner field (if any), or the analytic account partner. +Go to *Sales > Invoicing > Sales to invoice* and select some of the lines, +click on *Action > Invoice Order*. +Created invoice(s) will be split by the other partner field (if any), or the + partner. .. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas :alt: Try me on Runbot - :target: https://runbot.odoo-community.org/runbot/87/8.0 + :target: https://runbot.odoo-community.org/runbot/87/10.0 Known issues / Roadmap ====================== -* This module fully overwrites invoicing technical method - (*invoice_cost_create*), so it's incompatible with other modules that also - change something in the same method. If some hooks are provided on Odoo, - this module can be changed for respecting inheritance chain. 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 `_. +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 smash it by providing detailed and welcomed feedback. Credits ======= -Contributors ------------- - -* Pedro M. Baeza -* Rafael Blasco - -Icon ----- +Images +------ * Thanks to https://openclipart.org/detail/201137/primary%20template%20invoice * Thanks to https://openclipart.org/detail/15193/Arrow%20set%20%28Comic%29 -* Original Odoo icons +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Pedro M. Baeza +* Rafael Blasco +* Luis M. Ontalba Maintainer ---------- -.. image:: http://odoo-community.org/logo.png +.. image:: https://odoo-community.org/logo.png :alt: Odoo Community Association - :target: http://odoo-community.org + :target: https://odoo-community.org This module is maintained by the OCA. @@ -78,4 +70,4 @@ 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. +To contribute to this module, please visit https://odoo-community.org. diff --git a/analytic_partner_hr_timesheet_invoice/__init__.py b/analytic_partner_hr_timesheet_invoice/__init__.py index c7fc8d983c..4d8ae82d6d 100644 --- a/analytic_partner_hr_timesheet_invoice/__init__.py +++ b/analytic_partner_hr_timesheet_invoice/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- -# (c) 2015 Antiun Ingeniería S.L. - Pedro M. Baeza # License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + from . import models diff --git a/analytic_partner_hr_timesheet_invoice/__manifest__.py b/analytic_partner_hr_timesheet_invoice/__manifest__.py index c5475511c1..b8d1f20ece 100644 --- a/analytic_partner_hr_timesheet_invoice/__manifest__.py +++ b/analytic_partner_hr_timesheet_invoice/__manifest__.py @@ -1,23 +1,24 @@ # -*- coding: utf-8 -*- -# (c) 2015 Antiun Ingeniería S.L. - Pedro M. Baeza +# Copyright 2015 Antiun Ingeniería S.L. - Pedro M. Baeza +# Copyright 2017 Tecnativa, S.L. - Luis M. Ontalba # License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html { - 'name': 'Invoice to the partner in analytic lines', - 'version': '8.0.1.0.0', + 'name': 'Invoice to the other partner', + 'version': '10.0.1.0.0', 'license': 'AGPL-3', - 'summary': 'Invoice analytic lines for the specific partner in them', + 'summary': 'Invoice analytic lines for the other partner', 'category': 'Human Resources', - 'author': 'Antiun Ingeniería S.L., ' - 'Serv. Tecnol. Avanzados - Pedro M. Baeza, ' + 'author': 'Antiun Ingeniería, ' + 'Tecnativa, ' 'Odoo Community Association (OCA)', 'website': 'http://www.antiun.com', 'depends': [ + 'analytic_partner_hr_timesheet', 'analytic_partner', - 'hr_timesheet_invoice', ], 'data': [ ], - 'installable': False, + 'installable': True, "auto_install": True, } diff --git a/analytic_partner_hr_timesheet_invoice/models/__init__.py b/analytic_partner_hr_timesheet_invoice/models/__init__.py index ee6af6afa5..7e49547dc0 100644 --- a/analytic_partner_hr_timesheet_invoice/models/__init__.py +++ b/analytic_partner_hr_timesheet_invoice/models/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- -# (c) 2015 Antiun Ingeniería S.L. - Pedro M. Baeza # License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html -from . import account_analytic_line + +from . import sale_order_line diff --git a/analytic_partner_hr_timesheet_invoice/models/account_analytic_line.py b/analytic_partner_hr_timesheet_invoice/models/account_analytic_line.py deleted file mode 100644 index 412a3a3ced..0000000000 --- a/analytic_partner_hr_timesheet_invoice/models/account_analytic_line.py +++ /dev/null @@ -1,77 +0,0 @@ -# -*- coding: utf-8 -*- -# (c) 2015 Antiun Ingeniería S.L. - Pedro M. Baeza -# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html -from openerp import models, api, exceptions, _ - - -class AccountAnalyticLine(models.Model): - _inherit = 'account.analytic.line' - - @api.multi - def invoice_cost_create(self, data=None): - invoice_model = self.env['account.invoice'] - invoice_line_model = self.env['account.invoice.line'] - analytic_line_model = self.env['account.analytic.line'] - invoices = [] - data = {} if data is None else data - # use key (partner/account, company, currency) - # creates one invoice per key - invoice_grouping = {} - # prepare for iteration on journal and accounts - for line in self: - key = (line.other_partner_id or line.account_id.partner_id, - line.account_id.company_id, - line.account_id.pricelist_id.currency_id) - invoice_grouping.setdefault(key, []).append(line) - for (partner, company, currency), analytic_lines in \ - invoice_grouping.items(): - account = analytic_lines[0].account_id - if not partner or not currency: - raise exceptions.Warning( - _('Contract incomplete. Please fill in the Customer and ' - 'Pricelist fields for %s.') % account.name) - curr_invoice = self._prepare_cost_invoice( - partner, company.id, currency.id, analytic_lines) - invoice_context = dict( - self.env.context, lang=partner.lang, - # set force_company in context so the correct product - # properties are selected (eg. income account) - force_company=company.id, - # set company_id in context, so the correct default journal - # will be selected - company_id=company.id) - obj = invoice_model.with_context(invoice_context) - last_invoice = obj.create(curr_invoice) - invoices.append(last_invoice.id) - # use key (product, uom, user, invoiceable, analytic account, - # journal type) creates one invoice line per key - invoice_lines_grouping = {} - for analytic_line in analytic_lines: - if not analytic_line.to_invoice: - raise exceptions.Warning( - _('Trying to invoice non invoiceable line for %s.') % - analytic_line.product_id.name) - key = (analytic_line.product_id.id, - analytic_line.product_uom_id.id, - analytic_line.user_id.id, - analytic_line.to_invoice.id, - analytic_line.account_id, - analytic_line.journal_id.type) - # We want to retrieve the data in the partner language for - # the invoice creation - obj = analytic_line_model.with_context(invoice_context) - invoice_lines_grouping.setdefault(key, []).append( - obj.browse(analytic_line.id)) - # finally creates the invoice lines - for ((product_id, uom, user_id, factor_id, account, journal_type), - lines_to_invoice) in invoice_lines_grouping.items(): - obj = self.with_context(invoice_context) - invoice_line_vals = obj._prepare_cost_invoice_line( - last_invoice.id, product_id, uom, user_id, factor_id, - account, lines_to_invoice, journal_type, data) - invoice_line_model.create(invoice_line_vals) - analytic_lines = analytic_line_model.browse( - [l.id for l in analytic_lines]) - analytic_lines.write({'invoice_id': last_invoice.id}) - last_invoice.button_reset_taxes() - return invoices diff --git a/analytic_partner_hr_timesheet_invoice/models/sale_order_line.py b/analytic_partner_hr_timesheet_invoice/models/sale_order_line.py new file mode 100644 index 0000000000..cfd9f97af2 --- /dev/null +++ b/analytic_partner_hr_timesheet_invoice/models/sale_order_line.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Tecnativa, S.L. - Pedro M. Baeza, Luis M. Ontalba +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +from odoo import api, models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + @api.multi + def _compute_analytic(self, domain=None): + """Split by partner of the lines to filter only affected analytic + lines. + """ + for partner in self.mapped('order_partner_id'): + so_lines = self.filtered(lambda x: x.order_partner_id == partner) + if not domain: + # To filter on analyic lines linked to an expense + expense_type_id = self.env.ref( + 'account.data_account_type_expenses', + raise_if_not_found=False) + expense_type_id = expense_type_id and expense_type_id.id + domain = [ + ('so_line', 'in', self.ids), + '|', + ('amount', '<', 0), + '&', + ('amount', '=', 0), + '|', + ('move_id', '=', False), + ('move_id.account_id.user_type_id', '=', expense_type_id) + ] + partner_domain = list(domain) # make a copy of the domain + partner_domain += [ + '|', + ('other_partner_id', '=', partner.id), + ('other_partner_id', '=', False), + '|', + ('partner_id', '=', partner.id), + ('partner_id', '=', False), + ] + super(SaleOrderLine, so_lines)._compute_analytic( + domain=partner_domain, + ) diff --git a/analytic_partner_hr_timesheet_invoice/tests/__init__.py b/analytic_partner_hr_timesheet_invoice/tests/__init__.py index 356b98fafa..6d212e3fa1 100644 --- a/analytic_partner_hr_timesheet_invoice/tests/__init__.py +++ b/analytic_partner_hr_timesheet_invoice/tests/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- -# (c) 2015 Antiun Ingeniería S.L. - Pedro M. Baeza # License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + from . import test_analytic_partner_hr_timesheet_invoice diff --git a/analytic_partner_hr_timesheet_invoice/tests/test_analytic_partner_hr_timesheet_invoice.py b/analytic_partner_hr_timesheet_invoice/tests/test_analytic_partner_hr_timesheet_invoice.py index 1239cd9521..81207ef4fe 100644 --- a/analytic_partner_hr_timesheet_invoice/tests/test_analytic_partner_hr_timesheet_invoice.py +++ b/analytic_partner_hr_timesheet_invoice/tests/test_analytic_partner_hr_timesheet_invoice.py @@ -1,57 +1,76 @@ # -*- coding: utf-8 -*- -# (c) 2015 Antiun Ingeniería S.L. - Pedro M. Baeza +# Copyright 2017 Tecnativa, S.L. - Luis M. Ontalba # License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html -import openerp.tests.common as common -from openerp import fields +import odoo.tests.common as common +from odoo import fields +from datetime import timedelta, datetime -class TestAnalyticPartnerHrTimesheetInvoice(common.TransactionCase): - def setUp(self): - super(TestAnalyticPartnerHrTimesheetInvoice, self).setUp() - self.partner = self.env.ref('base.res_partner_1') - self.other_partner = self.env.ref('base.res_partner_3') - self.analytic_account = self.env['account.analytic.account'].create( - {'name': 'Test Analytic Account', - 'state': 'draft', - 'partner_id': self.partner.id, - 'pricelist_id': self.env.ref('product.list0').id, - 'to_invoice': self.env.ref( - 'hr_timesheet_invoice.timesheet_invoice_factor1').id, - 'type': 'normal'} - ) - self.analytic_line_model = self.env['account.analytic.line'] - self.analytic_line_1 = self.analytic_line_model.create( - {'name': 'Test for other partner', - 'account_id': self.analytic_account.id, - 'unit_amount': 1.0, - 'amount': 100.0, - 'date': fields.Date.today(), - 'general_account_id': self.env.ref('account.a_recv').id, - 'journal_id': self.env.ref('account.analytic_journal_sale').id, - 'to_invoice': self.analytic_account.to_invoice.id, - 'other_partner_id': self.other_partner.id}) - self.analytic_line_2 = self.analytic_line_model.create( - {'name': 'Test for account partner', - 'account_id': self.analytic_account.id, - 'unit_amount': 1.0, - 'amount': 200.0, - 'date': fields.Date.today(), - 'general_account_id': self.env.ref('account.a_recv').id, - 'journal_id': self.env.ref('account.analytic_journal_sale').id, - 'to_invoice': self.analytic_account.to_invoice.id}) +class TestAnalyticPartnerHrTimesheetInvoice(common.SavepointCase): - def test_invoices_split_by_partner(self): - lines = self.analytic_line_1 + self.analytic_line_2 - invoices = lines.invoice_cost_create() - self.assertEqual(len(invoices), 2, "Invoices has not been split") + @classmethod + def setUpClass(cls): + super(TestAnalyticPartnerHrTimesheetInvoice, cls).setUpClass() + cls.order_partner = cls.env['res.partner'].create({ + 'name': 'Test Order Partner', + }) + cls.category = cls.env['product.category'].create({ + 'name': 'Test Product Category', + }) + cls.product = cls.env['product.product'].create({ + 'name': 'Test Product', + 'sale_ok': True, + 'type': 'service', + 'categ_id': cls.category.id, + 'invoice_policy': 'order', + 'track_service': 'task', + }) + cls.sale_order_vals = [ + (0, 0, { + 'product_id': cls.product.id, + 'name': 'Test Sale order Line', + 'product_uom_qty': 100.0, + 'price_unit': 50.00, + }) + ] + cls.sale_order = cls.env['sale.order'].create({ + 'name': 'Test Sale Order', + 'partner_id': cls.order_partner.id, + 'order_line': cls.sale_order_vals, + }) + cls.sale_order.action_confirm() + cls.task = cls.env['project.task'].search( + [('sale_line_id.id', '=', cls.sale_order.order_line[0].id)]) + cls.date_time = fields.Datetime.to_string( + datetime.now() - timedelta(hours=1)) + cls.user = cls.env.user + cls.analytic_account = cls.env['account.analytic.account'].create({ + 'name': 'Test Analytic Account', + }) + cls.analytic_line_1 = cls.env['account.analytic.line'].create({ + 'date': cls.date_time, + 'user_id': cls.env.user.id, + 'name': 'Test Analytic line 1', + 'account_id': cls.analytic_account.id, + 'task_id': cls.task.id, + 'unit_amount': 10.0, + }) + cls.analytic_line_2 = cls.env['account.analytic.line'].create({ + 'date': cls.date_time, + 'user_id': cls.env.user.id, + 'name': 'Test Analytic line 2', + 'account_id': cls.analytic_account.id, + 'task_id': cls.task.id, + 'unit_amount': 10.0, + }) - def test_partner_invoice_1(self): - invoice_id = self.analytic_line_1.invoice_cost_create()[0] + def test_global(self): + self.analytic_line_1.other_partner_id = self.order_partner.id + self.analytic_line_1.partner_id = False + self.analytic_line_2.other_partner_id = False + self.analytic_line_1.partner_id = self.order_partner.id + invoice_id = self.sale_order.action_invoice_create() invoice = self.env['account.invoice'].browse(invoice_id) - self.assertEqual(invoice.partner_id, self.other_partner) - - def test_partner_invoice_2(self): - invoice_id = self.analytic_line_2.invoice_cost_create()[0] - invoice = self.env['account.invoice'].browse(invoice_id) - self.assertEqual(invoice.partner_id, self.partner) + self.assertEqual(invoice.partner_id, self.order_partner) + self.assertEqual(invoice.amount_untaxed, 5000)