Merge branch 'master-upstream-patch-16102021' into 'master'

[PATCH] Upstream patch - 16102021

See merge request flectra-hq/flectra!463
This commit is contained in:
Parthiv Patel
2021-10-17 04:50:23 +00:00
23 changed files with 1676 additions and 637 deletions
+3 -3
View File
@@ -4,13 +4,13 @@
<!-- Account Tax Group -->
<record id="tax_group_0" model="account.tax.group">
<field name="name">Thuế GTGT 0%</field>
<field name="name">VAT 0%</field>
</record>
<record id="tax_group_5" model="account.tax.group">
<field name="name">Thuế GTGT 5%</field>
<field name="name">VAT 5%</field>
</record>
<record id="tax_group_10" model="account.tax.group">
<field name="name">Thuế GTGT 10%</field>
<field name="name">VAT 10%</field>
</record>
</data>
+12 -12
View File
@@ -4,8 +4,8 @@
<!-- for purchase -->
<record id="tax_purchase_vat10" model="account.tax.template">
<field name="chart_template_id" ref="vn_template"/>
<field name="name">Thuế GTGT được khấu trừ 10%</field>
<field name="description">Thuế GTGT được khấu trừ 10%</field>
<field name="name">Deductible VAT 10%</field>
<field name="description">Deductible VAT 10%</field>
<field name="amount" eval="10"/>
<field name="amount_type">percent</field>
<field name="type_tax_use">purchase</field>
@@ -39,8 +39,8 @@
</record>
<record id="tax_purchase_vat5" model="account.tax.template">
<field name="chart_template_id" ref="vn_template"/>
<field name="name">Thuế GTGT được khấu trừ 5%</field>
<field name="description">Thuế GTGT được khấu trừ 5%</field>
<field name="name">Deductible VAT 5%</field>
<field name="description">Deductible VAT 5%</field>
<field name="amount" eval="5"/>
<field name="amount_type">percent</field>
<field name="type_tax_use">purchase</field>
@@ -74,8 +74,8 @@
</record>
<record id="tax_purchase_vat0" model="account.tax.template">
<field name="chart_template_id" ref="vn_template"/>
<field name="name">Thuế GTGT được khấu trừ 0%</field>
<field name="description">Thuế GTGT được khấu trừ 0%</field>
<field name="name">Deductible VAT 0%</field>
<field name="description">Deductible VAT 0%</field>
<field name="amount" eval="0"/>
<field name="amount_type">percent</field>
<field name="type_tax_use">purchase</field>
@@ -107,8 +107,8 @@
<!-- for sale -->
<record id="tax_sale_vat10" model="account.tax.template">
<field name="chart_template_id" ref="vn_template"/>
<field name="name">Thuế GTGT phải nộp 10%</field>
<field name="description">Thuế GTGT phải nộp 10%</field>
<field name="name">Value Added Tax (VAT) 10%</field>
<field name="description">Value Added Tax (VAT) 10%</field>
<field name="amount" eval="10"/>
<field name="amount_type">percent</field>
<field name="type_tax_use">sale</field>
@@ -142,8 +142,8 @@
</record>
<record id="tax_sale_vat5" model="account.tax.template">
<field name="chart_template_id" ref="vn_template"/>
<field name="name">Thuế GTGT phải nộp 5%</field>
<field name="description">Thuế GTGT phải nộp 5%</field>
<field name="name">Value Added Tax (VAT) 5%</field>
<field name="description">Value Added Tax (VAT) 5%</field>
<field name="amount" eval="5"/>
<field name="amount_type">percent</field>
<field name="type_tax_use">sale</field>
@@ -177,8 +177,8 @@
</record>
<record id="tax_sale_vat0" model="account.tax.template">
<field name="chart_template_id" ref="vn_template"/>
<field name="name">Thuế GTGT phải nộp 0%</field>
<field name="description">Thuế GTGT phải nộp 0%</field>
<field name="name">Value Added Tax (VAT) 0%</field>
<field name="description">Value Added Tax (VAT) 0%</field>
<field name="amount" eval="0"/>
<field name="amount_type">percent</field>
<field name="type_tax_use">sale</field>
+30 -30
View File
@@ -7,136 +7,136 @@
</record>
<record id="account_tax_report_line_01_vn" model="account.tax.report.line">
<field name="name">HHDV mua vào</field>
<field name="name">Purchase of Goods and Services</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="1"/>
</record>
<record id="account_tax_report_line_01_01_vn" model="account.tax.report.line">
<field name="name">Thuế GTGT HHDV mua vào</field>
<field name="name">VAT on purchase of goods and services</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="1"/>
<field name="parent_id" ref="account_tax_report_line_01_vn"/>
</record>
<record id="account_tax_report_line_01_01_01_vn" model="account.tax.report.line">
<field name="name">Thuế GTGT HHDV mua vào chịu thuế 0%</field>
<field name="tag_name">Thuế GTGT HHDV mua vào chịu thuế 0%</field>
<field name="name">VAT on purchase of goods and services 0%</field>
<field name="tag_name">VAT on purchase of goods and services 0%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="1"/>
<field name="parent_id" ref="account_tax_report_line_01_01_vn"/>
</record>
<record id="account_tax_report_line_02_01_01_vn" model="account.tax.report.line">
<field name="name">Thuế GTGT HHDV mua vào chịu thuế 5%</field>
<field name="tag_name">Thuế GTGT HHDV mua vào chịu thuế 5%</field>
<field name="name">VAT on purchase of goods and services 5%</field>
<field name="tag_name">VAT on purchase of goods and services 5%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="2"/>
<field name="parent_id" ref="account_tax_report_line_01_01_vn"/>
</record>
<record id="account_tax_report_line_03_01_01_vn" model="account.tax.report.line">
<field name="name">Thuế GTGT HHDV mua vào chịu thuế 10%</field>
<field name="tag_name">Thuế GTGT HHDV mua vào chịu thuế 10%</field>
<field name="name">VAT on purchase of goods and services 10%</field>
<field name="tag_name">VAT on purchase of goods and services 10%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="3"/>
<field name="parent_id" ref="account_tax_report_line_01_01_vn"/>
</record>
<record id="account_tax_report_line_02_01_vn" model="account.tax.report.line">
<field name="name">Giá trị HHDV mua vào</field>
<field name="name">Untaxed Purchase of Goods and Services</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="2"/>
<field name="parent_id" ref="account_tax_report_line_01_vn"/>
</record>
<record id="account_tax_report_line_01_02_01_vn" model="account.tax.report.line">
<field name="name">Giá trị HHDV mua vào chịu thuế 0%</field>
<field name="tag_name">Giá trị HHDV mua vào chịu thuế 0%</field>
<field name="name">Untaxed Purchase of Goods and Services taxed 0%</field>
<field name="tag_name">Untaxed Purchase of Goods and Services taxed 0%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="1"/>
<field name="parent_id" ref="account_tax_report_line_02_01_vn"/>
</record>
<record id="account_tax_report_line_02_02_01_vn" model="account.tax.report.line">
<field name="name">Giá trị HHDV mua vào chịu thuế 5%</field>
<field name="tag_name">Giá trị HHDV mua vào chịu thuế 5%</field>
<field name="name">Untaxed Purchase of Goods and Services taxed 5%</field>
<field name="tag_name">Untaxed Purchase of Goods and Services taxed 5%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="2"/>
<field name="parent_id" ref="account_tax_report_line_02_01_vn"/>
</record>
<record id="account_tax_report_line_03_02_01_vn" model="account.tax.report.line">
<field name="name">Giá trị HHDV mua vào chịu thuế 10%</field>
<field name="tag_name">Giá trị HHDV mua vào chịu thuế 10%</field>
<field name="name">Untaxed Purchase of Goods and Services taxed 10%</field>
<field name="tag_name">Untaxed Purchase of Goods and Services taxed 10%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="3"/>
<field name="parent_id" ref="account_tax_report_line_02_01_vn"/>
</record>
<record id="account_tax_report_line_02_vn" model="account.tax.report.line">
<field name="name">HHDV bán ra</field>
<field name="name">Sales of Goods and Services</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="2"/>
</record>
<record id="account_tax_report_line_01_02_vn" model="account.tax.report.line">
<field name="name">Thuế GTGT HHDV bán ra</field>
<field name="name">VAT on sales of goods and services</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="1"/>
<field name="parent_id" ref="account_tax_report_line_02_vn"/>
</record>
<record id="account_tax_report_line_01_01_02_vn" model="account.tax.report.line">
<field name="name">Thuế GTGT HHDV bán ra chịu thuế 0%</field>
<field name="tag_name">Thuế GTGT HHDV bán ra chịu thuế 0%</field>
<field name="name">VAT on sales of goods and services 0%</field>
<field name="tag_name">VAT on sales of goods and services 0%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="1"/>
<field name="parent_id" ref="account_tax_report_line_01_02_vn"/>
</record>
<record id="account_tax_report_line_02_01_02_vn" model="account.tax.report.line">
<field name="name">Thuế GTGT HHDV bán ra chịu thuế 5%</field>
<field name="tag_name">Thuế GTGT HHDV bán ra chịu thuế 5%</field>
<field name="name">VAT on sales of goods and services 5%</field>
<field name="tag_name">VAT on sales of goods and services 5%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="2"/>
<field name="parent_id" ref="account_tax_report_line_01_02_vn"/>
</record>
<record id="account_tax_report_line_03_01_02_vn" model="account.tax.report.line">
<field name="name">Thuế GTGT HHDV bán ra chịu thuế 10%</field>
<field name="tag_name">Thuế GTGT HHDV bán ra chịu thuế 10%</field>
<field name="name">VAT on sales of goods and services 10%</field>
<field name="tag_name">VAT on sales of goods and services 10%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="3"/>
<field name="parent_id" ref="account_tax_report_line_01_02_vn"/>
</record>
<record id="account_tax_report_line_02_02_vn" model="account.tax.report.line">
<field name="name">Giá trị HHDV bán ra</field>
<field name="name">Untaxed Sales of Goods and Services</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="2"/>
<field name="parent_id" ref="account_tax_report_line_02_vn"/>
</record>
<record id="account_tax_report_line_01_02_02_vn" model="account.tax.report.line">
<field name="name">Giá trị HHDV bán ra chịu thuế 0%</field>
<field name="tag_name">Giá trị HHDV bán ra chịu thuế 0%</field>
<field name="name">Untaxed sales of goods and services taxed 0%</field>
<field name="tag_name">Untaxed sales of goods and services taxed 0%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="1"/>
<field name="parent_id" ref="account_tax_report_line_02_02_vn"/>
</record>
<record id="account_tax_report_line_02_02_02_vn" model="account.tax.report.line">
<field name="name">Giá trị HHDV bán ra chịu thuế 5%</field>
<field name="tag_name">Giá trị HHDV bán ra chịu thuế 5%</field>
<field name="name">Untaxed sales of goods and services taxed 5%</field>
<field name="tag_name">Untaxed sales of goods and services taxed 5%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="2"/>
<field name="parent_id" ref="account_tax_report_line_02_02_vn"/>
</record>
<record id="account_tax_report_line_03_02_02_vn" model="account.tax.report.line">
<field name="name">Giá trị HHDV bán ra chịu thuế 10%</field>
<field name="tag_name">Giá trị HHDV bán ra chịu thuế 10%</field>
<field name="name">Untaxed sales of goods and services taxed 10%</field>
<field name="tag_name">Untaxed sales of goods and services taxed 10%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="3"/>
<field name="parent_id" ref="account_tax_report_line_02_02_vn"/>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -395,7 +395,7 @@ class StockMove(models.Model):
# Then, we collect every relevant moves related to a specific component
# to know how many are considered delivered.
uom_qty_per_kit = bom_line_data['qty'] / bom_line_data['original_qty']
qty_per_kit = bom_line.product_uom_id._compute_quantity(uom_qty_per_kit, bom_line.product_id.uom_id)
qty_per_kit = bom_line.product_uom_id._compute_quantity(uom_qty_per_kit, bom_line.product_id.uom_id, round=False)
if not qty_per_kit:
continue
incoming_moves = bom_line_moves.filtered(filters['incoming_moves'])
+2
View File
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from random import randint
from datetime import datetime
from flectra import fields, tools
from flectra.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
@@ -272,6 +273,7 @@ class TestPoSCommon(ValuationReconciliationTestCommon):
cls.env['res.currency.rate'].create({
'rate': 0.5,
'currency_id': cls.other_currency.id,
'name': datetime.today().date(),
})
other_cash_journal = cls.env['account.journal'].create({
'name': 'Cash Other',
+2 -2
View File
@@ -40,8 +40,8 @@
<attribute name="invisible">0</attribute>
</xpath>
<group name="purchase" position="before">
<field name="seller_ids" context="{'default_product_tmpl_id':context.get('product_tmpl_id',active_id), 'product_template_invisible_variant': True, 'tree_view_ref':'purchase.product_supplierinfo_tree_view2'}" nolabel="1" attrs="{'invisible': [('product_variant_count','&gt;',1)]}"/>
<field name="variant_seller_ids" context="{'default_product_tmpl_id': context.get('product_tmpl_id', active_id), 'tree_view_ref':'purchase.product_supplierinfo_tree_view2'}" nolabel="1" attrs="{'invisible': [('product_variant_count','&lt;=',1)]}"/>
<field name="seller_ids" context="{'default_product_tmpl_id':context.get('product_tmpl_id',active_id), 'product_template_invisible_variant': True, 'tree_view_ref':'purchase.product_supplierinfo_tree_view2'}" nolabel="1" attrs="{'invisible': [('product_variant_count','&gt;',1)], 'readonly': [('product_variant_count','&gt;',1)]}"/>
<field name="variant_seller_ids" context="{'default_product_tmpl_id': context.get('product_tmpl_id', active_id), 'tree_view_ref':'purchase.product_supplierinfo_tree_view2'}" nolabel="1" attrs="{'invisible': [('product_variant_count','&lt;=',1)], 'readonly': [('product_variant_count','&lt;=',1)]}"/>
</group>
<group name="bill" position="attributes">
<attribute name="groups">purchase.group_purchase_manager</attribute>
@@ -296,3 +296,50 @@ class TestSaleMrpKitBom(TransactionCase):
purchase_price = line.product_id.with_company(line.company_id)._compute_average_price(0, line.product_uom_qty, line.move_ids)
self.assertEqual(purchase_price, 92, "The purchase price must be the total cost of the components multiplied by their unit of measure")
def test_qty_delivered_with_bom(self):
"""Check the quantity delivered, when a bom line has a non integer quantity"""
self.env['ir.model.data'].xmlid_to_object('product.decimal_product_uom').digits = 5
self.kit = self._create_product('Kit', 'product', 0.00)
self.comp = self._create_product('Component', 'product', 0.00)
# Create BoM for Kit
bom_product_form = Form(self.env['mrp.bom'])
bom_product_form.product_id = self.kit
bom_product_form.product_tmpl_id = self.kit.product_tmpl_id
bom_product_form.product_qty = 1.0
bom_product_form.type = 'phantom'
with bom_product_form.bom_line_ids.new() as bom_line:
bom_line.product_id = self.comp
bom_line.product_qty = 0.08600
self.bom = bom_product_form.save()
self.customer = self.env['res.partner'].create({
'name': 'customer',
})
so = self.env['sale.order'].create({
'partner_id': self.customer.id,
'order_line': [
(0, 0, {
'name': self.kit.name,
'product_id': self.kit.id,
'product_uom_qty': 10.0,
'product_uom': self.kit.uom_id.id,
'price_unit': 1,
'tax_id': False,
})],
})
so.action_confirm()
self.assertTrue(so.picking_ids)
self.assertEqual(so.order_line.qty_delivered, 0)
picking = so.picking_ids
picking.move_lines.quantity_done = 0.86000
picking.button_validate()
# Checks the delivery amount (must be 10).
self.assertEqual(so.order_line.qty_delivered, 10)
+2 -1
View File
@@ -162,7 +162,8 @@ class SaleOrder(models.Model):
@api.onchange('user_id')
def onchange_user_id(self):
super().onchange_user_id()
self.warehouse_id = self.user_id.with_company(self.company_id.id)._get_default_warehouse_id().id
if self.state in ['draft','sent']:
self.warehouse_id = self.user_id.with_company(self.company_id.id)._get_default_warehouse_id().id
@api.onchange('partner_shipping_id')
def _onchange_partner_shipping_id(self):
+13 -1
View File
@@ -202,10 +202,11 @@ class ProjectTask(models.Model):
# override sale_order_id and make it computed stored field instead of regular field.
sale_order_id = fields.Many2one(compute='_compute_sale_order_id', store=True, readonly=False,
domain="['|', '|', ('partner_id', '=', partner_id), ('partner_id', 'child_of', commercial_partner_id), ('partner_id', 'parent_of', partner_id)]")
# content in related parameter of the field definition is removed to manually define the compute and the search method.
project_sale_order_id = fields.Many2one(compute='_compute_project_sale_order_id', search='_search_project_sale_order_id', related=None)
analytic_account_id = fields.Many2one('account.analytic.account', related='sale_order_id.analytic_account_id')
bill_type = fields.Selection(related="project_id.bill_type")
pricing_type = fields.Selection(related="project_id.pricing_type")
sale_line_id = fields.Many2one(domain="[('company_id', '=', company_id), ('is_service', '=', True), ('order_partner_id', 'child_of', commercial_partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done']), '|', ('project_id.bill_type', '=', 'customer_task'), ('order_id', '=?', project_sale_order_id)]")
is_project_map_empty = fields.Boolean("Is Project map empty", compute='_compute_is_project_map_empty')
has_multi_sol = fields.Boolean(compute='_compute_has_multi_sol', compute_sudo=True)
allow_billable = fields.Boolean(related="project_id.allow_billable")
@@ -271,6 +272,14 @@ class ProjectTask(models.Model):
for task in self:
task.analytic_account_active = task.analytic_account_active or task.analytic_account_id.active
@api.depends('project_id.bill_type', 'project_id.sale_order_id')
def _compute_project_sale_order_id(self):
for task in self:
if task.bill_type != 'customer_task':
task.project_sale_order_id = task.project_id.sale_order_id
else:
task.project_sale_order_id = False
@api.depends('sale_line_id', 'project_id', 'allow_billable', 'non_allow_billable')
def _compute_sale_order_id(self):
for task in self:
@@ -309,6 +318,9 @@ class ProjectTask(models.Model):
if not self.sale_line_id:
self.sale_line_id = self.project_id.sale_line_id
def _search_project_sale_order_id(self, operator, value):
return [('bill_type', '!=', 'customer_task'), ('project_id.sale_order_id', operator, value)]
def write(self, values):
res = super(ProjectTask, self).write(values)
# Done after super to avoid constraints on field recomputation
@@ -101,9 +101,10 @@ class Project(models.Model):
'amount_untaxed_to_invoice': 'to_invoice',
'timesheet_cost': 'cost',
'expense_cost': 'expense_cost',
'expense_amount_untaxed_invoiced': 'expense_amount_untaxed_invoiced',
'expense_amount_untaxed_invoiced': 'expense_amount_untaxed_invoiced',
'expense_amount_untaxed_to_invoice': 'expense_amount_untaxed_to_invoice',
'other_revenues': 'other_revenues'
}
}
profit = dict.fromkeys(list(field_map.values()) + ['other_revenues', 'total'], 0.0)
profitability_raw_data = self.env['project.profitability.report'].read_group([('project_id', 'in', self.ids)], ['project_id'] + list(field_map), ['project_id'])
for data in profitability_raw_data:
@@ -132,7 +132,6 @@ class ProfitabilityAnalysis(models.Model):
SUM(timesheet_cost) AS timesheet_cost,
SUM(expense_cost) AS expense_cost,
SUM(other_revenues) AS other_revenues,
SUM(downpayment_invoiced) AS downpayment_invoiced,
SUM(expense_amount_untaxed_to_invoice) AS expense_amount_untaxed_to_invoice,
SUM(expense_amount_untaxed_invoiced) AS expense_amount_untaxed_invoiced,
SUM(amount_untaxed_to_invoice) AS amount_untaxed_to_invoice,
@@ -148,7 +147,6 @@ class ProfitabilityAnalysis(models.Model):
TS.amount AS timesheet_cost,
0.0 AS other_revenues,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced,
0.0 AS expense_amount_untaxed_to_invoice,
0.0 AS expense_amount_untaxed_invoiced,
0.0 AS amount_untaxed_to_invoice,
@@ -159,7 +157,7 @@ class ProfitabilityAnalysis(models.Model):
UNION ALL
-- Get the other revenues
-- Get the other revenues (products that are not services)
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
@@ -168,7 +166,6 @@ class ProfitabilityAnalysis(models.Model):
0.0 AS timesheet_cost,
AAL.amount AS other_revenues,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced,
0.0 AS expense_amount_untaxed_to_invoice,
0.0 AS expense_amount_untaxed_invoiced,
0.0 AS amount_untaxed_to_invoice,
@@ -177,16 +174,12 @@ class ProfitabilityAnalysis(models.Model):
FROM project_project P
JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
JOIN account_analytic_line AAL ON AAL.account_id = AA.id
JOIN product_product PP ON PP.id = AAL.product_id
JOIN product_template PT ON PT.id = PP.product_tmpl_id
LEFT JOIN sale_order_line_invoice_rel SOINV ON SOINV.invoice_line_id = AAL.move_id
LEFT JOIN sale_order_line SOL ON SOINV.order_line_id = SOL.id
WHERE AAL.amount > 0.0 AND AAL.project_id IS NULL AND P.active = 't'
AND P.allow_timesheets = 't'
AND PT.service_type = 'manual' -- default value or Milestone service for services products
AND PT.service_tracking = 'no' -- default value or not a tracking service for services products
AND (SOL.id IS NULL
OR (SOL.is_expense IS NOT TRUE AND SOL.is_downpayment IS NOT TRUE))
OR (SOL.is_expense IS NOT TRUE AND SOL.is_downpayment IS NOT TRUE AND SOL.is_service IS NOT TRUE))
UNION ALL
@@ -199,64 +192,32 @@ class ProfitabilityAnalysis(models.Model):
0.0 AS timesheet_cost,
0.0 AS other_revenues,
AAL.amount AS expense_cost,
0.0 AS downpayment_invoiced,
0.0 AS expense_amount_untaxed_to_invoice,
0.0 AS expense_amount_untaxed_invoiced,
0.0 AS amount_untaxed_to_invoice,
0.0 AS amount_untaxed_invoiced,
AAL.date AS line_date
FROM project_project P
LEFT JOIN account_analytic_account AA ON P.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 NULL AND P.active = 't' AND P.allow_timesheets = 't'
UNION ALL
-- Get the invoiced downpayments
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
MY_SOLS.id AS sale_line_id,
0.0 AS timesheet_unit_amount,
0.0 AS timesheet_cost,
0.0 AS other_revenues,
0.0 AS expense_cost,
CASE WHEN MY_SOLS.invoice_status = 'invoiced' THEN MY_SOLS.price_reduce ELSE 0.0 END AS downpayment_invoiced,
0.0 AS expense_amount_untaxed_to_invoice,
0.0 AS expense_amount_untaxed_invoiced,
0.0 AS amount_untaxed_to_invoice,
0.0 AS amount_untaxed_invoiced,
MY_S.date_order AS line_date
FROM project_project P
LEFT JOIN sale_order_line MY_SOL ON P.sale_line_id = MY_SOL.id
LEFT JOIN sale_order MY_S ON MY_SOL.order_id = MY_S.id
LEFT JOIN sale_order_line MY_SOLS ON MY_SOLS.order_id = MY_S.id
WHERE MY_SOLS.is_downpayment = 't'
UNION ALL
-- Get the expense costs from sale order line
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
OLIS.id AS sale_line_id,
0.0 AS timesheet_unit_amount,
0.0 AS timesheet_cost,
0.0 AS other_revenues,
OLIS.price_reduce AS expense_cost,
0.0 AS downpayment_invoiced,
0.0 AS expense_amount_untaxed_to_invoice,
0.0 AS expense_amount_untaxed_invoiced,
0.0 AS amount_untaxed_to_invoice,
0.0 AS amount_untaxed_invoiced,
ANLI.date AS line_date
FROM project_project P
LEFT JOIN account_analytic_account ANAC ON P.analytic_account_id = ANAC.id
LEFT JOIN account_analytic_line ANLI ON ANAC.id = ANLI.account_id
LEFT JOIN sale_order_line OLI ON P.sale_line_id = OLI.id
LEFT JOIN sale_order ORD ON OLI.order_id = ORD.id
LEFT JOIN sale_order_line OLIS ON ORD.id = OLIS.order_id
WHERE OLIS.product_id = ANLI.product_id AND OLIS.is_downpayment = 't' AND ANLI.amount < 0.0 AND ANLI.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't'
JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
JOIN account_analytic_line AAL ON AAL.account_id = AA.id
LEFT JOIN account_move_line RINVL ON AAL.move_id = RINVL.id
AND RINVL.parent_state = 'posted'
AND RINVL.exclude_from_invoice_tab = 'f'
-- Check if the AAL is not related to a reversed credit note
LEFT JOIN account_move RINV ON RINV.id = RINVL.move_id
LEFT JOIN account_move_line INVL ON INVL.move_id = RINV.reversed_entry_id
AND INVL.parent_state = 'posted'
AND INVL.exclude_from_invoice_tab = 'f'
AND INVL.product_id = RINVL.product_id
LEFT JOIN sale_order_line_invoice_rel SOINV ON SOINV.invoice_line_id = INVL.id
LEFT JOIN sale_order_line SOL ON SOINV.order_line_id = SOL.id
AND SOL.product_id = AAL.product_id
-- Check if the AAL is not related to a consumed downpayment (when the SOL is fully invoiced - with downpayment discounted.)
LEFT JOIN sale_order_line_invoice_rel SOINVDOWN ON SOINVDOWN.invoice_line_id = RINVL.id
LEFT JOIN sale_order_line SOLDOWN on SOINVDOWN.order_line_id = SOLDOWN.id AND SOLDOWN.is_downpayment = 't'
WHERE AAL.amount < 0.0 AND AAL.project_id IS NULL
AND SOL.id IS NULL AND SOLDOWN.id IS NULL -- Not linked to a credit note and not a downpayment
AND P.active = 't' AND P.allow_timesheets = 't'
UNION ALL
@@ -270,17 +231,16 @@ class ProfitabilityAnalysis(models.Model):
0.0 AS timesheet_cost,
0.0 AS other_revenues,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced,
CASE
WHEN SOL.qty_delivered_method = 'analytic' THEN (SOL.untaxed_amount_to_invoice / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
ELSE 0.0
END AS expense_amount_untaxed_to_invoice,
CASE
WHEN SOL.qty_delivered_method = 'analytic' AND SOL.invoice_status != 'no'
WHEN SOL.qty_delivered_method = 'analytic' AND SOL.invoice_status = 'invoiced'
THEN
CASE
WHEN T.expense_policy = 'sales_price'
THEN (SOL.price_reduce / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END) * SOL.qty_invoiced
THEN (SOL.untaxed_amount_invoiced / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
ELSE -AMOUNT_UNTAXED.expense_cost
END
ELSE 0.0
@@ -290,31 +250,80 @@ class ProfitabilityAnalysis(models.Model):
ELSE 0.0
END AS amount_untaxed_to_invoice,
CASE
WHEN SOL.qty_delivered_method IN ('timesheet', 'manual') THEN (COALESCE(SOL.untaxed_amount_invoiced, AMOUNT_UNTAXED.downpayment_invoiced) / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
WHEN SOL.qty_delivered_method IN ('timesheet', 'manual') THEN (SOL.untaxed_amount_invoiced / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
ELSE 0.0
END AS amount_untaxed_invoiced,
S.date_order AS line_date
FROM project_project P
JOIN res_company C ON C.id = P.company_id
LEFT JOIN (
-- Gets SOL linked to timesheets
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
AAL.so_line AS sale_line_id,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced
0.0 AS expense_cost
FROM account_analytic_line AAL, project_project P
WHERE AAL.project_id IS NOT NULL AND P.id = AAL.project_id AND P.active = 't'
GROUP BY P.id, AAL.so_line
UNION
-- Service SOL linked to a project task AND not yet timesheeted
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
SOL.id AS sale_line_id,
0.0 AS expense_cost
FROM sale_order_line SOL
JOIN project_task T ON T.sale_line_id = SOL.id
JOIN project_project P ON T.project_id = P.id
LEFT JOIN account_analytic_line AAL ON AAL.task_id = T.id
WHERE SOL.is_service = 't'
AND AAL.id IS NULL -- not timesheeted
AND P.active = 't' AND P.allow_timesheets = 't'
GROUP BY P.id, SOL.id
UNION
-- Service SOL linked to project AND not yet timesheeted
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
SOL.id AS sale_line_id,
0.0 AS expense_cost
FROM sale_order_line SOL
JOIN project_project P ON P.sale_line_id = SOL.id
LEFT JOIN account_analytic_line AAL ON AAL.project_id = P.id
LEFT JOIN project_task T ON T.sale_line_id = SOL.id
WHERE SOL.is_service = 't'
AND AAL.id IS NULL -- not timesheeted
AND (T.id IS NULL OR T.project_id != P.id) -- not linked to a task in this project
AND P.active = 't' AND P.allow_timesheets = 't'
GROUP BY P.id, SOL.id
UNION
-- Service SOL linked to analytic account AND not yet timesheeted
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
SOL.id AS sale_line_id,
0.0 AS expense_cost
FROM sale_order_line SOL
JOIN sale_order SO ON SO.id = SOL.order_id
JOIN account_analytic_account AA ON AA.id = SO.analytic_account_id
JOIN project_project P ON P.analytic_account_id = AA.id
LEFT JOIN project_project PSOL ON PSOL.sale_line_id = SOL.id
LEFT JOIN project_task TSOL ON TSOL.sale_line_id = SOL.id
LEFT JOIN account_analytic_line AAL ON AAL.so_line = SOL.id
WHERE SOL.is_service = 't'
AND AAL.id IS NULL -- not timesheeted
AND TSOL.id IS NULL -- not linked to a task
AND PSOL.id IS NULL -- not linked to a project
AND P.active = 't' AND P.allow_timesheets = 't'
GROUP BY P.id, SOL.id
UNION
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
AAL.so_line AS sale_line_id,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced
0.0 AS expense_cost
FROM project_project P
LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id
@@ -325,8 +334,7 @@ class ProfitabilityAnalysis(models.Model):
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
AAL.so_line AS sale_line_id,
SUM(AAL.amount) AS expense_cost,
0.0 AS downpayment_invoiced
SUM(AAL.amount) AS expense_cost
FROM project_project P
LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id
@@ -336,37 +344,31 @@ class ProfitabilityAnalysis(models.Model):
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
MY_SOLS.id AS sale_line_id,
0.0 AS expense_cost,
CASE WHEN MY_SOLS.invoice_status = 'invoiced' THEN MY_SOLS.price_reduce ELSE 0.0 END AS downpayment_invoiced
SOLDOWN.id AS sale_line_id,
0.0 AS expense_cost
FROM project_project P
LEFT JOIN sale_order_line MY_SOL ON P.sale_line_id = MY_SOL.id
LEFT JOIN sale_order MY_S ON MY_SOL.order_id = MY_S.id
LEFT JOIN sale_order_line MY_SOLS ON MY_SOLS.order_id = MY_S.id
WHERE MY_SOLS.is_downpayment = 't'
GROUP BY P.id, MY_SOLS.id
UNION
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
OLIS.id AS sale_line_id,
OLIS.price_reduce AS expense_cost,
0.0 AS downpayment_invoiced
FROM project_project P
LEFT JOIN account_analytic_account ANAC ON P.analytic_account_id = ANAC.id
LEFT JOIN account_analytic_line ANLI ON ANAC.id = ANLI.account_id
LEFT JOIN sale_order_line OLI ON P.sale_line_id = OLI.id
LEFT JOIN sale_order ORD ON OLI.order_id = ORD.id
LEFT JOIN sale_order_line OLIS ON ORD.id = OLIS.order_id
WHERE OLIS.product_id = ANLI.product_id AND OLIS.is_downpayment = 't' AND ANLI.amount < 0.0 AND ANLI.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't'
GROUP BY P.id, OLIS.id
LEFT JOIN sale_order_line SOL ON P.sale_line_id = SOL.id
LEFT JOIN sale_order SO ON SO.id = SOL.order_id OR SO.analytic_account_id = P.analytic_account_id
LEFT JOIN sale_order_line SOLDOWN ON SOLDOWN.order_id = SO.id AND SOLDOWN.is_downpayment = 't'
LEFT JOIN sale_order_line_invoice_rel SOINV ON SOINV.order_line_id = SOLDOWN.id
LEFT JOIN account_move_line INVL ON SOINV.invoice_line_id = INVL.id
AND INVL.parent_state = 'posted'
AND INVL.exclude_from_invoice_tab = 'f'
LEFT JOIN account_move RINV ON INVL.move_id = RINV.reversed_entry_id
LEFT JOIN account_move_line RINVL ON RINV.id = RINVL.move_id
AND RINVL.parent_state = 'posted'
AND RINVL.exclude_from_invoice_tab = 'f'
AND RINVL.product_id = SOLDOWN.product_id
LEFT JOIN account_analytic_line ANLI ON ANLI.move_id = RINVL.id AND ANLI.amount < 0.0
WHERE ANLI.id IS NULL -- there are no credit note for this downpayment
AND P.active = 't' AND P.allow_timesheets = 't'
GROUP BY P.id, SOLDOWN.id
UNION
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
SOL.id AS sale_line_id,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced
0.0 AS expense_cost
FROM sale_order_line SOL
INNER JOIN project_project P ON SOL.project_id = P.id
WHERE P.active = 't' AND P.allow_timesheets = 't'
@@ -375,8 +377,7 @@ class ProfitabilityAnalysis(models.Model):
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
SOL.id AS sale_line_id,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced
0.0 AS expense_cost
FROM sale_order_line SOL
INNER JOIN project_task T ON SOL.task_id = T.id
INNER JOIN project_project P ON P.id = T.project_id
+1
View File
@@ -2,6 +2,7 @@
# # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from . import common
from . import common_reporting
from . import test_sale_timesheet
from . import test_sale_service
from . import test_project_billing
@@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra.tools import float_is_zero, float_compare
from flectra.addons.sale_timesheet.tests.common import TestCommonSaleTimesheet
class TestCommonReporting(TestCommonSaleTimesheet):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
# expense product
cls.product_expense = cls.env['product.product'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'name': "Expense service",
'standard_price': 10,
'list_price': 20,
'type': 'service',
'invoice_policy': 'delivery',
'expense_policy': 'sales_price',
'default_code': 'EXP',
'service_type': 'manual',
'taxes_id': False,
'property_account_income_id': cls.company_data['default_account_revenue'].id,
})
# create Analytic Accounts
cls.analytic_account_1 = cls.env['account.analytic.account'].create({
'name': 'Test AA 1',
'code': 'AA1',
'company_id': cls.company_data['company'].id,
'partner_id': cls.partner_a.id
})
cls.analytic_account_2 = cls.env['account.analytic.account'].create({
'name': 'Test AA 2',
'code': 'AA2',
'company_id': cls.company_data['company'].id,
'partner_id': cls.partner_a.id
})
cls.analytic_account_3 = cls.env['account.analytic.account'].create({
'name': 'Test AA 3',
'code': 'AA3',
'company_id': cls.company_data['company'].id,
'partner_id': cls.partner_a.id
})
# Sale orders each will create project and a task in a global project (one SO is 'delivered', the other is 'ordered')
# and a third one using fixed_price (which is 'delivered')
cls.sale_order_1 = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'partner_id': cls.partner_a.id,
'partner_invoice_id': cls.partner_a.id,
'partner_shipping_id': cls.partner_a.id,
'analytic_account_id': cls.analytic_account_1.id,
})
cls.so_line_deliver_project = cls.env['sale.order.line'].create({
'name': cls.product_delivery_timesheet3.name,
'product_id': cls.product_delivery_timesheet3.id,
'product_uom_qty': 5,
'product_uom': cls.product_delivery_timesheet3.uom_id.id,
'price_unit': cls.product_delivery_timesheet3.list_price,
'order_id': cls.sale_order_1.id,
})
cls.so_line_deliver_task = cls.env['sale.order.line'].create({
'name': cls.product_delivery_timesheet2.name,
'product_id': cls.product_delivery_timesheet2.id,
'product_uom_qty': 7,
'product_uom': cls.product_delivery_timesheet2.uom_id.id,
'price_unit': cls.product_delivery_timesheet2.list_price,
'order_id': cls.sale_order_1.id,
})
cls.sale_order_2 = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'partner_id': cls.partner_a.id,
'partner_invoice_id': cls.partner_a.id,
'partner_shipping_id': cls.partner_a.id,
'analytic_account_id': cls.analytic_account_2.id,
})
cls.so_line_order_project = cls.env['sale.order.line'].create({
'name': cls.product_order_timesheet3.name,
'product_id': cls.product_order_timesheet3.id,
'product_uom_qty': 5,
'product_uom': cls.product_order_timesheet3.uom_id.id,
'price_unit': cls.product_order_timesheet3.list_price,
'order_id': cls.sale_order_2.id,
})
cls.so_line_order_task = cls.env['sale.order.line'].create({
'name': cls.product_order_timesheet2.name,
'product_id': cls.product_order_timesheet2.id,
'product_uom_qty': 7,
'product_uom': cls.product_order_timesheet2.uom_id.id,
'price_unit': cls.product_order_timesheet2.list_price,
'order_id': cls.sale_order_2.id,
})
cls.sale_order_3 = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'partner_id': cls.partner_a.id,
'partner_invoice_id': cls.partner_a.id,
'partner_shipping_id': cls.partner_a.id,
'analytic_account_id': cls.analytic_account_3.id,
})
cls.so_line_deliver_manual_project = cls.env['sale.order.line'].create({
'name': cls.product_delivery_manual3.name,
'product_id': cls.product_delivery_manual3.id,
'product_uom_qty': 11,
'product_uom': cls.product_delivery_manual3.uom_id.id,
'price_unit': cls.product_delivery_manual3.list_price,
'order_id': cls.sale_order_3.id,
})
def _log_timesheet_user(self, project, unit_amount, task=False):
""" Utility method to log timesheet """
Timesheet = self.env['account.analytic.line']
return Timesheet.create({
'name': 'timesheet employee on project_so_1 only',
'account_id': project.analytic_account_id.id,
'project_id': project.id,
'employee_id': self.employee_user.id,
'unit_amount': unit_amount,
'task_id': task.id if task else False,
})
def _log_timesheet_manager(self, project, unit_amount, task=False):
""" Utility method to log timesheet """
Timesheet = self.env['account.analytic.line']
return Timesheet.create({
'name': 'timesheet employee on project_so_1 only',
'account_id': project.analytic_account_id.id,
'project_id': project.id,
'employee_id': self.employee_manager.id,
'unit_amount': unit_amount,
'task_id': task.id if task else False,
})
@@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra.addons.sale_timesheet.tests.test_reporting import TestReporting
from flectra.tools import float_compare
from flectra.addons.sale_timesheet.tests.common_reporting import TestCommonReporting
from flectra.tools import float_compare, float_is_zero
from flectra.tests import tagged
@tagged('-at_install', 'post_install')
class TestSaleProject(TestReporting):
class TestSaleProject(TestCommonReporting):
def test_project_overview_by_project(self):
rounding = self.env.company.currency_id.rounding
@@ -34,7 +34,6 @@ class TestSaleProject(TestReporting):
project_so = self.so_line_order_project.project_id
# log timesheet for billable time
timesheet1 = self._log_timesheet_manager(project_so, 10, so_line_deliver_global_project.task_id)
task_so = self.so_line_order_project.task_id
# logged some timesheets: on project only, then on tasks with different employees
timesheet2 = self._log_timesheet_user(project_so, 2)
@@ -89,7 +88,7 @@ class TestSaleProject(TestReporting):
})
other_revenues = self.env['account.analytic.line'].create({
'name': 'pther revenues on project_so',
'name': 'other revenues on project_so',
'account_id': project_so.analytic_account_id.id,
'employee_id': self.employee_user.id,
'unit_amount': 1,
@@ -103,10 +102,10 @@ class TestSaleProject(TestReporting):
dashboard_value = timesheet2.unit_amount + timesheet3.unit_amount + timesheet4.unit_amount + timesheet5.unit_amount + timesheet1.unit_amount
project_so_timesheet_sold_unit = timesheet3.unit_amount + timesheet4.unit_amount
project_rate_non_billable = timesheet5.unit_amount / dashboard_value * 100
project_rate_non_billable_project = timesheet2.unit_amount / dashboard_value * 100
project_rate_billable_time = timesheet1.unit_amount / dashboard_value * 100
project_rate_billable_fixed = project_so_timesheet_sold_unit / dashboard_value * 100
project_rate_non_billable = round(timesheet5.unit_amount / dashboard_value * 100, 2)
project_rate_non_billable_project = round(timesheet2.unit_amount / dashboard_value * 100, 2)
project_rate_billable_time = round(timesheet1.unit_amount / dashboard_value * 100, 2)
project_rate_billable_fixed = round(project_so_timesheet_sold_unit / dashboard_value * 100, 2)
project_rate_total = project_rate_non_billable + project_rate_non_billable_project + project_rate_billable_time + project_rate_billable_fixed
project_invoiced = self.so_line_order_project.price_unit * self.so_line_order_project.product_uom_qty * timesheet1.unit_amount
project_timesheet_cost = timesheet2.amount + timesheet3.amount + timesheet4.amount + timesheet5.amount + timesheet1.amount
@@ -122,9 +121,11 @@ class TestSaleProject(TestReporting):
self.assertEqual(float_compare(vals['dashboard']['rates']['billable_time'], project_rate_billable_time, precision_rounding=rounding), 0, "The rate billable-time should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['rates']['billable_fixed'], project_rate_billable_fixed, precision_rounding=rounding), 0, "The rate billable-fixed should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['rates']['total'], project_rate_total, precision_rounding=rounding), 0, "The total rates should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['profit']['invoiced'], project_invoiced, precision_rounding=rounding), 0, "The amount invoiced should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['profit']['invoiced'], project_invoiced + project_other_revenues.price_total, precision_rounding=rounding), 0, "The amount invoiced should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['profit']['cost'], project_timesheet_cost, precision_rounding=rounding), 0, "The amount cost should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['profit']['expense_cost'], expense.amount, precision_rounding=rounding), 0, "The amount expense-cost should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['profit']['other_revenues'], other_revenues.amount + project_other_revenues.price_total, precision_rounding=rounding), 0, "The amount of the other revenues should be equal to the corresponding account move line and the one from the SO line")
self.assertEqual(float_compare(vals['dashboard']['profit']['total'], project_invoiced + project_timesheet_cost + expense.amount + other_revenues.amount + project_other_revenues.price_total, precision_rounding=rounding), 0, "The total amount should be the sum of the SO2 line and the created other_revenues account analytic line")
self.assertEqual(float_compare(vals['dashboard']['profit']['other_revenues'], other_revenues.amount, precision_rounding=rounding), 0, "The amount of the other revenues should be equal to the corresponding account move line and the one from the SO line")
self.assertEqual(float_compare(vals['dashboard']['profit']['expense_amount_untaxed_to_invoice'], -expense.amount, precision_rounding=rounding), 0, "The amount of expense to re-invoice shoul be equal to the opposite of expense amount")
self.assertTrue(float_is_zero(vals['dashboard']['profit']['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The amount of expense re-invoiced should be equal to 0")
self.assertEqual(float_compare(vals['dashboard']['profit']['total'], project_invoiced + project_timesheet_cost + other_revenues.amount + project_other_revenues.price_total, precision_rounding=rounding), 0, "The total amount should be the sum of the SO2 line and the created other_revenues account analytic line")
self.assertEqual(float_compare(vals['repartition_employee_max'], 11.0, precision_rounding=rounding), 0, "The amount of repartition-employee-max should be the one from SO2 line")
+552 -124
View File
@@ -1,115 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra.osv import expression
from flectra.tools import float_is_zero, float_compare
from flectra.addons.sale_timesheet.tests.common import TestCommonSaleTimesheet
from flectra.addons.sale_timesheet.tests.common_reporting import TestCommonReporting
from flectra.tests import tagged
@tagged('-at_install', 'post_install')
class TestReporting(TestCommonSaleTimesheet):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
# expense product
cls.product_expense = cls.env['product.product'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'name': "Expense service",
'standard_price': 10,
'list_price': 20,
'type': 'service',
'invoice_policy': 'delivery',
'expense_policy': 'sales_price',
'default_code': 'EXP',
'service_type': 'manual',
'taxes_id': False,
'property_account_income_id': cls.company_data['default_account_revenue'].id,
})
# create Analytic Accounts
cls.analytic_account_1 = cls.env['account.analytic.account'].create({
'name': 'Test AA 1',
'code': 'AA1',
'company_id': cls.company_data['company'].id,
'partner_id': cls.partner_a.id
})
cls.analytic_account_2 = cls.env['account.analytic.account'].create({
'name': 'Test AA 2',
'code': 'AA2',
'company_id': cls.company_data['company'].id,
'partner_id': cls.partner_a.id
})
# Sale orders each will create project and a task in a global project (one SO is 'delivered', the other is 'ordered')
cls.sale_order_1 = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'partner_id': cls.partner_a.id,
'partner_invoice_id': cls.partner_a.id,
'partner_shipping_id': cls.partner_a.id,
'analytic_account_id': cls.analytic_account_1.id,
})
cls.so_line_deliver_project = cls.env['sale.order.line'].create({
'name': cls.product_delivery_timesheet3.name,
'product_id': cls.product_delivery_timesheet3.id,
'product_uom_qty': 5,
'product_uom': cls.product_delivery_timesheet3.uom_id.id,
'price_unit': cls.product_delivery_timesheet3.list_price,
'order_id': cls.sale_order_1.id,
})
cls.so_line_deliver_task = cls.env['sale.order.line'].create({
'name': cls.product_delivery_timesheet2.name,
'product_id': cls.product_delivery_timesheet2.id,
'product_uom_qty': 7,
'product_uom': cls.product_delivery_timesheet2.uom_id.id,
'price_unit': cls.product_delivery_timesheet2.list_price,
'order_id': cls.sale_order_1.id,
})
cls.sale_order_2 = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'partner_id': cls.partner_a.id,
'partner_invoice_id': cls.partner_a.id,
'partner_shipping_id': cls.partner_a.id,
'analytic_account_id': cls.analytic_account_2.id,
})
cls.so_line_order_project = cls.env['sale.order.line'].create({
'name': cls.product_order_timesheet3.name,
'product_id': cls.product_order_timesheet3.id,
'product_uom_qty': 5,
'product_uom': cls.product_order_timesheet3.uom_id.id,
'price_unit': cls.product_order_timesheet3.list_price,
'order_id': cls.sale_order_2.id,
})
cls.so_line_order_task = cls.env['sale.order.line'].create({
'name': cls.product_order_timesheet2.name,
'product_id': cls.product_order_timesheet2.id,
'product_uom_qty': 7,
'product_uom': cls.product_order_timesheet2.uom_id.id,
'price_unit': cls.product_order_timesheet2.list_price,
'order_id': cls.sale_order_2.id,
})
def _log_timesheet_user(self, project, unit_amount, task=False):
""" Utility method to log timesheet """
Timesheet = self.env['account.analytic.line']
return Timesheet.create({
'name': 'timesheet employee on project_so_1 only',
'account_id': project.analytic_account_id.id,
'project_id': project.id,
'employee_id': self.employee_user.id,
'unit_amount': unit_amount,
'task_id': task.id if task else False,
})
def _log_timesheet_manager(self, project, unit_amount, task=False):
""" Utility method to log timesheet """
Timesheet = self.env['account.analytic.line']
return Timesheet.create({
'name': 'timesheet employee on project_so_1 only',
'account_id': project.analytic_account_id.id,
'project_id': project.id,
'employee_id': self.employee_manager.id,
'unit_amount': unit_amount,
'task_id': task.id if task else False,
})
class TestReporting(TestCommonReporting):
def test_profitability_report(self):
@@ -125,31 +23,38 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
# confirm sales orders
self.sale_order_1.action_confirm()
self.sale_order_2.action_confirm()
self.sale_order_3.action_confirm()
self.env['project.profitability.report'].flush()
project_so_1 = self.so_line_deliver_project.project_id
project_so_2 = self.so_line_order_project.project_id
project_so_3 = self.so_line_deliver_manual_project.project_id
task_so_1 = self.so_line_deliver_project.task_id
task_so_2 = self.so_line_order_project.task_id
task_so_3 = self.so_line_deliver_manual_project.task_id
task_in_global_1 = self.so_line_deliver_task.task_id
task_in_global_2 = self.so_line_order_task.task_id
# deliver project should not be impacted, as no timesheet are logged yet
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
self.assertTrue(float_is_zero(project_so_1_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['timesheet_unit_amount'], precision_rounding=rounding), "The timesheet unit amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['timesheet_cost'], precision_rounding=rounding), "The timesheet cost of the global project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['timesheet_cost'], precision_rounding=rounding), "The timesheet cost of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the global project should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO1 should be 0.0")
# order project should be to invoice, but nothing has been delivered yet
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO2 should be 0.0")
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], self.so_line_order_project.price_unit * self.so_line_order_project.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice should be the one from the SO line, as we are in ordered quantity")
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_unit_amount'], precision_rounding=rounding), "The timesheet unit amount of the project from SO2 should be 0.0")
@@ -157,9 +62,21 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the global project should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO2 should be 0.0")
# global project now contain 2 tasks: 'deliver' one which has no effect (no timesheet yet), and the 'order' one which should update the 'to invoice' amount
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
# fixed price project should not be impacted, as no delivered quantity are logged yet
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['timesheet_unit_amount'], precision_rounding=rounding), "The timesheet unit amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['timesheet_cost'], precision_rounding=rounding), "The timesheet cost of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 should be 0.0")
# global project now contain 3 tasks: 'deliver' ones which have no effect (no timesheet or delivered yet), and the 'order' one which should update the 'to invoice' amount
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
self.assertTrue(float_is_zero(project_global_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the global project should be 0.0")
self.assertEqual(float_compare(project_global_stat['amount_untaxed_to_invoice'], self.so_line_order_task.price_unit * self.so_line_order_task.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice of global project should take the task in 'oredered qty' into account")
self.assertTrue(float_is_zero(project_global_stat['timesheet_unit_amount'], precision_rounding=rounding), "The timesheet unit amount of the global project should be 0.0")
@@ -167,21 +84,22 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
# logged some timesheets: on project only, then on tasks with different employees
timesheet1 = self._log_timesheet_user(project_so_1, 2)
timesheet2 = self._log_timesheet_user(project_so_2, 2)
timesheet3 = self._log_timesheet_user(project_so_1, 3, task_so_1)
timesheet4 = self._log_timesheet_user(project_so_2, 3, task_so_2)
timesheet5 = self._log_timesheet_manager(project_so_1, 1, task_so_1)
timesheet6 = self._log_timesheet_manager(project_so_2, 1, task_so_2)
timesheet7 = self._log_timesheet_manager(self.project_global, 3, task_in_global_1)
timesheet8 = self._log_timesheet_manager(self.project_global, 3, task_in_global_2)
timesheet9 = self._log_timesheet_user(project_so_3, 4, task_so_3)
self.env['project.profitability.report'].flush()
# deliver project should now have cost and something to invoice
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + timesheet3.unit_amount + timesheet5.unit_amount
self.assertTrue(float_is_zero(project_so_1_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO1 should be 0.0")
@@ -191,9 +109,10 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project should be 0.0")
# order project still have something to invoice but has costs now
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
project_so_2_timesheet_sold_unit = timesheet2.unit_amount + timesheet4.unit_amount + timesheet6.unit_amount
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO2 should be 0.0")
@@ -203,9 +122,23 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO2 should be 0.0")
# fixed price project should ow have cost and nothing to invoice as no delivered quantity are logged yet
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_3_timesheet_cost = timesheet9.amount
project_so_3_timesheet_sold_unit = timesheet9.unit_amount
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['timesheet_unit_amount'], project_so_3_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO3")
self.assertEqual(float_compare(project_so_3_stat['timesheet_cost'], project_so_3_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO3")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 should be 0.0")
# global project should have more to invoice, and should now have costs
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_global_timesheet_cost = timesheet7.amount + timesheet8.amount
project_global_timesheet_unit = timesheet7.unit_amount + timesheet8.unit_amount
project_global_to_invoice = (self.so_line_order_task.price_unit * self.so_line_order_task.product_uom_qty) + (self.so_line_deliver_task.price_unit * timesheet7.unit_amount)
@@ -216,6 +149,63 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
self.so_line_deliver_manual_project.write({'qty_delivered': 7.0})
self.env['project.profitability.report'].flush()
# deliver project should now have cost and something to invoice
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + timesheet3.unit_amount + timesheet5.unit_amount
self.assertTrue(float_is_zero(project_so_1_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO1 should be 0.0")
self.assertEqual(float_compare(project_so_1_stat['amount_untaxed_to_invoice'], self.so_line_deliver_project.price_unit * project_so_1_timesheet_sold_unit, precision_rounding=rounding), 0, "The amount to invoice of the project from SO1 should only include timesheet linked to task")
self.assertEqual(float_compare(project_so_1_stat['timesheet_unit_amount'], project_so_1_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO1 should include all timesheet in project")
self.assertEqual(float_compare(project_so_1_stat['timesheet_cost'], project_so_1_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO1 should include all timesheet")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project should be 0.0")
# order project still have something to invoice but has costs now
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
project_so_2_timesheet_sold_unit = timesheet2.unit_amount + timesheet4.unit_amount + timesheet6.unit_amount
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO2 should be 0.0")
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], self.so_line_order_project.price_unit * self.so_line_order_project.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice should be the one from the SO line, as we are in ordered quantity")
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO2 should include all timesheet")
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO2 should include all timesheet")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO2 should be 0.0")
# fixed price project should ow have cost and something to invoice as some delivered quantity are logged in
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_3_timesheet_cost = timesheet9.amount
project_so_3_timesheet_sold_unit = timesheet9.unit_amount
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['amount_untaxed_to_invoice'], self.so_line_deliver_manual_project.price_unit * self.so_line_deliver_manual_project.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['timesheet_unit_amount'], project_so_3_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO3")
self.assertEqual(float_compare(project_so_3_stat['timesheet_cost'], project_so_3_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO3")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 should be 0.0")
# global project should have more to invoice, and should now have costs
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_global_timesheet_cost = timesheet7.amount + timesheet8.amount
project_global_timesheet_unit = timesheet7.unit_amount + timesheet8.unit_amount
project_global_to_invoice = (self.so_line_order_task.price_unit * self.so_line_order_task.product_uom_qty) + (self.so_line_deliver_task.price_unit * timesheet7.unit_amount)
self.assertTrue(float_is_zero(project_global_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the global project should be 0.0")
self.assertEqual(float_compare(project_global_stat['amount_untaxed_to_invoice'], project_global_to_invoice, precision_rounding=rounding), 0, "The amount to invoice of global project should take the task in 'oredered qty' and the delivered timesheets into account")
self.assertEqual(float_compare(project_global_stat['timesheet_unit_amount'], project_global_timesheet_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the global project should include all timesheet")
self.assertEqual(float_compare(project_global_stat['timesheet_cost'], project_global_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the global project should include all timesheet")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
InvoiceWizard = self.env['sale.advance.payment.inv'].with_context(mail_notrack=True)
@@ -236,7 +226,7 @@ class TestReporting(TestCommonSaleTimesheet):
self.env['project.profitability.report'].flush()
# deliver project should now have cost and something invoiced
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + timesheet3.unit_amount + timesheet5.unit_amount
@@ -247,9 +237,10 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project should be 0.0")
# order project has still nothing invoiced
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
project_so_2_timesheet_sold_unit = timesheet2.unit_amount + timesheet4.unit_amount + timesheet6.unit_amount
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO2 should be 0.0")
@@ -259,9 +250,23 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO2 should be 0.0")
# fixed price project has still nothing invoice
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_3_timesheet_cost = timesheet9.amount
project_so_3_timesheet_sold_unit = timesheet9.unit_amount
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['amount_untaxed_to_invoice'], self.so_line_deliver_manual_project.price_unit * self.so_line_deliver_manual_project.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['timesheet_unit_amount'], project_so_3_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO3")
self.assertEqual(float_compare(project_so_3_stat['timesheet_cost'], project_so_3_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO3")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 should be 0.0")
# global project should have more to invoice, and should now have costs
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_global_timesheet_cost = timesheet7.amount + timesheet8.amount
project_global_timesheet_unit = timesheet7.unit_amount + timesheet8.unit_amount
project_global_to_invoice = self.so_line_order_task.price_unit * self.so_line_order_task.product_uom_qty
@@ -273,6 +278,7 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
# invoice the Sales Order SO2
context = {
@@ -291,7 +297,7 @@ class TestReporting(TestCommonSaleTimesheet):
self.env['project.profitability.report'].flush()
# deliver project should not be impacted by the invoice of the other SO
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + timesheet3.unit_amount + timesheet5.unit_amount
self.assertEqual(float_compare(project_so_1_stat['amount_untaxed_invoiced'], self.so_line_deliver_project.price_unit * project_so_1_timesheet_sold_unit, precision_rounding=rounding), 0, "The invoiced amount of the project from SO1 should only include timesheet linked to task")
@@ -301,9 +307,10 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project should be 0.0")
# order project is now totally invoiced, as we are in ordered qty
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
project_so_2_timesheet_sold_unit = timesheet2.unit_amount + timesheet4.unit_amount + timesheet6.unit_amount
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], self.so_line_order_project.price_unit * self.so_line_order_project.product_uom_qty, precision_rounding=rounding), 0, "The invoiced amount should be the one from the SO line, as we are in ordered quantity")
@@ -313,9 +320,23 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO2 should be 0.0")
# fixed price project has still nothing invoice
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_3_timesheet_cost = timesheet9.amount
project_so_3_timesheet_sold_unit = timesheet9.unit_amount
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['amount_untaxed_to_invoice'], self.so_line_deliver_manual_project.price_unit * self.so_line_deliver_manual_project.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['timesheet_unit_amount'], project_so_3_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO3")
self.assertEqual(float_compare(project_so_3_stat['timesheet_cost'], project_so_3_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO3")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 should be 0.0")
# global project should be totally invoiced
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_global_timesheet_cost = timesheet7.amount + timesheet8.amount
project_global_timesheet_unit = timesheet7.unit_amount + timesheet8.unit_amount
project_global_invoiced = self.so_line_order_task.price_unit * self.so_line_order_task.product_uom_qty + self.so_line_deliver_task.price_unit * timesheet7.unit_amount
@@ -326,6 +347,76 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
# invoice the Sales Order SO3
context = {
"active_model": 'sale.order',
"active_ids": [self.sale_order_3.id],
"active_id": self.sale_order_3.id,
'open_invoices': True,
}
payment = InvoiceWizard.create({
'advance_payment_method': 'delivered',
})
action_invoice = payment.with_context(context).create_invoices()
invoice_id = action_invoice['res_id']
invoice_3 = self.env['account.move'].browse(invoice_id)
invoice_3.action_post()
self.env['project.profitability.report'].flush()
# deliver project should not be impacted by the invoice of the other SO
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + timesheet3.unit_amount + timesheet5.unit_amount
self.assertEqual(float_compare(project_so_1_stat['amount_untaxed_invoiced'], self.so_line_deliver_project.price_unit * project_so_1_timesheet_sold_unit, precision_rounding=rounding), 0, "The invoiced amount of the project from SO1 should only include timesheet linked to task")
self.assertTrue(float_is_zero(project_so_1_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of the project from SO1 should be 0.0")
self.assertEqual(float_compare(project_so_1_stat['timesheet_unit_amount'], project_so_1_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO1 should include all timesheet in project")
self.assertEqual(float_compare(project_so_1_stat['timesheet_cost'], project_so_1_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO1 should include all timesheet")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project should be 0.0")
# order project should not be impacted by the invoice of the other SO
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
project_so_2_timesheet_sold_unit = timesheet2.unit_amount + timesheet4.unit_amount + timesheet6.unit_amount
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], self.so_line_order_project.price_unit * self.so_line_order_project.product_uom_qty, precision_rounding=rounding), 0, "The invoiced amount should be the one from the SO line, as we are in ordered quantity")
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice should be the one 0.0, as all ordered quantity is invoiced")
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO2 should include all timesheet")
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO2 should include all timesheet")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO2 should be 0.0")
# fixed price project is now partially invoiced, as we are in delivered qty
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_3_timesheet_cost = timesheet9.amount
project_so_3_timesheet_sold_unit = timesheet9.unit_amount
self.assertEqual(float_compare(project_so_3_stat['amount_untaxed_invoiced'], self.so_line_deliver_manual_project.price_unit * self.so_line_deliver_manual_project.qty_delivered, precision_rounding=rounding), 0, "The invoiced amount of the project from SO3 should be the delivered quantity * the unit price")
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['timesheet_unit_amount'], project_so_3_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO3")
self.assertEqual(float_compare(project_so_3_stat['timesheet_cost'], project_so_3_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO3")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 should be 0.0")
# global project should be totally invoiced
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_global_timesheet_cost = timesheet7.amount + timesheet8.amount
project_global_timesheet_unit = timesheet7.unit_amount + timesheet8.unit_amount
project_global_invoiced = self.so_line_order_task.price_unit * self.so_line_order_task.product_uom_qty + self.so_line_deliver_task.price_unit * timesheet7.unit_amount
self.assertEqual(float_compare(project_global_stat['amount_untaxed_invoiced'], project_global_invoiced, precision_rounding=rounding), 0, "The invoiced amount of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of global project should take the task in 'oredered qty' and the delivered timesheets into account")
self.assertEqual(float_compare(project_global_stat['timesheet_unit_amount'], project_global_timesheet_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the global project should include all timesheet")
self.assertEqual(float_compare(project_global_stat['timesheet_cost'], project_global_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the global project should include all timesheet")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
# simulate the auto creation of the SO line for expense, like we confirm a vendor bill.
so_line_expense = self.env['sale.order.line'].create({
@@ -362,7 +453,7 @@ class TestReporting(TestCommonSaleTimesheet):
self.env['project.profitability.report'].flush()
# deliver project should now have expense cost, and expense to reinvoice as there is a still open sales order linked to the AA1
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + timesheet3.unit_amount + timesheet5.unit_amount
self.assertEqual(float_compare(project_so_1_stat['amount_untaxed_invoiced'], self.so_line_deliver_project.price_unit * project_so_1_timesheet_sold_unit, precision_rounding=rounding), 0, "The invoiced amount of the project from SO1 should only include timesheet linked to task")
@@ -372,9 +463,10 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertEqual(float_compare(project_so_1_stat['expense_amount_untaxed_to_invoice'], -1 * expense1.amount, precision_rounding=rounding), 0, "The expense cost to reinvoice of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertEqual(float_compare(project_so_1_stat['expense_cost'], expense1.amount, precision_rounding=rounding), 0, "The expense cost of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project should be 0.0")
# order project is not impacted by the expenses
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
project_so_2_timesheet_sold_unit = timesheet2.unit_amount + timesheet4.unit_amount + timesheet6.unit_amount
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], self.so_line_order_project.price_unit * self.so_line_order_project.product_uom_qty, precision_rounding=rounding), 0, "The invoiced amount should be the one from the SO line, as we are in ordered quantity")
@@ -384,9 +476,10 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO2 should be 0.0")
# global project should have an expense, but not reinvoiceable
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_global_timesheet_cost = timesheet7.amount + timesheet8.amount
project_global_timesheet_unit = timesheet7.unit_amount + timesheet8.unit_amount
project_global_invoiced = self.so_line_order_task.price_unit * self.so_line_order_task.product_uom_qty + self.so_line_deliver_task.price_unit * timesheet7.unit_amount
@@ -397,3 +490,338 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertEqual(float_compare(project_global_stat['expense_cost'], expense2.amount, precision_rounding=rounding), 0, "The expense cost of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
def test_reversed_downpayment(self):
# this test suppose everything is in the same currency as the current one
currency = self.env.company.currency_id
rounding = currency.rounding
self.sale_order_2.action_confirm()
context = {
'active_model': 'sale.order',
'active_ids': self.sale_order_2.ids,
'active_id': self.sale_order_2.id,
'default_journal_id': self.company_data['default_journal_sale'].id,
'open_invoices': True,
}
# Let's do an invoice for a deposit of 100
downpayment = self.env['sale.advance.payment.inv'].with_context(context).create({
'advance_payment_method': 'percentage',
'amount': 10,
})
action_invoice = downpayment.create_invoices()
invoice_id = action_invoice['res_id']
invoice_downpayment = self.env['account.move'].browse(invoice_id)
invoice_downpayment.action_post()
posted_invoice_res_ids = [invoice_id]
downpayment2 = self.env['sale.advance.payment.inv'].with_context(context).create({
'advance_payment_method': 'percentage',
'amount': 25,
})
action_invoice = downpayment2.create_invoices()
invoice_downpayment2 = self.env['account.move'].search(expression.AND([action_invoice['domain'], [('id', 'not in', posted_invoice_res_ids)]]))
invoice_downpayment2.action_post()
posted_invoice_res_ids += invoice_downpayment2.ids
milestone_to_invoice = self.so_line_order_project.price_unit * self.so_line_order_project.qty_to_invoice
timesheets_to_invoice = self.so_line_order_task.price_unit * self.so_line_order_task.qty_to_invoice
total_product_price = milestone_to_invoice + timesheets_to_invoice
credit_note_wizard = self.env['account.move.reversal'].with_context({
'active_ids': invoice_downpayment2.ids,
'active_id': invoice_downpayment2.id,
'active_model': 'account.move'
}).create({
'refund_method': 'refund',
'reason': 'reason test create',
})
action_moves = credit_note_wizard.reverse_moves()
credit_id = action_moves['res_id']
invoice_credit = self.env['account.move'].browse(credit_id)
invoice_credit.action_post()
posted_invoice_res_ids += invoice_credit.ids
project_so_2 = self.so_line_order_project.project_id
task_so_2 = self.so_line_order_project.task_id
task_in_global_2 = self.so_line_order_task.task_id
self.env['project.profitability.report'].flush()
project_so_2_stat = self.env['project.profitability.report'].read_group(
[('project_id', 'in', project_so_2.ids)],
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
['project_id']
)[0]
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], 0.1 * total_product_price, precision_rounding=rounding), 0,
"The invoiced amount is the amount of downpayments not reversed")
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], milestone_to_invoice - 0.1 * total_product_price, precision_rounding=rounding), 0,
"The amount to invoice is the milestone product minus the downpayment not reversed")
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_unit_amount'], precision_rounding=rounding),
"The timesheet unit amount of the project from SO2 is 0")
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_cost'], precision_rounding=rounding),
"The timesheet cost of the project from SO2 is 0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding),
"The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding),
"The expense invoiced amount of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding),
"The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
"The other revenues of the project from SO2 should be 0.0")
# logged some timesheets: on project only, then on tasks with different employees
timesheet2 = self._log_timesheet_user(project_so_2, 2)
timesheet4 = self._log_timesheet_user(project_so_2, 3, task_so_2)
timesheet6 = self._log_timesheet_manager(project_so_2, 2, task_so_2)
timesheet8 = self._log_timesheet_manager(self.project_global, 3, task_in_global_2)
# simulate the auto creation of the SO line for expense, like we confirm a vendor bill.
so_line_expense = self.env['sale.order.line'].create({
'name': self.product_expense.name,
'product_id': self.product_expense.id,
'product_uom_qty': 0.0,
'product_uom': self.product_expense.uom_id.id,
'price_unit': self.product_expense.list_price, # reinvoice at sales price
'order_id': self.sale_order_2.id,
'is_expense': True,
})
expense1 = self.env['account.analytic.line'].create({
'name': 'expense on project_so_2',
'account_id': project_so_2.analytic_account_id.id,
'so_line': so_line_expense.id,
'employee_id': self.employee_user.id,
'unit_amount': 4,
'amount': 4 * self.product_expense.list_price * -1,
'product_id': self.product_expense.id,
'product_uom_id': self.product_expense.uom_id.id,
})
self.env['project.profitability.report'].flush()
project_so_2_stat = self.env['project.profitability.report'].read_group(
[('project_id', 'in', project_so_2.ids)],
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
['project_id']
)[0]
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
project_so_2_timesheet_sold_unit = timesheet2.unit_amount + timesheet4.unit_amount + timesheet6.unit_amount
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], 0.1 * total_product_price, precision_rounding=rounding), 0,
"The invoiced amount of the project from SO2 should only include downpayment not reversed")
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], milestone_to_invoice - 0.1 * total_product_price, precision_rounding=rounding), 0,
"The amount to invoice of the project from SO2 should include the milestone to invoice minus the downpayment")
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0,
"The timesheet unit amount of the project from SO2 should include all timesheet in project")
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0,
"The timesheet cost of the project from SO2 should include all timesheet")
self.assertEqual(float_compare(project_so_2_stat['expense_amount_untaxed_to_invoice'], -expense1.amount, precision_rounding=rounding), 0,
"The expense cost to reinvoice of the project from SO2 should be the expense amount")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding),
"The expense invoiced amount of the project from SO2 should be 0.0")
self.assertEqual(float_compare(project_so_2_stat['expense_cost'], expense1.amount, precision_rounding=rounding), 0,
"The expense cost of the project from SO1 should be expense amount")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
"The other revenues of the project from SO2 should be 0.0")
# invoice the Sales Order SO2
context = {
"active_model": 'sale.order',
"active_ids": self.sale_order_2.ids,
"active_id": self.sale_order_2.id,
'open_invoices': True,
'mail_notrack': True,
}
payment = self.env['sale.advance.payment.inv'].with_context(mail_notrack=True).create({
'advance_payment_method': 'delivered',
})
action_invoice = payment.with_context(context).create_invoices()
invoice_payment = self.env['account.move'].search(expression.AND([action_invoice['domain'], [('id', 'not in', posted_invoice_res_ids)]]))
invoice_payment.action_post()
self.env['project.profitability.report'].flush()
project_so_2_stat = self.env['project.profitability.report'].read_group(
[('project_id', 'in', project_so_2.ids)],
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
['project_id']
)[0]
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], milestone_to_invoice, precision_rounding=rounding), 0,
"The invoiced amount of the project from SO2 should only include timesheet linked to task")
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_to_invoice'], precision_rounding=rounding),
"The amount to invoice of the project from SO2 should be 0.0")
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0,
"The timesheet unit amount of the project from SO2 should include all timesheet in project")
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0,
"The timesheet cost of the project from SO2 should include all timesheet")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding),
"The expense to invoice amount of the project from SO2 should be 0.0")
self.assertEqual(float_compare(project_so_2_stat['expense_amount_untaxed_invoiced'], -1 * expense1.amount, precision_rounding=rounding), 0,
"The expense cost reinvoiced of the project from SO2 should be the expense amount")
self.assertEqual(float_compare(project_so_2_stat['expense_cost'], expense1.amount, precision_rounding=rounding), 0,
"The expense cost of the project from SO2 should be the expense amount")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
"The other revenues of the project from SO2 should be 0.0")
credit_note_wizard = self.env['account.move.reversal'].with_context({
'active_ids': invoice_payment.ids,
'active_id': invoice_payment.id,
'active_model': 'account.move'
}).create({
'refund_method': 'refund',
'reason': 'reason test create',
})
action_moves = credit_note_wizard.reverse_moves()
credit_id = action_moves['res_id']
invoice_credit = self.env['account.move'].browse(credit_id)
invoice_credit.action_post()
self.env['project.profitability.report'].flush()
project_so_2_stat = self.env['project.profitability.report'].read_group(
[('project_id', 'in', project_so_2.ids)],
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
['project_id']
)[0]
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], 0.1 * total_product_price, precision_rounding=rounding), 0,
"The invoiced amount of the project from SO2 should only include downpayment not reversed")
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], milestone_to_invoice - 0.1 * total_product_price, precision_rounding=rounding), 0,
"The amount to invoice of the project from SO2 should include the milestone to invoice minus the downpayment")
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0,
"The timesheet unit amount of the project from SO2 should include all timesheet in project")
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0,
"The timesheet cost of the project from SO2 should include all timesheet")
self.assertEqual(float_compare(project_so_2_stat['expense_amount_untaxed_to_invoice'], -expense1.amount, precision_rounding=rounding), 0,
"The expense cost to reinvoice of the project from SO2 should be the expense amount")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding),
"The expense invoiced amount of the project from SO2 should be 0.0")
self.assertEqual(float_compare(project_so_2_stat['expense_cost'], expense1.amount, precision_rounding=rounding), 0,
"The expense cost of the project from SO1 should be expense amount")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
"The other revenues of the project from SO2 should be 0.0")
def test_milestone_no_tracking(self):
# this test suppose everything is in the same currency as the current one
currency = self.env.company.currency_id
rounding = currency.rounding
so_line_deliver_no_task = self.env['sale.order.line'].create({
'name': self.product_delivery_manual1.name,
'product_id': self.product_delivery_manual1.id,
'product_uom_qty': 50,
'product_uom': self.product_delivery_manual1.uom_id.id,
'price_unit': self.product_delivery_manual1.list_price,
'order_id': self.sale_order_2.id,
})
so_line_deliver_no_task.write({'qty_delivered': 1.0})
self.sale_order_2.action_confirm()
milestone_to_invoice = self.so_line_order_project.price_unit * self.so_line_order_project.qty_to_invoice
milestone_no_task = so_line_deliver_no_task.price_unit * so_line_deliver_no_task.qty_to_invoice
self.env['project.profitability.report'].flush()
project_so_2 = self.so_line_order_project.project_id
project_so_2_stat = self.env['project.profitability.report'].read_group(
[('project_id', 'in', project_so_2.ids)],
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
['project_id']
)[0]
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding),
"The invoiced amount of the project from SO2 should be 0.0")
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], milestone_to_invoice + milestone_no_task, precision_rounding=rounding), 0,
"The amount to invoice of the project from SO2 should include the milestone to invoice")
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_unit_amount'], precision_rounding=rounding),
"The timesheet unit amount of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_cost'], precision_rounding=rounding),
"The timesheet cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding),
"The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding),
"The expense invoiced amount of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding),
"The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
"The other revenues of the project from SO2 should be 0.0")
task_using_milestone_not_tracked = self.env['project.task'].create({
'name': 'Task with milestone not tracked',
'project_id': project_so_2.id,
'partner_id': project_so_2.partner_id.id,
'sale_line_id': so_line_deliver_no_task.id,
})
self.env['project.profitability.report'].flush()
project_so_2_stat = self.env['project.profitability.report'].read_group(
[('project_id', 'in', project_so_2.ids)],
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
['project_id']
)[0]
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding),
"The invoiced amount of the project from SO2 should be 0.0")
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], milestone_to_invoice + milestone_no_task, precision_rounding=rounding), 0,
"The amount to invoice of the project from SO2 should include the milestone to invoice linked to the project or project's task")
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_unit_amount'], precision_rounding=rounding),
"The timesheet unit amount of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_cost'], precision_rounding=rounding),
"The timesheet cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding),
"The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding),
"The expense invoiced amount of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding),
"The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
"The other revenues of the project from SO2 should be 0.0")
timesheet = self._log_timesheet_user(project_so_2, 3, task_using_milestone_not_tracked)
self.env['project.profitability.report'].flush()
project_so_2_stat = self.env['project.profitability.report'].read_group(
[('project_id', 'in', project_so_2.ids)],
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
['project_id']
)[0]
project_so_2_timesheet_cost = timesheet.amount
project_so_2_timesheet_sold_unit = timesheet.unit_amount
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding),
"The invoiced amount of the project from SO2 should only include downpayment not reversed")
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], milestone_to_invoice + milestone_no_task, precision_rounding=rounding), 0,
"The amount to invoice of the project from SO2 should include the milestone to invoice minus the downpayment")
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0,
"The timesheet unit amount of the project from SO2 should include all timesheet in project")
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0,
"The timesheet cost of the project from SO2 should include all timesheet")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding),
"The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding),
"The expense invoiced amount of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding),
"The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
"The other revenues of the project from SO2 should be 0.0")
# invoice the Sales Order SO2
context = {
"active_model": 'sale.order',
"active_ids": self.sale_order_2.ids,
"active_id": self.sale_order_2.id,
'open_invoices': True,
'mail_notrack': True,
}
payment = self.env['sale.advance.payment.inv'].with_context(mail_notrack=True).create({
'advance_payment_method': 'delivered',
})
action_invoice = payment.with_context(context).create_invoices()
invoice_payment = self.env['account.move'].browse(action_invoice['res_id'])
invoice_payment.action_post()
self.env['project.profitability.report'].flush()
project_so_2_stat = self.env['project.profitability.report'].read_group(
[('project_id', 'in', project_so_2.ids)],
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
['project_id']
)[0]
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], milestone_to_invoice + milestone_no_task, precision_rounding=rounding), 0,
"The invoiced amount of the project from SO2 should include the milestone invoiced")
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_to_invoice'], precision_rounding=rounding),
"The amount to invoice of the project from SO2 should include the milestone to invoice")
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0,
"The timesheet unit amount of the project from SO2 should include all timesheet in project")
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0,
"The timesheet cost of the project from SO2 should include all timesheet")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding),
"The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding),
"The expense invoiced amount of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding),
"The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
"The other revenues of the project from SO2 should be 0.0")
@@ -241,6 +241,14 @@
Re-invoiced costs
</td>
</tr>
<tr t-if="display_cost &amp; (dashboard['profit']['expense_amount_untaxed_to_invoice'] != 0)">
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['profit']['expense_amount_untaxed_to_invoice']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td title="Costs from expenses that still need to be reinvoiced to your customer (provided that the Analytic Account of the Project was set on the Expense).">
To re-invoice costs
</td>
</tr>
<tr>
<td class="o_timesheet_plan_dashboard_total">
<b>
+6 -7
View File
@@ -1270,17 +1270,16 @@ class StockMove(models.Model):
# Find a candidate move line to update or create a new one.
for reserved_quant, quantity in quants:
to_update = self.move_line_ids.filtered(lambda ml: ml._reservation_is_updatable(quantity, reserved_quant))
to_update = next((line for line in self.move_line_ids if line._reservation_is_updatable(quantity, reserved_quant)), False)
if to_update:
uom_quantity = self.product_id.uom_id._compute_quantity(quantity, to_update[0].product_uom_id, rounding_method='HALF-UP')
uom_quantity = self.product_id.uom_id._compute_quantity(quantity, to_update.product_uom_id, rounding_method='HALF-UP')
uom_quantity = float_round(uom_quantity, precision_digits=rounding)
uom_quantity_back_to_product_uom = to_update[0].product_uom_id._compute_quantity(uom_quantity, self.product_id.uom_id, rounding_method='HALF-UP')
uom_quantity_back_to_product_uom = to_update.product_uom_id._compute_quantity(uom_quantity, self.product_id.uom_id, rounding_method='HALF-UP')
if to_update and float_compare(quantity, uom_quantity_back_to_product_uom, precision_digits=rounding) == 0:
to_update[0].with_context(bypass_reservation_update=True).product_uom_qty += uom_quantity
to_update.with_context(bypass_reservation_update=True).product_uom_qty += uom_quantity
else:
if self.product_id.tracking == 'serial':
for i in range(0, int(quantity)):
self.env['stock.move.line'].create(self._prepare_move_line_vals(quantity=1, reserved_quant=reserved_quant))
self.env['stock.move.line'].create([self._prepare_move_line_vals(quantity=1, reserved_quant=reserved_quant) for i in range(int(quantity))])
else:
self.env['stock.move.line'].create(self._prepare_move_line_vals(quantity=quantity, reserved_quant=reserved_quant))
return taken_quantity
@@ -1652,7 +1651,7 @@ class StockMove(models.Model):
else:
moves_state_to_write['confirmed'].add(move.id)
for state, moves_ids in moves_state_to_write.items():
self.browse(moves_ids).write({'state': state})
self.browse(moves_ids).filtered(lambda m: m.state != state).state = state
@api.model
def _consuming_picking_types(self):
+1 -1
View File
@@ -77,7 +77,7 @@ class ProductionLot(models.Model):
@api.model_create_multi
def create(self, vals_list):
self._check_create()
return super(ProductionLot, self).create(vals_list)
return super(ProductionLot, self.with_context(mail_create_nosubscribe=True)).create(vals_list)
def write(self, vals):
if 'company_id' in vals:
+1 -1
View File
@@ -460,7 +460,7 @@
<field name="name" string="Transfer" filter_domain="['|', ('name', 'ilike', self), ('origin', 'ilike', self)]"/>
<field name="partner_id" filter_domain="[('partner_id', 'child_of', self)]"/>
<field name="origin"/>
<field name="product_id"/>
<field name="product_id" filter_domain="['|', '|', ('product_id.default_code', 'ilike', self), ('product_id.name', 'ilike', self), ('product_id.barcode', 'ilike', self)]"/>
<field name="picking_type_id"/>
<filter name="my_transfers" string="My Transfers" domain="[('user_id', '=', uid)]"/>
<filter string="Starred" name="starred" domain="[('priority', '=', '1')]"/>
@@ -1660,8 +1660,11 @@ var SnippetsMenu = Widget.extend({
* when the DOM elements must be in an editable environment to be
* considered (@see noCheck), this is true if the DOM elements'
* parent must also be in an editable environment to be considered.
* @param {string} excludeParent
* jQuery selector that the parents of DOM elements must *not* match
* to be considered as potential snippet.
*/
_computeSelectorFunctions: function (selector, exclude, target, noCheck, isChildren) {
_computeSelectorFunctions: function (selector, exclude, target, noCheck, isChildren, excludeParent) {
var self = this;
exclude += `${exclude && ', '}.o_snippet_not_selectable`;
@@ -1675,6 +1678,12 @@ var SnippetsMenu = Widget.extend({
return oldFilter.apply(this) && $(this).find(target).length !== 0;
};
}
if (excludeParent) {
const oldFilter = filterFunc;
filterFunc = function () {
return oldFilter.apply(this) && !$(this).parent().is(excludeParent);
};
}
// Prepare the functions
var functions = {
@@ -1735,10 +1744,12 @@ var SnippetsMenu = Widget.extend({
this.templateOptions = [];
var selectors = [];
var $styles = $html.find('[data-selector]');
const snippetAdditionDropIn = $styles.filter('#so_snippet_addition').data('drop-in');
$styles.each(function () {
var $style = $(this);
var selector = $style.data('selector');
var exclude = $style.data('exclude') || '';
const excludeParent = $style.attr('id') === "so_content_addition" ? snippetAdditionDropIn : '';
var target = $style.data('target');
var noCheck = $style.data('no-check');
var optionID = $style.data('js') || $style.data('option-name'); // used in tour js as selector
@@ -1749,7 +1760,7 @@ var SnippetsMenu = Widget.extend({
'base_target': target,
'selector': self._computeSelectorFunctions(selector, exclude, target, noCheck),
'$el': $style,
'drop-near': $style.data('drop-near') && self._computeSelectorFunctions($style.data('drop-near'), '', false, noCheck, true),
'drop-near': $style.data('drop-near') && self._computeSelectorFunctions($style.data('drop-near'), '', false, noCheck, true, excludeParent),
'drop-in': $style.data('drop-in') && self._computeSelectorFunctions($style.data('drop-in'), '', false, noCheck),
'data': _.extend({string: $style.attr('string')}, $style.data()),
};
+1 -1
View File
@@ -173,7 +173,7 @@ class Slide(models.Model):
slide_resource_ids = fields.One2many('slide.slide.resource', 'slide_id', string="Additional Resource for this slide")
slide_resource_downloadable = fields.Boolean('Allow Download', default=True, help="Allow the user to download the content of the slide.")
mime_type = fields.Char('Mime-type')
html_content = fields.Html("HTML Content", help="Custom HTML content for slides of type 'Web Page'.", translate=True, sanitize_form=False)
html_content = fields.Html("HTML Content", help="Custom HTML content for slides of type 'Web Page'.", translate=True, sanitize_attributes=False, sanitize_form=False)
# website
website_id = fields.Many2one(related='channel_id.website_id', readonly=True)
date_published = fields.Datetime('Publish Date', readonly=True, tracking=1)