mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-18 04:12:06 +02:00
Initial commit: Sale packages
This commit is contained in:
commit
14e3d26998
6469 changed files with 2479670 additions and 0 deletions
48
odoo-bringout-oca-ocb-project_sale_expense/README.md
Normal file
48
odoo-bringout-oca-ocb-project_sale_expense/README.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Project - Sale - Expense
|
||||
|
||||
Adds a full traceability of reinvoice expenses on the profitability report.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-ocb-project_sale_expense
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
This addon depends on:
|
||||
- sale_project
|
||||
- sale_expense
|
||||
- project_hr_expense
|
||||
|
||||
## Manifest Information
|
||||
|
||||
- **Name**: Project - Sale - Expense
|
||||
- **Version**: 1.0
|
||||
- **Category**: Hidden
|
||||
- **License**: LGPL-3
|
||||
- **Installable**: False
|
||||
|
||||
## Source
|
||||
|
||||
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `project_sale_expense`.
|
||||
|
||||
## License
|
||||
|
||||
This package maintains the original LGPL-3 license from the upstream Odoo project.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Overview: doc/OVERVIEW.md
|
||||
- Architecture: doc/ARCHITECTURE.md
|
||||
- Models: doc/MODELS.md
|
||||
- Controllers: doc/CONTROLLERS.md
|
||||
- Wizards: doc/WIZARDS.md
|
||||
- Reports: doc/REPORTS.md
|
||||
- Security: doc/SECURITY.md
|
||||
- Install: doc/INSTALL.md
|
||||
- Usage: doc/USAGE.md
|
||||
- Configuration: doc/CONFIGURATION.md
|
||||
- Dependencies: doc/DEPENDENCIES.md
|
||||
- Troubleshooting: doc/TROUBLESHOOTING.md
|
||||
- FAQ: doc/FAQ.md
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
U[Users] -->|HTTP| V[Views and QWeb Templates]
|
||||
V --> C[Controllers]
|
||||
V --> W[Wizards – Transient Models]
|
||||
C --> M[Models and ORM]
|
||||
W --> M
|
||||
M --> R[Reports]
|
||||
DX[Data XML] --> M
|
||||
S[Security – ACLs and Groups] -. enforces .-> M
|
||||
|
||||
subgraph Project_sale_expense Module - project_sale_expense
|
||||
direction LR
|
||||
M:::layer
|
||||
W:::layer
|
||||
C:::layer
|
||||
V:::layer
|
||||
R:::layer
|
||||
S:::layer
|
||||
DX:::layer
|
||||
end
|
||||
|
||||
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
|
||||
```
|
||||
|
||||
Notes
|
||||
- Views include tree/form/kanban templates and report templates.
|
||||
- Controllers provide website/portal routes when present.
|
||||
- Wizards are UI flows implemented with `models.TransientModel`.
|
||||
- Data XML loads data/demo records; Security defines groups and access.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Configuration
|
||||
|
||||
Refer to Odoo settings for project_sale_expense. Configure related models, access rights, and options as needed.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Controllers
|
||||
|
||||
This module does not define custom HTTP controllers.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Dependencies
|
||||
|
||||
This addon depends on:
|
||||
|
||||
- [sale_project](../../odoo-bringout-oca-ocb-sale_project)
|
||||
- [sale_expense](../../odoo-bringout-oca-ocb-sale_expense)
|
||||
- [project_hr_expense](../../odoo-bringout-oca-ocb-project_hr_expense)
|
||||
4
odoo-bringout-oca-ocb-project_sale_expense/doc/FAQ.md
Normal file
4
odoo-bringout-oca-ocb-project_sale_expense/doc/FAQ.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# FAQ
|
||||
|
||||
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
|
||||
- Q: How to enable? A: Start server with --addon project_sale_expense or install in UI.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Install
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-ocb-project_sale_expense"
|
||||
# or
|
||||
uv pip install odoo-bringout-oca-ocb-project_sale_expense"
|
||||
```
|
||||
12
odoo-bringout-oca-ocb-project_sale_expense/doc/MODELS.md
Normal file
12
odoo-bringout-oca-ocb-project_sale_expense/doc/MODELS.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Models
|
||||
|
||||
Detected core models and extensions in project_sale_expense.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class project_project
|
||||
```
|
||||
|
||||
Notes
|
||||
- Classes show model technical names; fields omitted for brevity.
|
||||
- Items listed under _inherit are extensions of existing models.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# Overview
|
||||
|
||||
Packaged Odoo addon: project_sale_expense. Provides features documented in upstream Odoo 16 under this addon.
|
||||
|
||||
- Source: OCA/OCB 16.0, addon project_sale_expense
|
||||
- License: LGPL-3
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Reports
|
||||
|
||||
This module does not define custom reports.
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Security
|
||||
|
||||
This module does not define custom security rules or access controls beyond Odoo defaults.
|
||||
|
||||
Default Odoo security applies:
|
||||
- Base user access through standard groups
|
||||
- Model access inherited from dependencies
|
||||
- No custom row-level security rules
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Troubleshooting
|
||||
|
||||
- Ensure Python and Odoo environment matches repo guidance.
|
||||
- Check database connectivity and logs if startup fails.
|
||||
- Validate that dependent addons listed in DEPENDENCIES.md are installed.
|
||||
7
odoo-bringout-oca-ocb-project_sale_expense/doc/USAGE.md
Normal file
7
odoo-bringout-oca-ocb-project_sale_expense/doc/USAGE.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Usage
|
||||
|
||||
Start Odoo including this addon (from repo root):
|
||||
|
||||
```bash
|
||||
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon project_sale_expense
|
||||
```
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Wizards
|
||||
|
||||
This module does not include UI wizards.
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Project - Sale - Expense',
|
||||
'version': '1.0',
|
||||
'description': 'Adds a full traceability of reinvoice expenses on the profitability report.',
|
||||
'license': 'LGPL-3',
|
||||
'category': 'Hidden',
|
||||
'depends': ['sale_project', 'sale_expense', 'project_hr_expense'],
|
||||
'auto_install': True,
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * project_sale_expense
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-02-06 13:32+0000\n"
|
||||
"PO-Revision-Date: 2024-02-06 13:32+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: project_sale_expense
|
||||
#: model:ir.model,name:project_sale_expense.model_project_project
|
||||
msgid "Project"
|
||||
msgstr "Projekat"
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * project_sale_expense
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-02-06 13:32+0000\n"
|
||||
"PO-Revision-Date: 2024-02-06 13:32+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: project_sale_expense
|
||||
#: model:ir.model,name:project_sale_expense.model_project_project
|
||||
msgid "Project"
|
||||
msgstr ""
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import project_project
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class Project(models.Model):
|
||||
_inherit = 'project.project'
|
||||
|
||||
def _get_expenses_profitability_items(self, with_action=True):
|
||||
if not self.analytic_account_id:
|
||||
return {}
|
||||
can_see_expense = with_action and self.user_has_groups('hr_expense.group_hr_expense_team_approver')
|
||||
query = self.env['hr.expense']._search([('is_refused', '=', False), ('state', 'in', ['approved', 'done'])])
|
||||
query.add_where('hr_expense.analytic_distribution ? %s', [str(self.analytic_account_id.id)])
|
||||
query.order = None
|
||||
query_string, query_param = query.select('sale_order_id', 'product_id', 'currency_id', 'array_agg(id) as ids', 'SUM(untaxed_amount) as untaxed_amount')
|
||||
query_string = f"{query_string} GROUP BY sale_order_id, product_id, currency_id"
|
||||
self._cr.execute(query_string, query_param)
|
||||
expenses_read_group = [expense for expense in self._cr.dictfetchall()]
|
||||
if not expenses_read_group:
|
||||
return {}
|
||||
expenses_per_so_id = {}
|
||||
expense_ids = []
|
||||
amount_billed = 0.0
|
||||
dict_amount_per_currency = defaultdict(lambda: 0.0)
|
||||
for res in expenses_read_group:
|
||||
so_id = res['sale_order_id']
|
||||
product_id = res['product_id']
|
||||
expenses_per_so_id.setdefault(so_id, {})[product_id] = res['ids']
|
||||
if can_see_expense:
|
||||
expense_ids.extend(res['ids'])
|
||||
dict_amount_per_currency[res['currency_id']] += res['untaxed_amount']
|
||||
date = fields.Date.context_today(self)
|
||||
for currency_id in dict_amount_per_currency:
|
||||
if currency_id == self.company_id.currency_id.id:
|
||||
amount_billed += dict_amount_per_currency[currency_id]
|
||||
continue
|
||||
currency = self.env['res.currency'].browse(currency_id)
|
||||
amount_billed += currency._convert(dict_amount_per_currency[currency_id], self.company_id.currency_id, self.company_id, date)
|
||||
|
||||
sol_read_group = self.env['sale.order.line'].sudo()._read_group(
|
||||
[
|
||||
('order_id', 'in', list(expenses_per_so_id.keys())),
|
||||
('is_expense', '=', True),
|
||||
('state', 'in', ['sale', 'done']),
|
||||
],
|
||||
['order_id', 'product_id', 'untaxed_amount_to_invoice', 'untaxed_amount_invoiced'],
|
||||
['order_id', 'product_id'],
|
||||
lazy=False)
|
||||
total_amount_expense_invoiced = total_amount_expense_to_invoice = 0.0
|
||||
reinvoice_expense_ids = []
|
||||
for res in sol_read_group:
|
||||
expense_data_per_product_id = expenses_per_so_id[res['order_id'][0]]
|
||||
product_id = res['product_id'][0]
|
||||
if product_id in expense_data_per_product_id:
|
||||
total_amount_expense_to_invoice += res['untaxed_amount_to_invoice']
|
||||
total_amount_expense_invoiced += res['untaxed_amount_invoiced']
|
||||
reinvoice_expense_ids += expense_data_per_product_id[product_id]
|
||||
section_id = 'expenses'
|
||||
sequence = self._get_profitability_sequence_per_invoice_type()[section_id]
|
||||
expense_data = {
|
||||
'costs': {
|
||||
'id': section_id,
|
||||
'sequence': sequence,
|
||||
'billed': -amount_billed,
|
||||
'to_bill': 0.0,
|
||||
},
|
||||
}
|
||||
if reinvoice_expense_ids:
|
||||
expense_data['revenues'] = {
|
||||
'id': section_id,
|
||||
'sequence': sequence,
|
||||
'invoiced': total_amount_expense_invoiced,
|
||||
'to_invoice': total_amount_expense_to_invoice,
|
||||
}
|
||||
if can_see_expense:
|
||||
def get_action(res_ids):
|
||||
args = [section_id, [('id', 'in', res_ids)]]
|
||||
if len(res_ids) == 1:
|
||||
args.append(res_ids[0])
|
||||
return {'name': 'action_profitability_items', 'type': 'object', 'args': json.dumps(args)}
|
||||
|
||||
if reinvoice_expense_ids:
|
||||
expense_data['revenues']['action'] = get_action(reinvoice_expense_ids)
|
||||
if expense_ids:
|
||||
expense_data['costs']['action'] = get_action(expense_ids)
|
||||
return expense_data
|
||||
|
||||
def _get_already_included_profitability_invoice_line_ids(self):
|
||||
move_line_ids = super()._get_already_included_profitability_invoice_line_ids()
|
||||
query = self.env['hr.expense']._search([('is_refused', '=', False), ('state', 'in', ['approved', 'done'])])
|
||||
query.add_where('hr_expense.analytic_distribution ? %s', [str(self.analytic_account_id.id)])
|
||||
query.order = None
|
||||
query_string, query_param = query.select('sale_order_id')
|
||||
query_string = f"{query_string} GROUP BY sale_order_id"
|
||||
self._cr.execute(query_string, query_param)
|
||||
expenses_read_group = list(self._cr.dictfetchall())
|
||||
if not expenses_read_group:
|
||||
return move_line_ids
|
||||
for res in expenses_read_group:
|
||||
sale_order = self.env['sale.order'].browse(res['sale_order_id'])
|
||||
move_line_ids.extend(sale_order.invoice_ids.mapped('invoice_line_ids').ids)
|
||||
return move_line_ids
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_project_profitability
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.project_hr_expense.tests.test_project_profitability import TestProjectHrExpenseProfitabilityCommon
|
||||
from odoo.addons.sale.tests.common import TestSaleCommon
|
||||
from odoo.addons.sale_project.tests.test_project_profitability import TestProjectProfitabilityCommon
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestProjectSaleExpenseProfitability(TestProjectProfitabilityCommon, TestProjectHrExpenseProfitabilityCommon, TestSaleCommon):
|
||||
def test_project_profitability(self):
|
||||
expense = self.env['hr.expense'].create({
|
||||
'name': 'expense',
|
||||
'product_id': self.company_data['product_order_sales_price'].id,
|
||||
'unit_amount': self.company_data['product_order_sales_price'].list_price,
|
||||
'employee_id': self.expense_employee.id,
|
||||
'analytic_distribution': {self.project.analytic_account_id.id: 100},
|
||||
'sale_order_id': self.sale_order.id,
|
||||
})
|
||||
|
||||
# See method definition in `project_hr_expense.tests.test_project_profitability`
|
||||
expense_sheet = self.check_project_profitability_before_creating_and_approving_expense_sheet(
|
||||
expense,
|
||||
self.project,
|
||||
self.project_profitability_items_empty)
|
||||
|
||||
expense_profitability = self.project._get_expenses_profitability_items(False)
|
||||
sequence_per_invoice_type = self.project._get_profitability_sequence_per_invoice_type()
|
||||
self.assertIn('expenses', sequence_per_invoice_type)
|
||||
expense_sequence = sequence_per_invoice_type['expenses']
|
||||
|
||||
self.assertDictEqual(
|
||||
expense_profitability.get('revenues', {}),
|
||||
{},
|
||||
)
|
||||
self.assertDictEqual(
|
||||
expense_profitability['costs'],
|
||||
{'id': 'expenses', 'sequence': expense_sequence, 'billed': -280.0, 'to_bill': 0.0},
|
||||
)
|
||||
|
||||
expense_sheet.action_sheet_move_create()
|
||||
|
||||
self.assertRecordValues(self.sale_order.order_line, [
|
||||
# Original SO line:
|
||||
{
|
||||
'product_id': self.product_delivery_service.id,
|
||||
'qty_delivered': 0.0,
|
||||
'product_uom_qty': 10,
|
||||
'is_expense': False,
|
||||
},
|
||||
{
|
||||
'product_id': self.company_data['product_order_sales_price'].id,
|
||||
'qty_delivered': 1.0,
|
||||
'product_uom_qty': 1.0,
|
||||
'is_expense': True,
|
||||
},
|
||||
])
|
||||
expense_sol = self.sale_order.order_line.filtered(lambda sol: sol.product_id == self.company_data['product_order_sales_price'])
|
||||
|
||||
expense_profitability = self.project._get_expenses_profitability_items(False)
|
||||
self.assertDictEqual(
|
||||
expense_profitability.get('revenues', {}),
|
||||
{'id': 'expenses', 'sequence': expense_sequence, 'invoiced': 0.0, 'to_invoice': expense_sol.untaxed_amount_to_invoice},
|
||||
)
|
||||
self.assertDictEqual(
|
||||
expense_profitability['costs'],
|
||||
{'id': 'expenses', 'sequence': expense_sequence, 'billed': -280.0, 'to_bill': 0.0},
|
||||
)
|
||||
|
||||
self.assertDictEqual(
|
||||
self.project._get_profitability_items(False),
|
||||
{
|
||||
'revenues': {
|
||||
'data': [expense_profitability['revenues']],
|
||||
'total': {k: v for k, v in expense_profitability['revenues'].items() if k in ['to_invoice', 'invoiced']},
|
||||
},
|
||||
'costs': {
|
||||
'data': [expense_profitability['costs']],
|
||||
'total': {k: v for k, v in expense_profitability['costs'].items() if k in ['to_bill', 'billed']},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
invoice = self.env['sale.advance.payment.inv'] \
|
||||
.with_context({
|
||||
'active_model': 'sale.order',
|
||||
'active_id': self.sale_order.id,
|
||||
}).create({
|
||||
'advance_payment_method': 'delivered',
|
||||
})._create_invoices(self.sale_order)
|
||||
invoice.action_post()
|
||||
|
||||
expense_profitability = self.project._get_expenses_profitability_items(False)
|
||||
self.assertDictEqual(
|
||||
expense_profitability.get('revenues', {}),
|
||||
{'id': 'expenses', 'sequence': expense_sequence, 'invoiced': expense_sol.untaxed_amount_invoiced, 'to_invoice': 0.0},
|
||||
)
|
||||
|
||||
credit_note = invoice._reverse_moves()
|
||||
credit_note.action_post()
|
||||
|
||||
expense_profitability = self.project._get_expenses_profitability_items(False)
|
||||
self.assertDictEqual(
|
||||
expense_profitability.get('revenues', {}),
|
||||
{'id': 'expenses', 'sequence': expense_sequence, 'invoiced': 0.0, 'to_invoice': expense_sol.untaxed_amount_to_invoice},
|
||||
)
|
||||
|
||||
self.sale_order._action_cancel()
|
||||
expense_profitability = self.project._get_expenses_profitability_items(False)
|
||||
self.assertDictEqual(
|
||||
expense_profitability.get('revenues', {}),
|
||||
{},
|
||||
)
|
||||
self.assertDictEqual(
|
||||
expense_profitability['costs'],
|
||||
{'id': 'expenses', 'sequence': expense_sequence, 'billed': -280.0, 'to_bill': 0.0},
|
||||
)
|
||||
|
||||
expense_sheet.refuse_sheet('Test Cancel Expense')
|
||||
expense_profitability = self.project._get_expenses_profitability_items(False)
|
||||
self.assertDictEqual(
|
||||
expense_profitability.get('revenues', {}),
|
||||
{},
|
||||
)
|
||||
self.assertDictEqual(
|
||||
expense_profitability.get('costs', {}),
|
||||
{},
|
||||
)
|
||||
|
||||
def test_project_profitability_2(self):
|
||||
"""
|
||||
Test Case:
|
||||
==========
|
||||
- Create an expense for a project.
|
||||
- post it's entry moves
|
||||
- create an invoice for the sale order linked to the expense
|
||||
- post the invoice
|
||||
- the project profitability should not include the Customer invoice
|
||||
linked to the expense in the revenues, as the Expenses will be there.
|
||||
"""
|
||||
|
||||
product_new_project_task = self.env['product.product'].create({
|
||||
'name': "Service, create task in new project",
|
||||
'standard_price': 30,
|
||||
'list_price': 90,
|
||||
'type': 'service',
|
||||
'default_code': 'SERV-ORDERED2',
|
||||
'service_tracking': 'task_in_project',
|
||||
})
|
||||
|
||||
sale_order = self.env['sale.order'].with_context(tracking_disable=True).create({
|
||||
'partner_id': self.partner.id,
|
||||
'partner_invoice_id': self.partner.id,
|
||||
'partner_shipping_id': self.partner.id,
|
||||
})
|
||||
|
||||
self.env['sale.order.line'].create({
|
||||
'product_id': product_new_project_task.id,
|
||||
'product_uom_qty': 1,
|
||||
'order_id': sale_order.id,
|
||||
})
|
||||
|
||||
sale_order.action_confirm()
|
||||
project = sale_order.order_line.project_id
|
||||
|
||||
expense = self.env['hr.expense'].create({
|
||||
'name': 'expense',
|
||||
'product_id': self.company_data['product_order_cost'].id,
|
||||
'unit_amount': self.company_data['product_order_cost'].list_price,
|
||||
'employee_id': self.expense_employee.id,
|
||||
'analytic_distribution': {project.analytic_account_id.id: 100},
|
||||
'sale_order_id': sale_order.id,
|
||||
})
|
||||
|
||||
expense_sheet_vals_list = expense._get_default_expense_sheet_values()
|
||||
expense_sheet = self.env['hr.expense.sheet'].create(expense_sheet_vals_list)
|
||||
expense_sheet.action_submit_sheet()
|
||||
expense_sheet.approve_expense_sheets()
|
||||
expense_sheet.action_sheet_move_create()
|
||||
|
||||
invoice = sale_order._create_invoices()
|
||||
invoice.action_post()
|
||||
|
||||
sale_items = project.sudo()._get_sale_order_items()
|
||||
domain = [
|
||||
('order_id', 'in', sale_items.order_id.ids),
|
||||
'|',
|
||||
'|',
|
||||
('project_id', 'in', project.ids),
|
||||
('project_id', '=', False),
|
||||
('id', 'in', sale_items.ids),
|
||||
]
|
||||
|
||||
revenue_items_from_sol = project._get_revenues_items_from_sol(domain, False)
|
||||
expense_profitability = project._get_expenses_profitability_items(False)
|
||||
project_profitability = project._get_profitability_items(False)
|
||||
# invoice linked to the expense should not be included in the revenues
|
||||
self.assertDictEqual(
|
||||
project_profitability.get('revenues', {}),
|
||||
{
|
||||
'data': [expense_profitability['revenues'], revenue_items_from_sol['data'][0]],
|
||||
'total': {'invoiced': expense_profitability['revenues']['invoiced'] + revenue_items_from_sol['total']['invoiced'], 'to_invoice': expense_profitability['revenues']['to_invoice'] + revenue_items_from_sol['total']['to_invoice']},
|
||||
},
|
||||
)
|
||||
|
||||
def test_project_profitability_multi_currency(self):
|
||||
currency_rate = 0.5
|
||||
other_currency = self.env['res.currency'].create({
|
||||
'name': 'TEST',
|
||||
'symbol': 'T',
|
||||
'rate_ids': [(0, 0, {
|
||||
'name': '2020-01-01',
|
||||
'rate': currency_rate,
|
||||
})],
|
||||
})
|
||||
amount_in_other_currency = 100
|
||||
expense = self.env['hr.expense'].create({
|
||||
'name': 'Expense in another currency',
|
||||
'product_id': self.company_data['product_order_sales_price'].id,
|
||||
'total_amount': amount_in_other_currency,
|
||||
'employee_id': self.expense_employee.id,
|
||||
'analytic_distribution': {self.project.analytic_account_id.id: 100},
|
||||
'sale_order_id': self.sale_order.id,
|
||||
'currency_id': other_currency.id,
|
||||
})
|
||||
self.check_project_profitability_before_creating_and_approving_expense_sheet(expense, self.project, self.project_profitability_items_empty)
|
||||
|
||||
expense_profitability = self.project._get_expenses_profitability_items(False)
|
||||
self.assertTrue(expense_profitability['costs']['billed'] == - amount_in_other_currency / currency_rate)
|
||||
44
odoo-bringout-oca-ocb-project_sale_expense/pyproject.toml
Normal file
44
odoo-bringout-oca-ocb-project_sale_expense/pyproject.toml
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
[project]
|
||||
name = "odoo-bringout-oca-ocb-project_sale_expense"
|
||||
version = "16.0.0"
|
||||
description = "Project - Sale - Expense - Odoo addon"
|
||||
authors = [
|
||||
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
|
||||
]
|
||||
dependencies = [
|
||||
"odoo-bringout-oca-ocb-sale_project>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-sale_expense>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-project_hr_expense>=16.0.0",
|
||||
"requests>=2.25.1"
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">= 3.11"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Office/Business",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://github.com/bringout/0"
|
||||
repository = "https://github.com/bringout/0"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.metadata]
|
||||
allow-direct-references = true
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["project_sale_expense"]
|
||||
|
||||
[tool.rye]
|
||||
managed = true
|
||||
dev-dependencies = [
|
||||
"pytest>=8.4.1",
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue