Pembuatan Business category untuk semuanya

This commit is contained in:
2026-03-15 22:35:52 +07:00
parent b2f7b46042
commit 87b9a2b7f5
105 changed files with 3658 additions and 4183 deletions
+1 -3
View File
@@ -1,5 +1,3 @@
{
"python.defaultInterpreterPath": "C:/odoo14c/python/python.exe",
"python.debugpyPath": "C:/odoo14c/python/Lib/site-packages",
"debugpy.adapterPython": "C:/odoo14c/python/python.exe"
"python.defaultInterpreterPath": "C:/odoo14c/python/python.exe"
}
+50
View File
@@ -0,0 +1,50 @@
# Verifikasi Report Business Category
## 1) Upgrade Modul
Jalankan:
- `upgrade_business_category_reports.bat`
## 2) Verifikasi Menu Report
### Sales
- Menu: **Sales > Orders > Sales by Category**
- Cek tersedia view: Tree, Pivot, Graph
- Cek default grouping: `business_category_id`
### Purchase
- Menu: **Purchase > Purchases > Purchases by Category**
- Cek tersedia view: Tree, Pivot, Graph
- Cek default grouping: `business_category_id`
### Expense
- Menu: **Expenses > Expenses > Expenses by Category**
- Cek tersedia view: Tree, Pivot, Graph
- Cek default grouping: `business_category_id`
### Inventory
- Menu: **Inventory > Operations > Inventory by Category**
- Cek tersedia view: Tree, Pivot, Graph
- Cek default grouping: `business_category_id`
### CRM Pipeline
- Menu: **CRM > Reporting > Pipeline by Category**
- Cek domain otomatis: hanya `type = opportunity`
- Cek view: Tree, Pivot, Graph, Kanban, Form
### CRM Activity
- Menu: **CRM > Reporting > Activity History**
- Cek default grouping: `business_category_id`
- Cek view: Tree, Form, Pivot, Graph
## 3) Verifikasi Keamanan Akses
- Login sebagai user category A: pastikan data category B tidak muncul di report.
- Login sebagai user category B: pastikan data category A tidak muncul di report.
- Login sysadmin: boleh melihat semua sesuai company.
## 4) Validasi Cepat Runtime
Jalankan script:
- `python test_business_category_access_runtime.py`
Expected: semua skenario lintas category = **deny**.
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env python3
import xmlrpc.client
URL = "http://localhost:8070"
DB = "kanjabung_MRP"
USER = "admin"
PWD = "admin"
TARGETS = [
"grt_crm_business_category",
"grt_sales_business_category",
"grt_purchase_business_category",
"grt_expense_business_category",
"grt_inventory_business_category",
]
common = xmlrpc.client.ServerProxy("%s/xmlrpc/2/common" % URL)
uid = common.authenticate(DB, USER, PWD, {})
if not uid:
raise SystemExit("AUTH_FAILED")
models = xmlrpc.client.ServerProxy("%s/xmlrpc/2/object" % URL)
mods = models.execute_kw(
DB,
uid,
PWD,
"ir.module.module",
"search_read",
[[("name", "in", TARGETS)]],
{"fields": ["name", "state", "installed_version"], "order": "name asc"},
)
for m in mods:
print("%s | %s | %s" % (m.get("name"), m.get("state"), m.get("installed_version")))
@@ -18,6 +18,7 @@ This enables different staging flow per business category through team pipelines
"views/assets.xml",
"views/crm_activity_history_views.xml",
"views/crm_business_category_views.xml",
"views/crm_pipeline_report_views.xml",
"views/crm_lead_views.xml",
"views/crm_menu_views.xml",
"views/crm_stage_views.xml",
@@ -3,29 +3,29 @@
<record id="crm_lead_business_category_rule_user" model="ir.rule">
<field name="name">CRM Lead by allowed business category</field>
<field name="model_id" ref="crm.model_crm_lead"/>
<field name="domain_force">[('business_category_id', 'in', user.allowed_business_category_ids.ids)]</field>
<field name="domain_force">[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]</field>
<field name="groups" eval="[(4, ref('crm.group_use_lead'))]"/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_write" eval="0"/>
<field name="perm_create" eval="1"/>
<field name="perm_unlink" eval="1"/>
<field name="perm_unlink" eval="0"/>
</record>
<record id="crm_team_business_category_rule_user" model="ir.rule">
<field name="name">CRM Team by allowed business category</field>
<field name="model_id" ref="sales_team.model_crm_team"/>
<field name="domain_force">[('business_category_id', 'in', user.allowed_business_category_ids.ids)]</field>
<field name="domain_force">[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]</field>
<field name="groups" eval="[(4, ref('crm.group_use_lead'))]"/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_create" eval="1"/>
<field name="perm_unlink" eval="1"/>
<field name="perm_write" eval="0"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="0"/>
</record>
<record id="crm_business_category_rule_user" model="ir.rule">
<field name="name">Business Category by user allow list</field>
<field name="model_id" ref="model_crm_business_category"/>
<field name="domain_force">[('id', 'in', user.allowed_business_category_ids.ids)]</field>
<field name="domain_force">[('company_id','in',user.company_ids.ids),('id', 'in', user.effective_business_category_ids.ids)]</field>
<field name="groups" eval="[(4, ref('crm.group_use_lead'))]"/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="0"/>
@@ -36,7 +36,7 @@
<record id="crm_lead_business_category_rule_manager" model="ir.rule">
<field name="name">CRM Lead full access for manager</field>
<field name="model_id" ref="crm.model_crm_lead"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="domain_force">[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]</field>
<field name="groups" eval="[(4, ref('sales_team.group_sale_manager'))]"/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/>
@@ -47,7 +47,7 @@
<record id="crm_team_business_category_rule_manager" model="ir.rule">
<field name="name">CRM Team full access for manager</field>
<field name="model_id" ref="sales_team.model_crm_team"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="domain_force">[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]</field>
<field name="groups" eval="[(4, ref('sales_team.group_sale_manager'))]"/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/>
@@ -58,7 +58,7 @@
<record id="crm_business_category_rule_manager" model="ir.rule">
<field name="name">Business Category full access for manager</field>
<field name="model_id" ref="model_crm_business_category"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="domain_force">[('company_id','in',user.company_ids.ids),('id','in',user.effective_business_category_ids.ids)]</field>
<field name="groups" eval="[(4, ref('sales_team.group_sale_manager'))]"/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/>
@@ -3,10 +3,10 @@ crm_lead_business_category_rule_user_read_create,CRM Lead read/create by effecti
crm_lead_business_category_rule_user_write_own,CRM Lead write own by effective business category,crm.model_crm_lead,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids),'|',('user_id','=',user.id),('create_uid','=',user.id)]",crm.group_use_lead,0,1,0,1
crm_team_business_category_rule_user,CRM Team read by effective business category,sales_team.model_crm_team,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",crm.group_use_lead,1,0,0,0
crm_business_category_rule_user,Business Category by user effective list,model_crm_business_category,"[('company_id','in',user.company_ids.ids),('id','in',user.effective_business_category_ids.ids)]",crm.group_use_lead,1,0,0,0
crm_lead_business_category_rule_manager,CRM Lead full access for manager,crm.model_crm_lead,"[('company_id','in',user.company_ids.ids)]",sales_team.group_sale_manager,1,1,1,1
crm_team_business_category_rule_manager,CRM Team full access for manager,sales_team.model_crm_team,"[('company_id','in',user.company_ids.ids)]",sales_team.group_sale_manager,1,1,1,1
crm_business_category_rule_manager,Business Category full access for manager,model_crm_business_category,"[('company_id','in',user.company_ids.ids)]",sales_team.group_sale_manager,1,1,1,1
crm_lead_business_category_rule_manager,CRM Lead full access for manager in effective business category,crm.model_crm_lead,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",sales_team.group_sale_manager,1,1,1,1
crm_team_business_category_rule_manager,CRM Team full access for manager in effective business category,sales_team.model_crm_team,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",sales_team.group_sale_manager,1,1,1,1
crm_business_category_rule_manager,Business Category access for manager in effective business category,model_crm_business_category,"[('company_id','in',user.company_ids.ids),('id','in',user.effective_business_category_ids.ids)]",sales_team.group_sale_manager,1,1,1,1
crm_activity_history_rule_user,CRM Activity History by effective business category,model_crm_activity_history,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",crm.group_use_lead,1,0,0,0
crm_activity_history_rule_manager,CRM Activity History full access for manager,model_crm_activity_history,"[('company_id','in',user.company_ids.ids)]",sales_team.group_sale_manager,1,1,1,1
crm_activity_history_rule_manager,CRM Activity History full access for manager in effective business category,model_crm_activity_history,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",sales_team.group_sale_manager,1,1,1,1
crm_team_business_category_rule_sysadmin,CRM Team full access for system admin,sales_team.model_crm_team,"[('company_id','in',user.company_ids.ids)]",base.group_system,1,1,1,1
crm_activity_history_rule_sysadmin,CRM Activity History full access for system admin,model_crm_activity_history,"[('company_id','in',user.company_ids.ids)]",base.group_system,1,1,1,1
1 id name model_id:id domain_force groups:id perm_read perm_write perm_create perm_unlink
3 crm_lead_business_category_rule_user_write_own CRM Lead write own by effective business category crm.model_crm_lead [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids),'|',('user_id','=',user.id),('create_uid','=',user.id)] crm.group_use_lead 0 1 0 1
4 crm_team_business_category_rule_user CRM Team read by effective business category sales_team.model_crm_team [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] crm.group_use_lead 1 0 0 0
5 crm_business_category_rule_user Business Category by user effective list model_crm_business_category [('company_id','in',user.company_ids.ids),('id','in',user.effective_business_category_ids.ids)] crm.group_use_lead 1 0 0 0
6 crm_lead_business_category_rule_manager CRM Lead full access for manager CRM Lead full access for manager in effective business category crm.model_crm_lead [('company_id','in',user.company_ids.ids)] [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] sales_team.group_sale_manager 1 1 1 1
7 crm_team_business_category_rule_manager CRM Team full access for manager CRM Team full access for manager in effective business category sales_team.model_crm_team [('company_id','in',user.company_ids.ids)] [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] sales_team.group_sale_manager 1 1 1 1
8 crm_business_category_rule_manager Business Category full access for manager Business Category access for manager in effective business category model_crm_business_category [('company_id','in',user.company_ids.ids)] [('company_id','in',user.company_ids.ids),('id','in',user.effective_business_category_ids.ids)] sales_team.group_sale_manager 1 1 1 1
9 crm_activity_history_rule_user CRM Activity History by effective business category model_crm_activity_history [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] crm.group_use_lead 1 0 0 0
10 crm_activity_history_rule_manager CRM Activity History full access for manager CRM Activity History full access for manager in effective business category model_crm_activity_history [('company_id','in',user.company_ids.ids)] [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] sales_team.group_sale_manager 1 1 1 1
11 crm_team_business_category_rule_sysadmin CRM Team full access for system admin sales_team.model_crm_team [('company_id','in',user.company_ids.ids)] base.group_system 1 1 1 1
12 crm_activity_history_rule_sysadmin CRM Activity History full access for system admin model_crm_activity_history [('company_id','in',user.company_ids.ids)] base.group_system 1 1 1 1
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_crm_pipeline_pivot_business_category" model="ir.ui.view">
<field name="name">crm.lead.pipeline.pivot.business.category</field>
<field name="model">crm.lead</field>
<field name="arch" type="xml">
<pivot string="Pipeline by Business Category">
<field name="business_category_id" type="row"/>
<field name="team_id" type="row"/>
<field name="stage_id" type="col"/>
<field name="create_date" interval="month" type="col"/>
<field name="expected_revenue" type="measure"/>
<field name="id" type="measure" string="Opportunities"/>
</pivot>
</field>
</record>
<record id="view_crm_pipeline_graph_business_category" model="ir.ui.view">
<field name="name">crm.lead.pipeline.graph.business.category</field>
<field name="model">crm.lead</field>
<field name="arch" type="xml">
<graph string="Pipeline by Business Category" type="bar">
<field name="business_category_id"/>
<field name="expected_revenue" type="measure"/>
</graph>
</field>
</record>
<record id="action_crm_pipeline_business_category_report" model="ir.actions.act_window">
<field name="name">Pipeline by Business Category</field>
<field name="res_model">crm.lead</field>
<field name="view_mode">tree,pivot,graph,kanban,form</field>
<field name="domain">[('type', '=', 'opportunity')]</field>
<field name="context">{'group_by': 'business_category_id'}</field>
<field name="search_view_id" ref="view_crm_case_opportunities_filter_business_category"/>
</record>
<menuitem id="menu_crm_pipeline_business_category_report"
name="Pipeline by Category"
parent="crm.crm_menu_report"
action="action_crm_pipeline_business_category_report"
sequence="36"
groups="crm.group_use_lead,sales_team.group_sale_manager,base.group_system"/>
<record id="action_crm_activity_history_report" model="ir.actions.act_window">
<field name="context">{'group_by': 'business_category_id'}</field>
</record>
</odoo>
@@ -0,0 +1 @@
from . import models
@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
{
"name": "GRT Expense Business Category",
"summary": "Business category segregation and expense teams for expenses",
"description": """
Adds business category handling to Expenses with dedicated Expense Teams,
record rules per business category, and automatic analytic account propagation
for expense transactions and accounting entries.
""",
"author": "PT Gagak Rimang Teknologi",
"website": "https://rimang.id",
"category": "Human Resources/Expenses",
"version": "14.0.1.0.0",
"depends": [
"hr_expense",
"account",
"grt_crm_business_category",
],
"data": [
"security/ir.model.access.csv",
"security/ir.rule.csv",
"views/crm_business_category_views.xml",
"views/expense_team_views.xml",
"views/hr_expense_views.xml",
"views/expense_report_business_category_views.xml",
"views/expense_menu_views.xml",
],
"installable": True,
"application": False,
}
@@ -0,0 +1,4 @@
from . import account_move
from . import crm_business_category
from . import hr_expense
from . import expense_team
@@ -0,0 +1,105 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class AccountMove(models.Model):
_inherit = "account.move"
expense_business_category_id = fields.Many2one(
"crm.business.category",
string="Expense Business Category",
domain="[('company_id', '=', company_id)]",
ondelete="restrict",
copy=False,
)
expense_analytic_account_id = fields.Many2one(
"account.analytic.account",
string="Expense Analytic Account",
domain="[('company_id', '=', company_id)]",
ondelete="restrict",
copy=False,
)
@api.onchange("expense_business_category_id")
def _onchange_expense_business_category_id_analytic(self):
for move in self:
move.expense_analytic_account_id = move.expense_business_category_id.expense_analytic_account_id
@api.model_create_multi
def create(self, vals_list):
vals_list = [self._prepare_expense_business_category_vals(vals) for vals in vals_list]
return super().create(vals_list)
def write(self, vals):
vals = self._prepare_expense_business_category_vals(vals)
return super().write(vals)
def _prepare_expense_business_category_vals(self, vals):
vals = dict(vals)
if vals.get("expense_business_category_id") and not vals.get("expense_analytic_account_id"):
category = self.env["crm.business.category"].browse(vals["expense_business_category_id"])
if category.expense_analytic_account_id:
vals["expense_analytic_account_id"] = category.expense_analytic_account_id.id
return vals
@api.constrains("company_id", "expense_business_category_id", "expense_analytic_account_id")
def _check_expense_business_category_analytic_company(self):
for move in self:
if move.expense_business_category_id and move.expense_business_category_id.company_id != move.company_id:
raise ValidationError(
_("Journal Entry '%s' uses an Expense Business Category from another company.") % (move.display_name,)
)
if move.expense_analytic_account_id and move.expense_analytic_account_id.company_id != move.company_id:
raise ValidationError(
_("Journal Entry '%s' uses an Expense Analytic Account from another company.") % (move.display_name,)
)
if (
move.expense_business_category_id
and move.expense_business_category_id.expense_analytic_account_id
and move.expense_analytic_account_id
and move.expense_business_category_id.expense_analytic_account_id != move.expense_analytic_account_id
):
raise ValidationError(
_(
"Journal Entry '%s' must use the Expense Analytic Account configured on its Business Category."
)
% (move.display_name,)
)
def action_post(self):
result = super().action_post()
for move in self:
analytic = move.expense_analytic_account_id
if not analytic:
continue
lines = move.line_ids.filtered(
lambda line: line.account_id.internal_type not in ("receivable", "payable")
and not line.analytic_account_id
)
if lines:
lines.write({"analytic_account_id": analytic.id})
return result
class AccountMoveLine(models.Model):
_inherit = "account.move.line"
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get("analytic_account_id"):
continue
move_id = vals.get("move_id")
account_id = vals.get("account_id")
if not move_id or not account_id:
continue
move = self.env["account.move"].browse(move_id)
account = self.env["account.account"].browse(account_id)
if (
move
and move.expense_analytic_account_id
and account
and account.internal_type not in ("receivable", "payable")
):
vals["analytic_account_id"] = move.expense_analytic_account_id.id
return super().create(vals_list)
@@ -0,0 +1,27 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class CrmBusinessCategory(models.Model):
_inherit = "crm.business.category"
expense_analytic_account_id = fields.Many2one(
"account.analytic.account",
string="Expense Analytic Account",
domain="[('company_id', '=', company_id)]",
ondelete="restrict",
help="Default analytic account used for expense transactions in this business category.",
)
@api.constrains("company_id", "expense_analytic_account_id")
def _check_expense_analytic_account_company(self):
for category in self:
if not category.expense_analytic_account_id or not category.company_id:
continue
if category.expense_analytic_account_id.company_id != category.company_id:
raise ValidationError(
_(
"Business Category '%s' must use an Expense Analytic Account from company '%s'."
)
% (category.name, category.company_id.name)
)
@@ -0,0 +1,56 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class ExpenseTeam(models.Model):
_name = "expense.team"
_description = "Expense Team"
_order = "name"
name = fields.Char(required=True)
active = fields.Boolean(default=True)
company_id = fields.Many2one(
"res.company",
string="Company",
required=True,
default=lambda self: self.env.company,
)
user_id = fields.Many2one(
"res.users",
string="Team Leader",
help="Responsible user for this Expense Team.",
)
member_ids = fields.Many2many(
"res.users",
"expense_team_res_users_rel",
"team_id",
"user_id",
string="Members",
)
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
required=True,
default=lambda self: self.env["business.category.mixin"]._default_business_category_id(),
ondelete="restrict",
domain="[('company_id', '=', company_id)]",
)
@api.onchange("company_id")
def _onchange_company_id_business_category(self):
for team in self:
if team.business_category_id and team.business_category_id.company_id != team.company_id:
team.business_category_id = False
@api.constrains("company_id", "business_category_id")
def _check_business_category_company(self):
for team in self:
if not team.company_id or not team.business_category_id:
continue
if team.business_category_id.company_id != team.company_id:
raise ValidationError(
_(
"Expense Team '%s' must use a Business Category from company '%s'."
)
% (team.name, team.company_id.name)
)
@@ -0,0 +1,192 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class HrExpense(models.Model):
_inherit = "hr.expense"
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
default=lambda self: self.env["business.category.mixin"]._default_business_category_id(),
domain="[('company_id', '=', company_id)]",
ondelete="restrict",
tracking=True,
)
expense_team_id = fields.Many2one(
"expense.team",
string="Expense Team",
domain="[('company_id', '=', company_id), ('business_category_id', '=', business_category_id)]",
tracking=True,
)
expense_analytic_account_id = fields.Many2one(
"account.analytic.account",
string="Expense Analytic Account",
related="business_category_id.expense_analytic_account_id",
store=True,
readonly=True,
)
@api.onchange("business_category_id")
def _onchange_business_category_id_reset_team(self):
for expense in self:
if expense.expense_team_id and expense.expense_team_id.business_category_id != expense.business_category_id:
expense.expense_team_id = False
@api.onchange("expense_team_id")
def _onchange_expense_team_id(self):
for expense in self:
if expense.expense_team_id and expense.expense_team_id.business_category_id:
expense.business_category_id = expense.expense_team_id.business_category_id
@api.onchange("business_category_id")
def _onchange_business_category_id_analytic(self):
for expense in self:
if "analytic_account_id" in expense._fields:
expense.analytic_account_id = expense.business_category_id.expense_analytic_account_id
@api.model_create_multi
def create(self, vals_list):
vals_list = [self._prepare_business_category_vals(vals) for vals in vals_list]
return super().create(vals_list)
def write(self, vals):
vals = self._prepare_business_category_vals(vals)
return super().write(vals)
def _prepare_business_category_vals(self, vals):
vals = dict(vals)
if vals.get("expense_team_id") and not vals.get("business_category_id"):
team = self.env["expense.team"].browse(vals["expense_team_id"])
if team.business_category_id:
vals["business_category_id"] = team.business_category_id.id
if vals.get("business_category_id") and "analytic_account_id" in self._fields and not vals.get("analytic_account_id"):
category = self.env["crm.business.category"].browse(vals["business_category_id"])
if category.expense_analytic_account_id:
vals["analytic_account_id"] = category.expense_analytic_account_id.id
return vals
@api.constrains("business_category_id", "expense_team_id", "company_id", "expense_analytic_account_id")
def _check_business_category_team(self):
for expense in self:
if expense.business_category_id and expense.business_category_id.company_id != expense.company_id:
raise ValidationError(
_("Expense '%s' uses a Business Category from another company.") % expense.name
)
if expense.expense_analytic_account_id and expense.expense_analytic_account_id.company_id != expense.company_id:
raise ValidationError(
_("Expense '%s' uses an Analytic Account from another company.") % expense.name
)
if expense.expense_team_id:
if expense.expense_team_id.company_id != expense.company_id:
raise ValidationError(
_("Expense Team '%s' belongs to another company.") % expense.expense_team_id.name
)
if expense.expense_team_id.business_category_id != expense.business_category_id:
raise ValidationError(
_(
"Expense Team '%s' is linked to business category '%s'. "
"Please select a matching business category."
)
% (expense.expense_team_id.name, expense.expense_team_id.business_category_id.name)
)
if (
"analytic_account_id" in expense._fields
and expense.expense_analytic_account_id
and expense.analytic_account_id
and expense.expense_analytic_account_id != expense.analytic_account_id
):
raise ValidationError(
_("Expense '%s' must use the Expense Analytic Account configured on its Business Category.")
% expense.name
)
@api.constrains("business_category_id", "expense_team_id", "employee_id")
def _check_current_user_expense_scope(self):
current_user = self.env.user
if current_user.has_group("base.group_system"):
return
effective_categories = current_user.effective_business_category_ids
for expense in self:
if expense.business_category_id and expense.business_category_id not in effective_categories:
raise ValidationError(
_("You can only use Expenses in your registered Business Category.")
)
if expense.expense_team_id:
team_users = expense.expense_team_id.user_id | expense.expense_team_id.member_ids
if current_user not in team_users:
raise ValidationError(
_("You must be registered in the selected Expense Team before using it on an Expense.")
)
class HrExpenseSheet(models.Model):
_inherit = "hr.expense.sheet"
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
compute="_compute_business_category_id",
store=True,
readonly=False,
)
expense_team_id = fields.Many2one(
"expense.team",
string="Expense Team",
compute="_compute_expense_team_id",
store=True,
readonly=False,
)
expense_analytic_account_id = fields.Many2one(
"account.analytic.account",
string="Expense Analytic Account",
compute="_compute_expense_analytic_account_id",
store=True,
readonly=False,
)
@api.depends("expense_line_ids.business_category_id")
def _compute_business_category_id(self):
for sheet in self:
categories = sheet.expense_line_ids.mapped("business_category_id")
sheet.business_category_id = categories[:1].id if len(categories) == 1 else False
@api.depends("expense_line_ids.expense_team_id")
def _compute_expense_team_id(self):
for sheet in self:
teams = sheet.expense_line_ids.mapped("expense_team_id")
sheet.expense_team_id = teams[:1].id if len(teams) == 1 else False
@api.depends("expense_line_ids.expense_analytic_account_id")
def _compute_expense_analytic_account_id(self):
for sheet in self:
analytics = sheet.expense_line_ids.mapped("expense_analytic_account_id")
sheet.expense_analytic_account_id = analytics[:1].id if len(analytics) == 1 else False
@api.constrains("expense_line_ids", "business_category_id", "expense_team_id")
def _check_expense_lines_same_business_category(self):
for sheet in self:
categories = sheet.expense_line_ids.mapped("business_category_id")
if len(categories) > 1:
raise ValidationError(
_("Expense Report '%s' cannot contain multiple Business Categories.") % sheet.name
)
teams = sheet.expense_line_ids.mapped("expense_team_id")
if len(teams) > 1:
raise ValidationError(
_("Expense Report '%s' cannot contain multiple Expense Teams.") % sheet.name
)
def action_sheet_move_create(self):
result = super().action_sheet_move_create()
for sheet in self:
move = getattr(sheet, "account_move_id", False)
if move and sheet.business_category_id:
move.write(
{
"expense_business_category_id": sheet.business_category_id.id,
"expense_analytic_account_id": sheet.expense_analytic_account_id.id,
}
)
return result
@@ -0,0 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_expense_team_user,expense.team user,model_expense_team,hr_expense.group_hr_expense_user,1,0,0,0
access_expense_team_manager,expense.team manager,model_expense_team,hr_expense.group_hr_expense_manager,1,1,1,1
access_expense_team_sysadmin,expense.team sysadmin,model_expense_team,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_expense_team_user expense.team user model_expense_team hr_expense.group_hr_expense_user 1 0 0 0
3 access_expense_team_manager expense.team manager model_expense_team hr_expense.group_hr_expense_manager 1 1 1 1
4 access_expense_team_sysadmin expense.team sysadmin model_expense_team base.group_system 1 1 1 1
@@ -0,0 +1,18 @@
id,name,model_id:id,domain_force,groups:id,perm_read,perm_write,perm_create,perm_unlink
hr_expense_business_category_rule_user_read_create,Expense read/create by effective business category,hr_expense.model_hr_expense,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",hr_expense.group_hr_expense_user,1,0,1,0
hr_expense_business_category_rule_user_write_own,Expense write own by effective business category,hr_expense.model_hr_expense,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids),'|',('employee_id.user_id','=',user.id),('create_uid','=',user.id)]",hr_expense.group_hr_expense_user,0,1,0,1
hr_expense_business_category_rule_manager,Expense full access for expense manager in effective business category,hr_expense.model_hr_expense,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",hr_expense.group_hr_expense_manager,1,1,1,1
hr_expense_business_category_rule_sysadmin,Expense full access for system admin,hr_expense.model_hr_expense,"[('company_id','in',user.company_ids.ids)]",base.group_system,1,1,1,1
hr_expense_sheet_business_category_rule_user,Expense Report read by effective business category,hr_expense.model_hr_expense_sheet,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",hr_expense.group_hr_expense_user,1,0,0,0
hr_expense_sheet_business_category_rule_manager,Expense Report full access for expense manager in effective business category,hr_expense.model_hr_expense_sheet,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",hr_expense.group_hr_expense_manager,1,1,1,1
hr_expense_sheet_business_category_rule_sysadmin,Expense Report full access for system admin,hr_expense.model_hr_expense_sheet,"[('company_id','in',user.company_ids.ids)]",base.group_system,1,1,1,1
expense_team_business_category_rule_user,Expense Team read by effective business category,model_expense_team,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",hr_expense.group_hr_expense_user,1,0,0,0
expense_team_business_category_rule_manager,Expense Team full access for expense manager in effective business category,model_expense_team,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",hr_expense.group_hr_expense_manager,1,1,1,1
expense_team_business_category_rule_sysadmin,Expense Team full access for system admin,model_expense_team,"[('company_id','in',user.company_ids.ids)]",base.group_system,1,1,1,1
crm_business_category_rule_expense_user,Business Category by user effective list for expense user,grt_crm_business_category.model_crm_business_category,"[('company_id','in',user.company_ids.ids),('id','in',user.effective_business_category_ids.ids)]",hr_expense.group_hr_expense_user,1,0,0,0
crm_business_category_rule_expense_manager,Business Category access for expense manager in effective business category,grt_crm_business_category.model_crm_business_category,"[('company_id','in',user.company_ids.ids),('id','in',user.effective_business_category_ids.ids)]",hr_expense.group_hr_expense_manager,1,1,1,1
crm_business_category_rule_expense_sysadmin,Business Category full access for system admin,grt_crm_business_category.model_crm_business_category,"[('company_id','in',user.company_ids.ids)]",base.group_system,1,1,1,1
account_move_expense_business_category_rule_user,Journal Entry read by effective business category for expense user,account.model_account_move,"[('company_id','in',user.company_ids.ids),('expense_business_category_id','in',user.effective_business_category_ids.ids)]",hr_expense.group_hr_expense_user,1,0,0,0
account_move_expense_business_category_rule_manager,Journal Entry access for expense manager in effective business category,account.model_account_move,"[('company_id','in',user.company_ids.ids),('expense_business_category_id','in',user.effective_business_category_ids.ids)]",hr_expense.group_hr_expense_manager,1,1,1,1
account_move_line_expense_business_category_rule_user,Journal Item read by effective business category for expense user,account.model_account_move_line,"[('company_id','in',user.company_ids.ids),('move_id.expense_business_category_id','in',user.effective_business_category_ids.ids)]",hr_expense.group_hr_expense_user,1,0,0,0
account_move_line_expense_business_category_rule_manager,Journal Item access for expense manager in effective business category,account.model_account_move_line,"[('company_id','in',user.company_ids.ids),('move_id.expense_business_category_id','in',user.effective_business_category_ids.ids)]",hr_expense.group_hr_expense_manager,1,1,1,1
1 id name model_id:id domain_force groups:id perm_read perm_write perm_create perm_unlink
2 hr_expense_business_category_rule_user_read_create Expense read/create by effective business category hr_expense.model_hr_expense [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] hr_expense.group_hr_expense_user 1 0 1 0
3 hr_expense_business_category_rule_user_write_own Expense write own by effective business category hr_expense.model_hr_expense [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids),'|',('employee_id.user_id','=',user.id),('create_uid','=',user.id)] hr_expense.group_hr_expense_user 0 1 0 1
4 hr_expense_business_category_rule_manager Expense full access for expense manager in effective business category hr_expense.model_hr_expense [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] hr_expense.group_hr_expense_manager 1 1 1 1
5 hr_expense_business_category_rule_sysadmin Expense full access for system admin hr_expense.model_hr_expense [('company_id','in',user.company_ids.ids)] base.group_system 1 1 1 1
6 hr_expense_sheet_business_category_rule_user Expense Report read by effective business category hr_expense.model_hr_expense_sheet [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] hr_expense.group_hr_expense_user 1 0 0 0
7 hr_expense_sheet_business_category_rule_manager Expense Report full access for expense manager in effective business category hr_expense.model_hr_expense_sheet [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] hr_expense.group_hr_expense_manager 1 1 1 1
8 hr_expense_sheet_business_category_rule_sysadmin Expense Report full access for system admin hr_expense.model_hr_expense_sheet [('company_id','in',user.company_ids.ids)] base.group_system 1 1 1 1
9 expense_team_business_category_rule_user Expense Team read by effective business category model_expense_team [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] hr_expense.group_hr_expense_user 1 0 0 0
10 expense_team_business_category_rule_manager Expense Team full access for expense manager in effective business category model_expense_team [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] hr_expense.group_hr_expense_manager 1 1 1 1
11 expense_team_business_category_rule_sysadmin Expense Team full access for system admin model_expense_team [('company_id','in',user.company_ids.ids)] base.group_system 1 1 1 1
12 crm_business_category_rule_expense_user Business Category by user effective list for expense user grt_crm_business_category.model_crm_business_category [('company_id','in',user.company_ids.ids),('id','in',user.effective_business_category_ids.ids)] hr_expense.group_hr_expense_user 1 0 0 0
13 crm_business_category_rule_expense_manager Business Category access for expense manager in effective business category grt_crm_business_category.model_crm_business_category [('company_id','in',user.company_ids.ids),('id','in',user.effective_business_category_ids.ids)] hr_expense.group_hr_expense_manager 1 1 1 1
14 crm_business_category_rule_expense_sysadmin Business Category full access for system admin grt_crm_business_category.model_crm_business_category [('company_id','in',user.company_ids.ids)] base.group_system 1 1 1 1
15 account_move_expense_business_category_rule_user Journal Entry read by effective business category for expense user account.model_account_move [('company_id','in',user.company_ids.ids),('expense_business_category_id','in',user.effective_business_category_ids.ids)] hr_expense.group_hr_expense_user 1 0 0 0
16 account_move_expense_business_category_rule_manager Journal Entry access for expense manager in effective business category account.model_account_move [('company_id','in',user.company_ids.ids),('expense_business_category_id','in',user.effective_business_category_ids.ids)] hr_expense.group_hr_expense_manager 1 1 1 1
17 account_move_line_expense_business_category_rule_user Journal Item read by effective business category for expense user account.model_account_move_line [('company_id','in',user.company_ids.ids),('move_id.expense_business_category_id','in',user.effective_business_category_ids.ids)] hr_expense.group_hr_expense_user 1 0 0 0
18 account_move_line_expense_business_category_rule_manager Journal Item access for expense manager in effective business category account.model_account_move_line [('company_id','in',user.company_ids.ids),('move_id.expense_business_category_id','in',user.effective_business_category_ids.ids)] hr_expense.group_hr_expense_manager 1 1 1 1
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_crm_business_category_form_expense_analytic" model="ir.ui.view">
<field name="name">crm.business.category.form.expense.analytic</field>
<field name="model">crm.business.category</field>
<field name="inherit_id" ref="grt_crm_business_category.view_crm_business_category_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='description']/.." position="before">
<group string="Expenses">
<field name="company_id" invisible="1"/>
<field name="expense_analytic_account_id" domain="[('company_id', '=', company_id)]"/>
</group>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem id="menu_expense_team"
name="Team Expenses"
parent="hr_expense.menu_hr_expense_root"
sequence="45"
action="action_expense_team"
groups="hr_expense.group_hr_expense_manager"/>
<menuitem id="menu_business_category_from_expense"
name="Business Categories"
parent="hr_expense.menu_hr_expense_root"
sequence="46"
action="grt_crm_business_category.action_crm_business_category"
groups="hr_expense.group_hr_expense_manager"/>
</odoo>
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_hr_expense_pivot_business_category_report" model="ir.ui.view">
<field name="name">hr.expense.pivot.business.category.report</field>
<field name="model">hr.expense</field>
<field name="arch" type="xml">
<pivot string="Expenses by Business Category">
<field name="business_category_id" type="row"/>
<field name="expense_team_id" type="row"/>
<field name="state" type="col"/>
<field name="date" interval="month" type="col"/>
<field name="total_amount" type="measure"/>
<field name="id" type="measure" string="Expenses"/>
</pivot>
</field>
</record>
<record id="view_hr_expense_graph_business_category_report" model="ir.ui.view">
<field name="name">hr.expense.graph.business.category.report</field>
<field name="model">hr.expense</field>
<field name="arch" type="xml">
<graph string="Expenses by Business Category" type="bar">
<field name="business_category_id"/>
<field name="total_amount" type="measure"/>
</graph>
</field>
</record>
<record id="action_hr_expense_business_category_report" model="ir.actions.act_window">
<field name="name">Expenses by Business Category</field>
<field name="res_model">hr.expense</field>
<field name="view_mode">tree,pivot,graph</field>
<field name="context">{'group_by': 'business_category_id'}</field>
<field name="search_view_id" ref="view_hr_expense_filter_business_category"/>
</record>
<menuitem id="menu_hr_expense_business_category_report"
name="Expenses by Category"
parent="hr_expense.menu_hr_expense_root"
sequence="50"
action="action_hr_expense_business_category_report"
groups="hr_expense.group_hr_expense_user,hr_expense.group_hr_expense_manager,base.group_system"/>
</odoo>
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_expense_team_tree" model="ir.ui.view">
<field name="name">expense.team.tree</field>
<field name="model">expense.team</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="company_id"/>
<field name="user_id"/>
<field name="member_ids" widget="many2many_tags"/>
<field name="business_category_id"/>
<field name="active"/>
</tree>
</field>
</record>
<record id="view_expense_team_form" model="ir.ui.view">
<field name="name">expense.team.form</field>
<field name="model">expense.team</field>
<field name="arch" type="xml">
<form string="Expense Team">
<sheet>
<group>
<field name="name"/>
<field name="company_id"/>
<field name="user_id"/>
<field name="member_ids" widget="many2many_tags"/>
<field name="business_category_id" domain="[('company_id', '=', company_id)]"/>
<field name="active"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_expense_team" model="ir.actions.act_window">
<field name="name">Team Expenses</field>
<field name="res_model">expense.team</field>
<field name="view_mode">tree,form</field>
</record>
</odoo>
@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_hr_expense_form_business_category" model="ir.ui.view">
<field name="name">hr.expense.form.business.category</field>
<field name="model">hr.expense</field>
<field name="inherit_id" ref="hr_expense.hr_expense_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='employee_id']" position="after">
<field name="company_id" invisible="1"/>
<field name="business_category_id"/>
<field name="expense_team_id"/>
<field name="expense_analytic_account_id" readonly="1"/>
</xpath>
</field>
</record>
<record id="view_hr_expense_tree_business_category" model="ir.ui.view">
<field name="name">hr.expense.tree.business.category</field>
<field name="model">hr.expense</field>
<field name="inherit_id" ref="hr_expense.view_expenses_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='employee_id']" position="after">
<field name="business_category_id" optional="show"/>
<field name="expense_team_id" optional="show"/>
</xpath>
</field>
</record>
<record id="view_hr_expense_filter_business_category" model="ir.ui.view">
<field name="name">hr.expense.search.business.category</field>
<field name="model">hr.expense</field>
<field name="inherit_id" ref="hr_expense.hr_expense_view_search"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='employee_id']" position="after">
<field name="business_category_id"/>
<field name="expense_team_id"/>
</xpath>
<xpath expr="//filter[@name='my_expenses']" position="after">
<filter string="Business Category" name="group_by_business_category" context="{'group_by': 'business_category_id'}"/>
<filter string="Expense Team" name="group_by_expense_team" context="{'group_by': 'expense_team_id'}"/>
</xpath>
</field>
</record>
<record id="view_hr_expense_sheet_form_business_category" model="ir.ui.view">
<field name="name">hr.expense.sheet.form.business.category</field>
<field name="model">hr.expense.sheet</field>
<field name="inherit_id" ref="hr_expense.view_hr_expense_sheet_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='employee_id']" position="after">
<field name="business_category_id" readonly="1"/>
<field name="expense_team_id" readonly="1"/>
<field name="expense_analytic_account_id" readonly="1"/>
</xpath>
</field>
</record>
<record id="view_hr_expense_sheet_tree_business_category" model="ir.ui.view">
<field name="name">hr.expense.sheet.tree.business.category</field>
<field name="model">hr.expense.sheet</field>
<field name="inherit_id" ref="hr_expense.view_hr_expense_sheet_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='employee_id']" position="after">
<field name="business_category_id" optional="show"/>
<field name="expense_team_id" optional="show"/>
</xpath>
</field>
</record>
<record id="view_move_form_expense_business_category" model="ir.ui.view">
<field name="name">account.move.form.expense.business.category</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='invoice_payment_term_id']" position="after">
<field name="expense_business_category_id"/>
<field name="expense_analytic_account_id"/>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1 @@
from . import models
@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
{
"name": "GRT Inventory Business Category",
"summary": "Business category segregation and inventory teams for stock operations",
"description": """
Adds business category handling to Inventory with dedicated Inventory Teams,
warehouse segregation, record rules per business category, and analytic account
propagation for stock valuation journal entries.
""",
"author": "PT Gagak Rimang Teknologi",
"website": "https://rimang.id",
"category": "Inventory",
"version": "14.0.1.0.0",
"depends": [
"stock_account",
"sale_stock",
"purchase_stock",
"grt_crm_business_category",
"grt_sales_business_category",
"grt_purchase_business_category",
],
"data": [
"security/ir.model.access.csv",
"security/ir.rule.csv",
"views/crm_business_category_views.xml",
"views/stock_team_views.xml",
"views/stock_warehouse_views.xml",
"views/stock_picking_views.xml",
"views/inventory_report_business_category_views.xml",
"views/stock_menu_views.xml",
],
"installable": True,
"application": False,
}
@@ -0,0 +1,5 @@
from . import account_move
from . import crm_business_category
from . import stock_picking
from . import stock_team
from . import stock_warehouse
@@ -0,0 +1,92 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class AccountMove(models.Model):
_inherit = "account.move"
inventory_business_category_id = fields.Many2one(
"crm.business.category",
string="Inventory Business Category",
domain="[('company_id', '=', company_id)]",
ondelete="restrict",
copy=False,
)
inventory_analytic_account_id = fields.Many2one(
"account.analytic.account",
string="Inventory Analytic Account",
domain="[('company_id', '=', company_id)]",
ondelete="restrict",
copy=False,
)
@api.onchange("inventory_business_category_id")
def _onchange_inventory_business_category_id_analytic(self):
for move in self:
move.inventory_analytic_account_id = move.inventory_business_category_id.inventory_analytic_account_id
@api.model_create_multi
def create(self, vals_list):
vals_list = [self._prepare_inventory_business_category_vals(vals) for vals in vals_list]
return super().create(vals_list)
def write(self, vals):
vals = self._prepare_inventory_business_category_vals(vals)
return super().write(vals)
def _prepare_inventory_business_category_vals(self, vals):
vals = dict(vals)
if vals.get("inventory_business_category_id") and not vals.get("inventory_analytic_account_id"):
category = self.env["crm.business.category"].browse(vals["inventory_business_category_id"])
if category.inventory_analytic_account_id:
vals["inventory_analytic_account_id"] = category.inventory_analytic_account_id.id
return vals
@api.constrains("company_id", "inventory_business_category_id", "inventory_analytic_account_id")
def _check_inventory_business_category_analytic_company(self):
for move in self:
if move.inventory_business_category_id and move.inventory_business_category_id.company_id != move.company_id:
raise ValidationError(
_("Journal Entry '%s' uses an Inventory Business Category from another company.") % move.display_name
)
if move.inventory_analytic_account_id and move.inventory_analytic_account_id.company_id != move.company_id:
raise ValidationError(
_("Journal Entry '%s' uses an Inventory Analytic Account from another company.") % move.display_name
)
if (
move.inventory_business_category_id
and move.inventory_business_category_id.inventory_analytic_account_id
and move.inventory_analytic_account_id
and move.inventory_business_category_id.inventory_analytic_account_id
!= move.inventory_analytic_account_id
):
raise ValidationError(
_(
"Journal Entry '%s' must use the Inventory Analytic Account configured on its Business Category."
)
% move.display_name
)
class AccountMoveLine(models.Model):
_inherit = "account.move.line"
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get("analytic_account_id"):
continue
move_id = vals.get("move_id")
account_id = vals.get("account_id")
if not move_id or not account_id:
continue
move = self.env["account.move"].browse(move_id)
account = self.env["account.account"].browse(account_id)
if (
move
and move.inventory_analytic_account_id
and account
and account.internal_type not in ("receivable", "payable")
):
vals["analytic_account_id"] = move.inventory_analytic_account_id.id
return super().create(vals_list)
@@ -0,0 +1,27 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class CrmBusinessCategory(models.Model):
_inherit = "crm.business.category"
inventory_analytic_account_id = fields.Many2one(
"account.analytic.account",
string="Inventory Analytic Account",
domain="[('company_id', '=', company_id)]",
ondelete="restrict",
help="Default analytic account used for stock valuation entries in this business category.",
)
@api.constrains("company_id", "inventory_analytic_account_id")
def _check_inventory_analytic_account_company(self):
for category in self:
if not category.inventory_analytic_account_id or not category.company_id:
continue
if category.inventory_analytic_account_id.company_id != category.company_id:
raise ValidationError(
_(
"Business Category '%s' must use an Inventory Analytic Account from company '%s'."
)
% (category.name, category.company_id.name)
)
@@ -0,0 +1,189 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class StockPicking(models.Model):
_inherit = "stock.picking"
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
default=lambda self: self._default_business_category_from_context(),
domain="[('company_id', '=', company_id)]",
ondelete="restrict",
tracking=True,
)
stock_team_id = fields.Many2one(
"stock.team",
string="Inventory Team",
domain="[('company_id', '=', company_id), ('business_category_id', '=', business_category_id)]",
tracking=True,
)
inventory_analytic_account_id = fields.Many2one(
"account.analytic.account",
string="Inventory Analytic Account",
related="business_category_id.inventory_analytic_account_id",
store=True,
readonly=True,
)
@api.model
def _default_business_category_from_context(self):
picking_type_id = self.env.context.get("default_picking_type_id")
if picking_type_id:
picking_type = self.env["stock.picking.type"].browse(picking_type_id)
if picking_type.warehouse_id and picking_type.warehouse_id.business_category_id:
return picking_type.warehouse_id.business_category_id.id
return self.env["business.category.mixin"]._default_business_category_id()
@api.onchange("business_category_id")
def _onchange_business_category_id_reset_team(self):
for picking in self:
if picking.stock_team_id and picking.stock_team_id.business_category_id != picking.business_category_id:
picking.stock_team_id = False
@api.onchange("stock_team_id")
def _onchange_stock_team_id(self):
for picking in self:
if picking.stock_team_id and picking.stock_team_id.business_category_id:
picking.business_category_id = picking.stock_team_id.business_category_id
@api.onchange("picking_type_id")
def _onchange_picking_type_id_business_category(self):
for picking in self:
warehouse = picking.picking_type_id.warehouse_id
if warehouse and warehouse.business_category_id:
picking.business_category_id = warehouse.business_category_id
if warehouse and warehouse.stock_team_id and not picking.stock_team_id:
picking.stock_team_id = warehouse.stock_team_id
@api.model_create_multi
def create(self, vals_list):
vals_list = [self._prepare_business_category_vals(vals) for vals in vals_list]
return super().create(vals_list)
def write(self, vals):
vals = self._prepare_business_category_vals(vals)
return super().write(vals)
def _prepare_business_category_vals(self, vals):
vals = dict(vals)
if vals.get("stock_team_id") and not vals.get("business_category_id"):
team = self.env["stock.team"].browse(vals["stock_team_id"])
if team.business_category_id:
vals["business_category_id"] = team.business_category_id.id
if vals.get("picking_type_id") and not vals.get("business_category_id"):
picking_type = self.env["stock.picking.type"].browse(vals["picking_type_id"])
if picking_type.warehouse_id and picking_type.warehouse_id.business_category_id:
vals["business_category_id"] = picking_type.warehouse_id.business_category_id.id
return vals
@api.constrains("business_category_id", "stock_team_id", "company_id")
def _check_business_category_team(self):
for picking in self:
if picking.business_category_id and picking.business_category_id.company_id != picking.company_id:
raise ValidationError(
_("Transfer '%s' uses a Business Category from another company.") % picking.name
)
if picking.stock_team_id:
if picking.stock_team_id.company_id != picking.company_id:
raise ValidationError(
_("Inventory Team '%s' belongs to another company.") % picking.stock_team_id.name
)
if picking.stock_team_id.business_category_id != picking.business_category_id:
raise ValidationError(
_(
"Inventory Team '%s' is linked to business category '%s'. "
"Please select a matching business category."
)
% (picking.stock_team_id.name, picking.stock_team_id.business_category_id.name)
)
warehouse = picking.picking_type_id.warehouse_id
if warehouse and warehouse.business_category_id and picking.business_category_id:
if warehouse.business_category_id != picking.business_category_id:
raise ValidationError(
_("Transfer '%s' must use the same Business Category as its warehouse.") % picking.name
)
@api.constrains("business_category_id", "stock_team_id")
def _check_current_user_inventory_scope(self):
current_user = self.env.user
if current_user.has_group("base.group_system"):
return
effective_categories = current_user.effective_business_category_ids
for picking in self:
if picking.business_category_id and picking.business_category_id not in effective_categories:
raise ValidationError(
_("You can only use Inventory Transfers in your registered Business Category.")
)
if picking.stock_team_id:
team_users = picking.stock_team_id.user_id | picking.stock_team_id.member_ids
if current_user not in team_users:
raise ValidationError(
_("You must be registered in the selected Inventory Team before using it on a Transfer.")
)
def button_validate(self):
for picking in self:
if not picking.business_category_id:
raise ValidationError(
_("Transfer '%s' requires a Business Category before validation.") % picking.name
)
return super().button_validate()
class StockMove(models.Model):
_inherit = "stock.move"
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
related="picking_id.business_category_id",
store=True,
readonly=True,
)
stock_team_id = fields.Many2one(
"stock.team",
string="Inventory Team",
related="picking_id.stock_team_id",
store=True,
readonly=True,
)
inventory_analytic_account_id = fields.Many2one(
"account.analytic.account",
string="Inventory Analytic Account",
related="picking_id.inventory_analytic_account_id",
store=True,
readonly=True,
)
def _get_new_picking_values(self):
vals = super()._get_new_picking_values()
sale_order = self.sale_line_id.order_id if "sale_line_id" in self._fields else self.env["sale.order"]
purchase_order = (
self.purchase_line_id.order_id if "purchase_line_id" in self._fields else self.env["purchase.order"]
)
business_category = False
if sale_order and sale_order.business_category_id:
business_category = sale_order.business_category_id
elif purchase_order and purchase_order.business_category_id:
business_category = purchase_order.business_category_id
elif self.picking_type_id.warehouse_id and self.picking_type_id.warehouse_id.business_category_id:
business_category = self.picking_type_id.warehouse_id.business_category_id
if business_category:
vals["business_category_id"] = business_category.id
warehouse = self.picking_type_id.warehouse_id
if warehouse and warehouse.stock_team_id:
vals.setdefault("stock_team_id", warehouse.stock_team_id.id)
return vals
def _prepare_account_move_line(self, qty, cost, credit_account_id, debit_account_id, description):
res = super()._prepare_account_move_line(qty, cost, credit_account_id, debit_account_id, description)
analytic = self.inventory_analytic_account_id
if not analytic:
return res
for line_vals in res:
if isinstance(line_vals, dict) and not line_vals.get("analytic_account_id"):
line_vals["analytic_account_id"] = analytic.id
return res
@@ -0,0 +1,56 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class StockTeam(models.Model):
_name = "stock.team"
_description = "Inventory Team"
_order = "name"
name = fields.Char(required=True)
active = fields.Boolean(default=True)
company_id = fields.Many2one(
"res.company",
string="Company",
required=True,
default=lambda self: self.env.company,
)
user_id = fields.Many2one(
"res.users",
string="Team Leader",
help="Responsible user for this Inventory Team.",
)
member_ids = fields.Many2many(
"res.users",
"stock_team_res_users_rel",
"team_id",
"user_id",
string="Members",
)
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
required=True,
default=lambda self: self.env["business.category.mixin"]._default_business_category_id(),
ondelete="restrict",
domain="[('company_id', '=', company_id)]",
)
@api.onchange("company_id")
def _onchange_company_id_business_category(self):
for team in self:
if team.business_category_id and team.business_category_id.company_id != team.company_id:
team.business_category_id = False
@api.constrains("company_id", "business_category_id")
def _check_business_category_company(self):
for team in self:
if not team.company_id or not team.business_category_id:
continue
if team.business_category_id.company_id != team.company_id:
raise ValidationError(
_(
"Inventory Team '%s' must use a Business Category from company '%s'."
)
% (team.name, team.company_id.name)
)
@@ -0,0 +1,52 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class StockWarehouse(models.Model):
_inherit = "stock.warehouse"
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
default=lambda self: self.env["business.category.mixin"]._default_business_category_id(),
ondelete="restrict",
domain="[('company_id', '=', company_id)]",
)
stock_team_id = fields.Many2one(
"stock.team",
string="Inventory Team",
domain="[('company_id', '=', company_id), ('business_category_id', '=', business_category_id)]",
)
@api.onchange("business_category_id")
def _onchange_business_category_id_reset_stock_team(self):
for warehouse in self:
if warehouse.stock_team_id and warehouse.stock_team_id.business_category_id != warehouse.business_category_id:
warehouse.stock_team_id = False
@api.onchange("stock_team_id")
def _onchange_stock_team_id(self):
for warehouse in self:
if warehouse.stock_team_id and warehouse.stock_team_id.business_category_id:
warehouse.business_category_id = warehouse.stock_team_id.business_category_id
@api.constrains("company_id", "business_category_id", "stock_team_id")
def _check_business_category_team(self):
for warehouse in self:
if warehouse.business_category_id and warehouse.business_category_id.company_id != warehouse.company_id:
raise ValidationError(
_("Warehouse '%s' uses a Business Category from another company.") % warehouse.name
)
if warehouse.stock_team_id:
if warehouse.stock_team_id.company_id != warehouse.company_id:
raise ValidationError(
_("Inventory Team '%s' belongs to another company.") % warehouse.stock_team_id.name
)
if warehouse.stock_team_id.business_category_id != warehouse.business_category_id:
raise ValidationError(
_(
"Inventory Team '%s' is linked to business category '%s'. "
"Please select a matching business category."
)
% (warehouse.stock_team_id.name, warehouse.stock_team_id.business_category_id.name)
)
@@ -0,0 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_stock_team_user,stock.team user,model_stock_team,stock.group_stock_user,1,0,0,0
access_stock_team_manager,stock.team manager,model_stock_team,stock.group_stock_manager,1,1,1,1
access_stock_team_sysadmin,stock.team sysadmin,model_stock_team,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_stock_team_user stock.team user model_stock_team stock.group_stock_user 1 0 0 0
3 access_stock_team_manager stock.team manager model_stock_team stock.group_stock_manager 1 1 1 1
4 access_stock_team_sysadmin stock.team sysadmin model_stock_team base.group_system 1 1 1 1
@@ -0,0 +1,21 @@
id,name,model_id:id,domain_force,groups:id,perm_read,perm_write,perm_create,perm_unlink
stock_picking_business_category_rule_user_read_create,Transfer read/create by effective business category,stock.model_stock_picking,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",stock.group_stock_user,1,0,1,0
stock_picking_business_category_rule_user_write_own,Transfer write own by effective business category,stock.model_stock_picking,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids),'|',('user_id','=',user.id),('create_uid','=',user.id)]",stock.group_stock_user,0,1,0,1
stock_picking_business_category_rule_manager,Transfer full access for inventory manager in effective business category,stock.model_stock_picking,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",stock.group_stock_manager,1,1,1,1
stock_picking_business_category_rule_sysadmin,Transfer full access for system admin,stock.model_stock_picking,"[('company_id','in',user.company_ids.ids)]",base.group_system,1,1,1,1
stock_move_business_category_rule_user,Stock Move by effective business category,stock.model_stock_move,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",stock.group_stock_user,1,0,0,0
stock_move_business_category_rule_manager,Stock Move full access for inventory manager in effective business category,stock.model_stock_move,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",stock.group_stock_manager,1,1,1,1
stock_move_business_category_rule_sysadmin,Stock Move full access for system admin,stock.model_stock_move,"[('company_id','in',user.company_ids.ids)]",base.group_system,1,1,1,1
stock_warehouse_business_category_rule_user,Warehouse by effective business category,stock.model_stock_warehouse,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",stock.group_stock_user,1,0,0,0
stock_warehouse_business_category_rule_manager,Warehouse full access for inventory manager in effective business category,stock.model_stock_warehouse,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",stock.group_stock_manager,1,1,1,1
stock_warehouse_business_category_rule_sysadmin,Warehouse full access for system admin,stock.model_stock_warehouse,"[('company_id','in',user.company_ids.ids)]",base.group_system,1,1,1,1
stock_team_business_category_rule_user,Inventory Team read by effective business category,model_stock_team,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",stock.group_stock_user,1,0,0,0
stock_team_business_category_rule_manager,Inventory Team full access for inventory manager in effective business category,model_stock_team,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",stock.group_stock_manager,1,1,1,1
stock_team_business_category_rule_sysadmin,Inventory Team full access for system admin,model_stock_team,"[('company_id','in',user.company_ids.ids)]",base.group_system,1,1,1,1
crm_business_category_rule_inventory_user,Business Category by user effective list for inventory user,grt_crm_business_category.model_crm_business_category,"[('company_id','in',user.company_ids.ids),('id','in',user.effective_business_category_ids.ids)]",stock.group_stock_user,1,0,0,0
crm_business_category_rule_inventory_manager,Business Category access for inventory manager in effective business category,grt_crm_business_category.model_crm_business_category,"[('company_id','in',user.company_ids.ids),('id','in',user.effective_business_category_ids.ids)]",stock.group_stock_manager,1,1,1,1
crm_business_category_rule_inventory_sysadmin,Business Category full access for system admin,grt_crm_business_category.model_crm_business_category,"[('company_id','in',user.company_ids.ids)]",base.group_system,1,1,1,1
account_move_inventory_business_category_rule_user,Journal Entry read by effective business category for inventory user,account.model_account_move,"[('company_id','in',user.company_ids.ids),('inventory_business_category_id','in',user.effective_business_category_ids.ids)]",stock.group_stock_user,1,0,0,0
account_move_inventory_business_category_rule_manager,Journal Entry access for inventory manager in effective business category,account.model_account_move,"[('company_id','in',user.company_ids.ids),('inventory_business_category_id','in',user.effective_business_category_ids.ids)]",stock.group_stock_manager,1,1,1,1
account_move_line_inventory_business_category_rule_user,Journal Item read by effective business category for inventory user,account.model_account_move_line,"[('company_id','in',user.company_ids.ids),('move_id.inventory_business_category_id','in',user.effective_business_category_ids.ids)]",stock.group_stock_user,1,0,0,0
account_move_line_inventory_business_category_rule_manager,Journal Item access for inventory manager in effective business category,account.model_account_move_line,"[('company_id','in',user.company_ids.ids),('move_id.inventory_business_category_id','in',user.effective_business_category_ids.ids)]",stock.group_stock_manager,1,1,1,1
1 id name model_id:id domain_force groups:id perm_read perm_write perm_create perm_unlink
2 stock_picking_business_category_rule_user_read_create Transfer read/create by effective business category stock.model_stock_picking [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] stock.group_stock_user 1 0 1 0
3 stock_picking_business_category_rule_user_write_own Transfer write own by effective business category stock.model_stock_picking [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids),'|',('user_id','=',user.id),('create_uid','=',user.id)] stock.group_stock_user 0 1 0 1
4 stock_picking_business_category_rule_manager Transfer full access for inventory manager in effective business category stock.model_stock_picking [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] stock.group_stock_manager 1 1 1 1
5 stock_picking_business_category_rule_sysadmin Transfer full access for system admin stock.model_stock_picking [('company_id','in',user.company_ids.ids)] base.group_system 1 1 1 1
6 stock_move_business_category_rule_user Stock Move by effective business category stock.model_stock_move [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] stock.group_stock_user 1 0 0 0
7 stock_move_business_category_rule_manager Stock Move full access for inventory manager in effective business category stock.model_stock_move [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] stock.group_stock_manager 1 1 1 1
8 stock_move_business_category_rule_sysadmin Stock Move full access for system admin stock.model_stock_move [('company_id','in',user.company_ids.ids)] base.group_system 1 1 1 1
9 stock_warehouse_business_category_rule_user Warehouse by effective business category stock.model_stock_warehouse [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] stock.group_stock_user 1 0 0 0
10 stock_warehouse_business_category_rule_manager Warehouse full access for inventory manager in effective business category stock.model_stock_warehouse [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] stock.group_stock_manager 1 1 1 1
11 stock_warehouse_business_category_rule_sysadmin Warehouse full access for system admin stock.model_stock_warehouse [('company_id','in',user.company_ids.ids)] base.group_system 1 1 1 1
12 stock_team_business_category_rule_user Inventory Team read by effective business category model_stock_team [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] stock.group_stock_user 1 0 0 0
13 stock_team_business_category_rule_manager Inventory Team full access for inventory manager in effective business category model_stock_team [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] stock.group_stock_manager 1 1 1 1
14 stock_team_business_category_rule_sysadmin Inventory Team full access for system admin model_stock_team [('company_id','in',user.company_ids.ids)] base.group_system 1 1 1 1
15 crm_business_category_rule_inventory_user Business Category by user effective list for inventory user grt_crm_business_category.model_crm_business_category [('company_id','in',user.company_ids.ids),('id','in',user.effective_business_category_ids.ids)] stock.group_stock_user 1 0 0 0
16 crm_business_category_rule_inventory_manager Business Category access for inventory manager in effective business category grt_crm_business_category.model_crm_business_category [('company_id','in',user.company_ids.ids),('id','in',user.effective_business_category_ids.ids)] stock.group_stock_manager 1 1 1 1
17 crm_business_category_rule_inventory_sysadmin Business Category full access for system admin grt_crm_business_category.model_crm_business_category [('company_id','in',user.company_ids.ids)] base.group_system 1 1 1 1
18 account_move_inventory_business_category_rule_user Journal Entry read by effective business category for inventory user account.model_account_move [('company_id','in',user.company_ids.ids),('inventory_business_category_id','in',user.effective_business_category_ids.ids)] stock.group_stock_user 1 0 0 0
19 account_move_inventory_business_category_rule_manager Journal Entry access for inventory manager in effective business category account.model_account_move [('company_id','in',user.company_ids.ids),('inventory_business_category_id','in',user.effective_business_category_ids.ids)] stock.group_stock_manager 1 1 1 1
20 account_move_line_inventory_business_category_rule_user Journal Item read by effective business category for inventory user account.model_account_move_line [('company_id','in',user.company_ids.ids),('move_id.inventory_business_category_id','in',user.effective_business_category_ids.ids)] stock.group_stock_user 1 0 0 0
21 account_move_line_inventory_business_category_rule_manager Journal Item access for inventory manager in effective business category account.model_account_move_line [('company_id','in',user.company_ids.ids),('move_id.inventory_business_category_id','in',user.effective_business_category_ids.ids)] stock.group_stock_manager 1 1 1 1
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_crm_business_category_form_inventory_analytic" model="ir.ui.view">
<field name="name">crm.business.category.form.inventory.analytic</field>
<field name="model">crm.business.category</field>
<field name="inherit_id" ref="grt_crm_business_category.view_crm_business_category_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='description']/.." position="before">
<group string="Inventory">
<field name="company_id" invisible="1"/>
<field name="inventory_analytic_account_id" domain="[('company_id', '=', company_id)]"/>
</group>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_stock_picking_pivot_business_category_report" model="ir.ui.view">
<field name="name">stock.picking.pivot.business.category.report</field>
<field name="model">stock.picking</field>
<field name="arch" type="xml">
<pivot string="Inventory Transfers by Business Category">
<field name="business_category_id" type="row"/>
<field name="stock_team_id" type="row"/>
<field name="state" type="col"/>
<field name="scheduled_date" interval="month" type="col"/>
<field name="id" type="measure" string="Transfers"/>
</pivot>
</field>
</record>
<record id="view_stock_picking_graph_business_category_report" model="ir.ui.view">
<field name="name">stock.picking.graph.business.category.report</field>
<field name="model">stock.picking</field>
<field name="arch" type="xml">
<graph string="Inventory Transfers by Business Category" type="bar">
<field name="business_category_id"/>
<field name="id" type="measure" string="Transfers"/>
</graph>
</field>
</record>
<record id="action_stock_picking_business_category_report" model="ir.actions.act_window">
<field name="name">Inventory Transfers by Business Category</field>
<field name="res_model">stock.picking</field>
<field name="view_mode">tree,pivot,graph</field>
<field name="context">{'group_by': 'business_category_id'}</field>
</record>
<menuitem id="menu_stock_picking_business_category_report"
name="Inventory by Category"
parent="stock.menu_stock_root"
sequence="50"
action="action_stock_picking_business_category_report"
groups="stock.group_stock_user,stock.group_stock_manager,base.group_system"/>
</odoo>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem id="menu_stock_team"
name="Team Inventory"
parent="stock.menu_stock_root"
sequence="45"
action="action_stock_team"
groups="stock.group_stock_manager"/>
<menuitem id="menu_business_category_from_inventory"
name="Business Categories"
parent="stock.menu_stock_root"
sequence="46"
action="grt_crm_business_category.action_crm_business_category"
groups="stock.group_stock_manager"/>
</odoo>
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_picking_form_business_category" model="ir.ui.view">
<field name="name">stock.picking.form.business.category</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='location_dest_id']" position="after">
<field name="company_id" invisible="1"/>
<field name="business_category_id"/>
<field name="stock_team_id"/>
<field name="inventory_analytic_account_id" readonly="1"/>
</xpath>
</field>
</record>
<record id="view_picking_search_business_category" model="ir.ui.view">
<field name="name">stock.picking.search.business.category</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.vpicktree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='origin']" position="after">
<field name="business_category_id"/>
<field name="stock_team_id"/>
</xpath>
</field>
</record>
<record id="view_move_search_business_category" model="ir.ui.view">
<field name="name">stock.move.search.business.category</field>
<field name="model">stock.move</field>
<field name="inherit_id" ref="stock.view_move_search"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='product_id']" position="after">
<field name="business_category_id"/>
<field name="stock_team_id"/>
</xpath>
</field>
</record>
<record id="view_move_form_inventory_business_category" model="ir.ui.view">
<field name="name">account.move.form.inventory.business.category</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='invoice_payment_term_id']" position="after">
<field name="inventory_business_category_id"/>
<field name="inventory_analytic_account_id"/>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_stock_team_tree" model="ir.ui.view">
<field name="name">stock.team.tree</field>
<field name="model">stock.team</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="company_id"/>
<field name="user_id"/>
<field name="member_ids" widget="many2many_tags"/>
<field name="business_category_id"/>
<field name="active"/>
</tree>
</field>
</record>
<record id="view_stock_team_form" model="ir.ui.view">
<field name="name">stock.team.form</field>
<field name="model">stock.team</field>
<field name="arch" type="xml">
<form string="Inventory Team">
<sheet>
<group>
<field name="name"/>
<field name="company_id"/>
<field name="user_id"/>
<field name="member_ids" widget="many2many_tags"/>
<field name="business_category_id" domain="[('company_id', '=', company_id)]"/>
<field name="active"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_stock_team" model="ir.actions.act_window">
<field name="name">Team Inventory</field>
<field name="res_model">stock.team</field>
<field name="view_mode">tree,form</field>
</record>
</odoo>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_warehouse_form_business_category" model="ir.ui.view">
<field name="name">stock.warehouse.form.business.category</field>
<field name="model">stock.warehouse</field>
<field name="inherit_id" ref="stock.view_warehouse"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='company_id']" position="after">
<field name="business_category_id"/>
<field name="stock_team_id"/>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1 @@
from . import models
@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
{
"name": "GRT Purchase Business Category",
"summary": "Business category segregation and purchase teams for purchasing",
"description": """
Adds business category handling to Purchase Orders with dedicated Purchase Teams,
record rules per business category, and automatic analytic account propagation
for purchasing transactions.
""",
"author": "PT Gagak Rimang Teknologi",
"website": "https://rimang.id",
"category": "Purchases",
"version": "14.0.1.0.0",
"depends": [
"purchase",
"account",
"grt_crm_business_category",
],
"data": [
"security/ir.model.access.csv",
"security/ir.rule.csv",
"views/crm_business_category_views.xml",
"views/purchase_team_views.xml",
"views/purchase_order_views.xml",
"views/purchase_report_business_category_views.xml",
"views/purchase_menu_views.xml",
],
"installable": True,
"application": False,
}
@@ -0,0 +1,4 @@
from . import account_move
from . import crm_business_category
from . import purchase_order
from . import purchase_team
@@ -0,0 +1,89 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class AccountMove(models.Model):
_inherit = "account.move"
purchase_business_category_id = fields.Many2one(
"crm.business.category",
string="Purchase Business Category",
domain="[('company_id', '=', company_id)]",
ondelete="restrict",
copy=False,
)
purchase_analytic_account_id = fields.Many2one(
"account.analytic.account",
string="Purchase Analytic Account",
domain="[('company_id', '=', company_id)]",
ondelete="restrict",
copy=False,
)
@api.onchange("purchase_business_category_id")
def _onchange_purchase_business_category_id_analytic(self):
for move in self:
move.purchase_analytic_account_id = move.purchase_business_category_id.purchase_analytic_account_id
@api.constrains("company_id", "purchase_business_category_id", "purchase_analytic_account_id")
def _check_purchase_business_category_analytic_company(self):
for move in self:
if move.purchase_business_category_id and move.purchase_business_category_id.company_id != move.company_id:
raise ValidationError(
_("Vendor Bill '%s' uses a Business Category from another company.") % (move.display_name,)
)
if move.purchase_analytic_account_id and move.purchase_analytic_account_id.company_id != move.company_id:
raise ValidationError(
_("Vendor Bill '%s' uses an Analytic Account from another company.") % (move.display_name,)
)
if (
move.purchase_business_category_id
and move.purchase_business_category_id.purchase_analytic_account_id
and move.purchase_analytic_account_id
and move.purchase_business_category_id.purchase_analytic_account_id != move.purchase_analytic_account_id
):
raise ValidationError(
_(
"Vendor Bill '%s' must use the Purchase Analytic Account configured on its Business Category."
)
% (move.display_name,)
)
def action_post(self):
result = super().action_post()
for move in self.filtered(lambda m: m.move_type in ("in_invoice", "in_refund")):
analytic = move.purchase_analytic_account_id
if not analytic:
continue
lines = move.line_ids.filtered(
lambda line: line.account_id.internal_type not in ("receivable", "payable")
and not line.analytic_account_id
)
if lines:
lines.write({"analytic_account_id": analytic.id})
return result
class AccountMoveLine(models.Model):
_inherit = "account.move.line"
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get("analytic_account_id"):
continue
move_id = vals.get("move_id")
account_id = vals.get("account_id")
if not move_id or not account_id:
continue
move = self.env["account.move"].browse(move_id)
account = self.env["account.account"].browse(account_id)
if (
move
and move.move_type in ("in_invoice", "in_refund")
and move.purchase_analytic_account_id
and account
and account.internal_type not in ("receivable", "payable")
):
vals["analytic_account_id"] = move.purchase_analytic_account_id.id
return super().create(vals_list)
@@ -0,0 +1,27 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class CrmBusinessCategory(models.Model):
_inherit = "crm.business.category"
purchase_analytic_account_id = fields.Many2one(
"account.analytic.account",
string="Purchase Analytic Account",
domain="[('company_id', '=', company_id)]",
ondelete="restrict",
help="Default analytic account used for purchasing transactions in this business category.",
)
@api.constrains("company_id", "purchase_analytic_account_id")
def _check_purchase_analytic_account_company(self):
for category in self:
if not category.purchase_analytic_account_id or not category.company_id:
continue
if category.purchase_analytic_account_id.company_id != category.company_id:
raise ValidationError(
_(
"Business Category '%s' must use a Purchase Analytic Account from company '%s'."
)
% (category.name, category.company_id.name)
)
@@ -0,0 +1,177 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class PurchaseOrder(models.Model):
_inherit = "purchase.order"
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
default=lambda self: self.env["business.category.mixin"]._default_business_category_id(),
domain="[('company_id', '=', company_id)]",
ondelete="restrict",
tracking=True,
)
analytic_account_id = fields.Many2one(
"account.analytic.account",
string="Purchase Analytic Account",
related="business_category_id.purchase_analytic_account_id",
store=True,
readonly=True,
)
purchase_team_id = fields.Many2one(
"purchase.team",
string="Purchase Team",
domain="[('company_id', '=', company_id), ('business_category_id', '=', business_category_id)]",
tracking=True,
)
def _is_purchase_admin_user(self):
self.ensure_one()
return self.env.user.has_group("purchase.group_purchase_manager") or self.env.user.has_group("base.group_system")
@api.onchange("business_category_id")
def _onchange_business_category_id(self):
for order in self:
if (
order.purchase_team_id
and order.purchase_team_id.business_category_id != order.business_category_id
):
order.purchase_team_id = False
@api.onchange("purchase_team_id")
def _onchange_purchase_team_id(self):
for order in self:
if order.purchase_team_id and order.purchase_team_id.business_category_id:
order.business_category_id = order.purchase_team_id.business_category_id
@api.model_create_multi
def create(self, vals_list):
vals_list = [self._prepare_business_category_vals(vals) for vals in vals_list]
return super().create(vals_list)
def write(self, vals):
vals = self._prepare_business_category_vals(vals)
return super().write(vals)
def _prepare_business_category_vals(self, vals):
vals = dict(vals)
if vals.get("purchase_team_id") and not vals.get("business_category_id"):
team = self.env["purchase.team"].browse(vals["purchase_team_id"])
if team.business_category_id:
vals["business_category_id"] = team.business_category_id.id
return vals
@api.constrains("business_category_id", "purchase_team_id", "company_id", "analytic_account_id")
def _check_business_category_team(self):
for order in self:
if order.business_category_id and order.business_category_id.company_id != order.company_id:
raise ValidationError(
_("Purchase Order '%s' uses a Business Category from another company.") % order.name
)
if order.analytic_account_id and order.analytic_account_id.company_id != order.company_id:
raise ValidationError(
_("Purchase Order '%s' uses an Analytic Account from another company.") % order.name
)
if not order.purchase_team_id:
continue
if order.purchase_team_id.company_id != order.company_id:
raise ValidationError(
_("Purchase Team '%s' belongs to another company.") % order.purchase_team_id.name
)
if not order.purchase_team_id.business_category_id:
raise ValidationError(
_("Purchase Team '%s' must have a Business Category before it can be used.")
% order.purchase_team_id.name
)
if (
order.business_category_id
and order.purchase_team_id.business_category_id != order.business_category_id
):
raise ValidationError(
_(
"Purchase Team '%s' is linked to business category '%s'. "
"Please select a matching business category."
)
% (order.purchase_team_id.name, order.purchase_team_id.business_category_id.name)
)
@api.constrains("business_category_id", "purchase_team_id", "user_id")
def _check_current_user_purchase_scope(self):
current_user = self.env.user
if current_user.has_group("base.group_system"):
return
effective_categories = current_user.effective_business_category_ids
for order in self:
if order.business_category_id and order.business_category_id not in effective_categories:
raise ValidationError(
_("You can only use Purchase Orders in your registered Business Category.")
)
if order.purchase_team_id and current_user not in (order.purchase_team_id.user_id | order.purchase_team_id.member_ids):
raise ValidationError(
_("You must be registered in the selected Purchase Team before using it on a Purchase Order.")
)
def button_confirm(self):
for order in self:
if order.state not in ("draft", "sent", "to approve"):
continue
if not order.business_category_id:
raise ValidationError(
_("Purchase Order '%s' requires a Business Category before confirmation.") % order.name
)
if not order.purchase_team_id:
raise ValidationError(
_("Purchase Order '%s' requires a Purchase Team before confirmation.") % order.name
)
return super().button_confirm()
def _prepare_invoice(self):
vals = super()._prepare_invoice()
vals["purchase_business_category_id"] = self.business_category_id.id
vals["purchase_analytic_account_id"] = self.analytic_account_id.id
return vals
class PurchaseOrderLine(models.Model):
_inherit = "purchase.order.line"
@api.onchange("order_id")
def _onchange_order_id_set_analytic(self):
for line in self:
if "account_analytic_id" in line._fields and line.order_id.analytic_account_id:
line.account_analytic_id = line.order_id.analytic_account_id
@api.onchange("product_id")
def _onchange_product_id_set_analytic(self):
for line in self:
if "account_analytic_id" in line._fields and line.order_id.analytic_account_id:
line.account_analytic_id = line.order_id.analytic_account_id
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get("account_analytic_id") or not vals.get("order_id"):
continue
order = self.env["purchase.order"].browse(vals["order_id"])
if order.analytic_account_id:
vals["account_analytic_id"] = order.analytic_account_id.id
return super().create(vals_list)
def write(self, vals):
vals = dict(vals)
if "account_analytic_id" not in vals:
analytic = self[:1].order_id.analytic_account_id
if analytic:
vals["account_analytic_id"] = analytic.id
return super().write(vals)
def _prepare_account_move_line(self, move=False):
vals = super()._prepare_account_move_line(move=move)
analytic = self.order_id.analytic_account_id
if analytic and not vals.get("analytic_account_id"):
vals["analytic_account_id"] = analytic.id
return vals
@@ -0,0 +1,56 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class PurchaseTeam(models.Model):
_name = "purchase.team"
_description = "Purchase Team"
_order = "name"
name = fields.Char(required=True)
active = fields.Boolean(default=True)
company_id = fields.Many2one(
"res.company",
string="Company",
required=True,
default=lambda self: self.env.company,
)
user_id = fields.Many2one(
"res.users",
string="Team Leader",
help="Responsible user for this Purchase Team.",
)
member_ids = fields.Many2many(
"res.users",
"purchase_team_res_users_rel",
"team_id",
"user_id",
string="Members",
)
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
required=True,
default=lambda self: self.env["business.category.mixin"]._default_business_category_id(),
ondelete="restrict",
domain="[('company_id', '=', company_id)]",
)
@api.onchange("company_id")
def _onchange_company_id_business_category(self):
for team in self:
if team.business_category_id and team.business_category_id.company_id != team.company_id:
team.business_category_id = False
@api.constrains("company_id", "business_category_id")
def _check_business_category_company(self):
for team in self:
if not team.company_id or not team.business_category_id:
continue
if team.business_category_id.company_id != team.company_id:
raise ValidationError(
_(
"Purchase Team '%s' must use a Business Category from company '%s'."
)
% (team.name, team.company_id.name)
)
@@ -0,0 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_purchase_team_user,purchase.team user,model_purchase_team,purchase.group_purchase_user,1,0,0,0
access_purchase_team_manager,purchase.team manager,model_purchase_team,purchase.group_purchase_manager,1,1,1,1
access_purchase_team_sysadmin,purchase.team sysadmin,model_purchase_team,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_purchase_team_user purchase.team user model_purchase_team purchase.group_purchase_user 1 0 0 0
3 access_purchase_team_manager purchase.team manager model_purchase_team purchase.group_purchase_manager 1 1 1 1
4 access_purchase_team_sysadmin purchase.team sysadmin model_purchase_team base.group_system 1 1 1 1
@@ -0,0 +1,14 @@
id,name,model_id:id,domain_force,groups:id,perm_read,perm_write,perm_create,perm_unlink
purchase_order_business_category_rule_user_read_create,Purchase Order read/create by effective business category,purchase.model_purchase_order,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",purchase.group_purchase_user,1,0,1,0
purchase_order_business_category_rule_user_write_own,Purchase Order write own by effective business category,purchase.model_purchase_order,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids),'|',('user_id','=',user.id),('create_uid','=',user.id)]",purchase.group_purchase_user,0,1,0,1
purchase_order_business_category_rule_manager,Purchase Order full access for purchase manager in effective business category,purchase.model_purchase_order,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",purchase.group_purchase_manager,1,1,1,1
purchase_order_business_category_rule_sysadmin,Purchase Order full access for system admin,purchase.model_purchase_order,"[('company_id','in',user.company_ids.ids)]",base.group_system,1,1,1,1
purchase_team_business_category_rule_user,Purchase Team read by effective business category,model_purchase_team,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",purchase.group_purchase_user,1,0,0,0
purchase_team_business_category_rule_manager,Purchase Team full access for purchase manager in effective business category,model_purchase_team,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",purchase.group_purchase_manager,1,1,1,1
purchase_team_business_category_rule_sysadmin,Purchase Team full access for system admin,model_purchase_team,"[('company_id','in',user.company_ids.ids)]",base.group_system,1,1,1,1
crm_business_category_rule_purchase_user,Business Category by user effective list for purchase user,grt_crm_business_category.model_crm_business_category,"[('company_id','in',user.company_ids.ids),('id','in',user.effective_business_category_ids.ids)]",purchase.group_purchase_user,1,0,0,0
crm_business_category_rule_purchase_manager,Business Category access for purchase manager in effective business category,grt_crm_business_category.model_crm_business_category,"[('company_id','in',user.company_ids.ids),('id','in',user.effective_business_category_ids.ids)]",purchase.group_purchase_manager,1,1,1,1
account_move_purchase_business_category_rule_user,Vendor Bill read by effective business category for purchase user,account.model_account_move,"[('company_id','in',user.company_ids.ids),('purchase_business_category_id','in',user.effective_business_category_ids.ids)]",purchase.group_purchase_user,1,0,0,0
account_move_purchase_business_category_rule_manager,Vendor Bill access for purchase manager in effective business category,account.model_account_move,"[('company_id','in',user.company_ids.ids),('purchase_business_category_id','in',user.effective_business_category_ids.ids)]",purchase.group_purchase_manager,1,1,1,1
account_move_line_purchase_business_category_rule_user,Journal Item read by effective business category for purchase user,account.model_account_move_line,"[('company_id','in',user.company_ids.ids),('move_id.purchase_business_category_id','in',user.effective_business_category_ids.ids)]",purchase.group_purchase_user,1,0,0,0
account_move_line_purchase_business_category_rule_manager,Journal Item access for purchase manager in effective business category,account.model_account_move_line,"[('company_id','in',user.company_ids.ids),('move_id.purchase_business_category_id','in',user.effective_business_category_ids.ids)]",purchase.group_purchase_manager,1,1,1,1
1 id name model_id:id domain_force groups:id perm_read perm_write perm_create perm_unlink
2 purchase_order_business_category_rule_user_read_create Purchase Order read/create by effective business category purchase.model_purchase_order [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] purchase.group_purchase_user 1 0 1 0
3 purchase_order_business_category_rule_user_write_own Purchase Order write own by effective business category purchase.model_purchase_order [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids),'|',('user_id','=',user.id),('create_uid','=',user.id)] purchase.group_purchase_user 0 1 0 1
4 purchase_order_business_category_rule_manager Purchase Order full access for purchase manager in effective business category purchase.model_purchase_order [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] purchase.group_purchase_manager 1 1 1 1
5 purchase_order_business_category_rule_sysadmin Purchase Order full access for system admin purchase.model_purchase_order [('company_id','in',user.company_ids.ids)] base.group_system 1 1 1 1
6 purchase_team_business_category_rule_user Purchase Team read by effective business category model_purchase_team [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] purchase.group_purchase_user 1 0 0 0
7 purchase_team_business_category_rule_manager Purchase Team full access for purchase manager in effective business category model_purchase_team [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] purchase.group_purchase_manager 1 1 1 1
8 purchase_team_business_category_rule_sysadmin Purchase Team full access for system admin model_purchase_team [('company_id','in',user.company_ids.ids)] base.group_system 1 1 1 1
9 crm_business_category_rule_purchase_user Business Category by user effective list for purchase user grt_crm_business_category.model_crm_business_category [('company_id','in',user.company_ids.ids),('id','in',user.effective_business_category_ids.ids)] purchase.group_purchase_user 1 0 0 0
10 crm_business_category_rule_purchase_manager Business Category access for purchase manager in effective business category grt_crm_business_category.model_crm_business_category [('company_id','in',user.company_ids.ids),('id','in',user.effective_business_category_ids.ids)] purchase.group_purchase_manager 1 1 1 1
11 account_move_purchase_business_category_rule_user Vendor Bill read by effective business category for purchase user account.model_account_move [('company_id','in',user.company_ids.ids),('purchase_business_category_id','in',user.effective_business_category_ids.ids)] purchase.group_purchase_user 1 0 0 0
12 account_move_purchase_business_category_rule_manager Vendor Bill access for purchase manager in effective business category account.model_account_move [('company_id','in',user.company_ids.ids),('purchase_business_category_id','in',user.effective_business_category_ids.ids)] purchase.group_purchase_manager 1 1 1 1
13 account_move_line_purchase_business_category_rule_user Journal Item read by effective business category for purchase user account.model_account_move_line [('company_id','in',user.company_ids.ids),('move_id.purchase_business_category_id','in',user.effective_business_category_ids.ids)] purchase.group_purchase_user 1 0 0 0
14 account_move_line_purchase_business_category_rule_manager Journal Item access for purchase manager in effective business category account.model_account_move_line [('company_id','in',user.company_ids.ids),('move_id.purchase_business_category_id','in',user.effective_business_category_ids.ids)] purchase.group_purchase_manager 1 1 1 1
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_crm_business_category_form_purchase_analytic" model="ir.ui.view">
<field name="name">crm.business.category.form.purchase.analytic</field>
<field name="model">crm.business.category</field>
<field name="inherit_id" ref="grt_crm_business_category.view_crm_business_category_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='description']/.." position="before">
<group string="Purchasing">
<field name="company_id" invisible="1"/>
<field name="purchase_analytic_account_id" domain="[('company_id', '=', company_id)]"/>
</group>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem id="menu_purchase_team"
name="Team Purchases"
parent="purchase.menu_purchase_root"
sequence="45"
action="action_purchase_team"
groups="purchase.group_purchase_manager"/>
<menuitem id="menu_business_category_from_purchase"
name="Business Categories"
parent="purchase.menu_purchase_root"
sequence="46"
action="grt_crm_business_category.action_crm_business_category"
groups="purchase.group_purchase_manager"/>
</odoo>
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_purchase_order_form_business_category" model="ir.ui.view">
<field name="name">purchase.order.form.business.category</field>
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.purchase_order_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="after">
<field name="company_id" invisible="1"/>
<field name="business_category_id"/>
<field name="purchase_team_id"/>
<field name="analytic_account_id" readonly="1"/>
</xpath>
</field>
</record>
<record id="view_purchase_order_filter_business_category" model="ir.ui.view">
<field name="name">purchase.order.search.business.category</field>
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.view_purchase_order_filter"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='user_id']" position="after">
<field name="business_category_id"/>
<field name="purchase_team_id"/>
</xpath>
<xpath expr="//search" position="inside">
<filter string="Business Category" name="group_by_business_category" context="{'group_by': 'business_category_id'}"/>
<filter string="Purchase Team" name="group_by_purchase_team" context="{'group_by': 'purchase_team_id'}"/>
</xpath>
</field>
</record>
<record id="view_move_form_purchase_business_category" model="ir.ui.view">
<field name="name">account.move.form.purchase.business.category</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='invoice_payment_term_id']" position="after">
<field name="purchase_business_category_id" attrs="{'invisible': [('move_type', 'not in', ('in_invoice', 'in_refund'))]}"/>
<field name="purchase_analytic_account_id" attrs="{'invisible': [('move_type', 'not in', ('in_invoice', 'in_refund'))]}"/>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_purchase_order_pivot_business_category_report" model="ir.ui.view">
<field name="name">purchase.order.pivot.business.category.report</field>
<field name="model">purchase.order</field>
<field name="arch" type="xml">
<pivot string="Purchases by Business Category">
<field name="business_category_id" type="row"/>
<field name="purchase_team_id" type="row"/>
<field name="state" type="col"/>
<field name="date_order" interval="month" type="col"/>
<field name="amount_total" type="measure"/>
<field name="id" type="measure" string="Orders"/>
</pivot>
</field>
</record>
<record id="view_purchase_order_graph_business_category_report" model="ir.ui.view">
<field name="name">purchase.order.graph.business.category.report</field>
<field name="model">purchase.order</field>
<field name="arch" type="xml">
<graph string="Purchases by Business Category" type="bar">
<field name="business_category_id"/>
<field name="amount_total" type="measure"/>
</graph>
</field>
</record>
<record id="action_purchase_order_business_category_report" model="ir.actions.act_window">
<field name="name">Purchases by Business Category</field>
<field name="res_model">purchase.order</field>
<field name="view_mode">tree,pivot,graph</field>
<field name="context">{'group_by': 'business_category_id'}</field>
<field name="search_view_id" ref="view_purchase_order_filter_business_category"/>
</record>
<menuitem id="menu_purchase_order_business_category_report"
name="Purchases by Category"
parent="purchase.menu_purchase_root"
sequence="50"
action="action_purchase_order_business_category_report"
groups="purchase.group_purchase_user,purchase.group_purchase_manager,base.group_system"/>
</odoo>
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_purchase_team_tree" model="ir.ui.view">
<field name="name">purchase.team.tree</field>
<field name="model">purchase.team</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="company_id"/>
<field name="user_id"/>
<field name="member_ids" widget="many2many_tags"/>
<field name="business_category_id"/>
<field name="active"/>
</tree>
</field>
</record>
<record id="view_purchase_team_form" model="ir.ui.view">
<field name="name">purchase.team.form</field>
<field name="model">purchase.team</field>
<field name="arch" type="xml">
<form string="Purchase Team">
<sheet>
<group>
<field name="name"/>
<field name="company_id"/>
<field name="user_id"/>
<field name="member_ids" widget="many2many_tags"/>
<field name="business_category_id" domain="[('company_id', '=', company_id)]"/>
<field name="active"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_purchase_team" model="ir.actions.act_window">
<field name="name">Team Purchases</field>
<field name="res_model">purchase.team</field>
<field name="view_mode">tree,form</field>
</record>
</odoo>
@@ -34,6 +34,7 @@ plus a two-step approval flow: Sales Team Leader then Accounting Manager.
"views/res_partner_views.xml",
"views/crm_team_views.xml",
"views/sale_order_views.xml",
"views/sale_report_business_category_views.xml",
"views/sale_team_menu_views.xml",
],
"installable": True,
@@ -195,7 +195,7 @@ class SaleOrder(models.Model):
@api.constrains("business_category_id", "team_id", "user_id")
def _check_current_user_sales_scope(self):
current_user = self.env.user
if current_user.has_group("sales_team.group_sale_manager") or current_user.has_group("base.group_system"):
if current_user.has_group("base.group_system"):
return
effective_categories = current_user.effective_business_category_ids
@@ -1,11 +1,17 @@
id,name,model_id:id,domain_force,groups:id,perm_read,perm_write,perm_create,perm_unlink
sale_order_business_category_rule_user_read_create,Sale Order read/create by effective business category,sale.model_sale_order,"[('business_category_id','in',user.effective_business_category_ids.ids)]",sales_team.group_sale_salesman,1,0,1,0
sale_order_business_category_rule_user_write_own,Sale Order write own by effective business category,sale.model_sale_order,"[('business_category_id','in',user.effective_business_category_ids.ids),'|',('user_id','=',user.id),('create_uid','=',user.id)]",sales_team.group_sale_salesman,0,1,0,1
sale_order_business_category_rule_manager,Sale Order full access for sales manager,sale.model_sale_order,"[(1,'=',1)]",sales_team.group_sale_manager,1,1,1,1
sale_order_business_category_rule_manager,Sale Order full access for sales manager in effective business category,sale.model_sale_order,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",sales_team.group_sale_manager,1,1,1,1
sale_order_business_category_rule_sysadmin,Sale Order full access for system admin,sale.model_sale_order,"[(1,'=',1)]",base.group_system,1,1,1,1
crm_team_business_category_rule_sales_user,CRM Team read by effective business category for sales user,sales_team.model_crm_team,"[('business_category_id','in',user.effective_business_category_ids.ids)]",sales_team.group_sale_salesman,1,0,0,0
crm_team_business_category_rule_sales_manager,CRM Team full access for sales manager,sales_team.model_crm_team,"[(1,'=',1)]",sales_team.group_sale_manager,1,1,1,1
crm_team_business_category_rule_sales_manager,CRM Team full access for sales manager in effective business category,sales_team.model_crm_team,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",sales_team.group_sale_manager,1,1,1,1
crm_business_category_rule_sales_user,Business Category by user effective list for sales user,grt_crm_business_category.model_crm_business_category,"[('id','in',user.effective_business_category_ids.ids)]",sales_team.group_sale_salesman,1,0,0,0
crm_business_category_rule_sales_manager,Business Category full access for sales manager,grt_crm_business_category.model_crm_business_category,"[(1,'=',1)]",sales_team.group_sale_manager,1,1,1,1
crm_business_category_rule_sales_manager,Business Category access for sales manager in effective business category,grt_crm_business_category.model_crm_business_category,"[('company_id','in',user.company_ids.ids),('id','in',user.effective_business_category_ids.ids)]",sales_team.group_sale_manager,1,1,1,1
customer_behavior_analysis_rule_sales_user,Customer Behavior Analysis by effective business category,model_customer_behavior_analysis,"[('business_category_id','in',user.effective_business_category_ids.ids)]",sales_team.group_sale_salesman,1,0,0,0
customer_behavior_analysis_rule_sales_manager,Customer Behavior Analysis full access for sales manager,model_customer_behavior_analysis,"[(1,'=',1)]",sales_team.group_sale_manager,1,1,1,1
customer_behavior_analysis_rule_sales_manager,Customer Behavior Analysis full access for sales manager in effective business category,model_customer_behavior_analysis,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",sales_team.group_sale_manager,1,1,1,1
account_move_business_category_rule_sales_user,Invoice read by effective business category for sales user,account.model_account_move,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",sales_team.group_sale_salesman,1,0,0,0
account_move_business_category_rule_sales_manager,Invoice access for sales manager in effective business category,account.model_account_move,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",sales_team.group_sale_manager,1,1,1,1
account_payment_business_category_rule_sales_user,Payment read by effective business category for sales user,account.model_account_payment,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",sales_team.group_sale_salesman,1,0,0,0
account_payment_business_category_rule_sales_manager,Payment access for sales manager in effective business category,account.model_account_payment,"[('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)]",sales_team.group_sale_manager,1,1,1,1
account_move_line_business_category_rule_sales_user,Journal Item read by effective business category for sales user,account.model_account_move_line,"[('company_id','in',user.company_ids.ids),('move_id.business_category_id','in',user.effective_business_category_ids.ids)]",sales_team.group_sale_salesman,1,0,0,0
account_move_line_business_category_rule_sales_manager,Journal Item access for sales manager in effective business category,account.model_account_move_line,"[('company_id','in',user.company_ids.ids),('move_id.business_category_id','in',user.effective_business_category_ids.ids)]",sales_team.group_sale_manager,1,1,1,1
1 id name model_id:id domain_force groups:id perm_read perm_write perm_create perm_unlink
2 sale_order_business_category_rule_user_read_create Sale Order read/create by effective business category sale.model_sale_order [('business_category_id','in',user.effective_business_category_ids.ids)] sales_team.group_sale_salesman 1 0 1 0
3 sale_order_business_category_rule_user_write_own Sale Order write own by effective business category sale.model_sale_order [('business_category_id','in',user.effective_business_category_ids.ids),'|',('user_id','=',user.id),('create_uid','=',user.id)] sales_team.group_sale_salesman 0 1 0 1
4 sale_order_business_category_rule_manager Sale Order full access for sales manager Sale Order full access for sales manager in effective business category sale.model_sale_order [(1,'=',1)] [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] sales_team.group_sale_manager 1 1 1 1
5 sale_order_business_category_rule_sysadmin Sale Order full access for system admin sale.model_sale_order [(1,'=',1)] base.group_system 1 1 1 1
6 crm_team_business_category_rule_sales_user CRM Team read by effective business category for sales user sales_team.model_crm_team [('business_category_id','in',user.effective_business_category_ids.ids)] sales_team.group_sale_salesman 1 0 0 0
7 crm_team_business_category_rule_sales_manager CRM Team full access for sales manager CRM Team full access for sales manager in effective business category sales_team.model_crm_team [(1,'=',1)] [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] sales_team.group_sale_manager 1 1 1 1
8 crm_business_category_rule_sales_user Business Category by user effective list for sales user grt_crm_business_category.model_crm_business_category [('id','in',user.effective_business_category_ids.ids)] sales_team.group_sale_salesman 1 0 0 0
9 crm_business_category_rule_sales_manager Business Category full access for sales manager Business Category access for sales manager in effective business category grt_crm_business_category.model_crm_business_category [(1,'=',1)] [('company_id','in',user.company_ids.ids),('id','in',user.effective_business_category_ids.ids)] sales_team.group_sale_manager 1 1 1 1
10 customer_behavior_analysis_rule_sales_user Customer Behavior Analysis by effective business category model_customer_behavior_analysis [('business_category_id','in',user.effective_business_category_ids.ids)] sales_team.group_sale_salesman 1 0 0 0
11 customer_behavior_analysis_rule_sales_manager Customer Behavior Analysis full access for sales manager Customer Behavior Analysis full access for sales manager in effective business category model_customer_behavior_analysis [(1,'=',1)] [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] sales_team.group_sale_manager 1 1 1 1
12 account_move_business_category_rule_sales_user Invoice read by effective business category for sales user account.model_account_move [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] sales_team.group_sale_salesman 1 0 0 0
13 account_move_business_category_rule_sales_manager Invoice access for sales manager in effective business category account.model_account_move [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] sales_team.group_sale_manager 1 1 1 1
14 account_payment_business_category_rule_sales_user Payment read by effective business category for sales user account.model_account_payment [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] sales_team.group_sale_salesman 1 0 0 0
15 account_payment_business_category_rule_sales_manager Payment access for sales manager in effective business category account.model_account_payment [('company_id','in',user.company_ids.ids),('business_category_id','in',user.effective_business_category_ids.ids)] sales_team.group_sale_manager 1 1 1 1
16 account_move_line_business_category_rule_sales_user Journal Item read by effective business category for sales user account.model_account_move_line [('company_id','in',user.company_ids.ids),('move_id.business_category_id','in',user.effective_business_category_ids.ids)] sales_team.group_sale_salesman 1 0 0 0
17 account_move_line_business_category_rule_sales_manager Journal Item access for sales manager in effective business category account.model_account_move_line [('company_id','in',user.company_ids.ids),('move_id.business_category_id','in',user.effective_business_category_ids.ids)] sales_team.group_sale_manager 1 1 1 1
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_sale_order_pivot_business_category_report" model="ir.ui.view">
<field name="name">sale.order.pivot.business.category.report</field>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<pivot string="Sales by Business Category">
<field name="business_category_id" type="row"/>
<field name="team_id" type="row"/>
<field name="state" type="col"/>
<field name="date_order" interval="month" type="col"/>
<field name="amount_total" type="measure"/>
<field name="id" type="measure" string="Orders"/>
</pivot>
</field>
</record>
<record id="view_sale_order_graph_business_category_report" model="ir.ui.view">
<field name="name">sale.order.graph.business.category.report</field>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<graph string="Sales by Business Category" type="bar">
<field name="business_category_id"/>
<field name="amount_total" type="measure"/>
</graph>
</field>
</record>
<record id="action_sale_order_business_category_report" model="ir.actions.act_window">
<field name="name">Sales by Business Category</field>
<field name="res_model">sale.order</field>
<field name="view_mode">tree,pivot,graph</field>
<field name="context">{'group_by': 'business_category_id'}</field>
<field name="search_view_id" ref="view_sales_order_filter_business_category"/>
</record>
<menuitem id="menu_sale_order_business_category_report"
name="Sales by Category"
parent="sale.sale_order_menu"
sequence="50"
action="action_sale_order_business_category_report"
groups="sales_team.group_sale_salesman,sales_team.group_sale_manager,base.group_system"/>
</odoo>
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env python3
import xmlrpc.client
URL = "http://localhost:8070"
DB = "kanjabung_MRP"
USER = "admin"
PWD = "admin"
common = xmlrpc.client.ServerProxy("%s/xmlrpc/2/common" % URL)
uid = common.authenticate(DB, USER, PWD, {})
if not uid:
raise SystemExit("AUTH_FAILED")
models = xmlrpc.client.ServerProxy("%s/xmlrpc/2/object" % URL)
view_ids = models.execute_kw(
DB, uid, PWD,
"ir.ui.view", "search_read",
[[("model", "=", "hr.expense")]],
{"fields": ["id", "name", "key"], "limit": 200, "order": "id asc"},
)
print("hr.expense view candidates:")
for v in view_ids:
print(v.get("id"), "|", v.get("key"), "|", v.get("name"))
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env python3
import xmlrpc.client
URL = "http://localhost:8070"
DB = "kanjabung_MRP"
USER = "admin"
PWD = "admin"
common = xmlrpc.client.ServerProxy("%s/xmlrpc/2/common" % URL)
uid = common.authenticate(DB, USER, PWD, {})
models = xmlrpc.client.ServerProxy("%s/xmlrpc/2/object" % URL)
imd = models.execute_kw(
DB, uid, PWD,
"ir.model.data", "search_read",
[[("module", "=", "hr_expense"), ("model", "=", "ir.ui.view")]],
{"fields": ["name", "res_id"], "limit": 500, "order": "name asc"},
)
print("hr_expense view xmlids:")
for row in imd:
print("hr_expense.%s -> %s" % (row.get("name"), row.get("res_id")))
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env python3
import xmlrpc.client
URL = "http://localhost:8070"
DB = "kanjabung_MRP"
USER = "admin"
PWD = "admin"
common = xmlrpc.client.ServerProxy("%s/xmlrpc/2/common" % URL)
uid = common.authenticate(DB, USER, PWD, {})
models = xmlrpc.client.ServerProxy("%s/xmlrpc/2/object" % URL)
imd = models.execute_kw(
DB, uid, PWD,
"ir.model.data", "search_read",
[[("module", "=", "stock"), ("model", "=", "ir.ui.view")]],
{"fields": ["name", "res_id"], "limit": 1200},
)
for row in imd:
name = row.get("name")
if "move" in name or "picking" in name or "filter" in name:
print("stock.%s -> %s" % (name, row.get("res_id")))

Some files were not shown because too many files have changed in this diff Show More