This commit is contained in:
esam 2026-01-05 16:42:04 -05:00
parent 9a432e947e
commit fcbfb11b56
92 changed files with 5447 additions and 0 deletions

View File

@ -0,0 +1,3 @@
from . import models

View File

@ -0,0 +1,18 @@
{
'name': "Customer duplicate validation",
'author': 'Ascetic Business Solution',
'category': 'Odex30-Accounting/Odex25-Accounting',
'summary': """Notify about duplicate while creating partner""",
'website': 'http://www.asceticbs.com',
'license': 'AGPL-3',
'description': """
""",
'version': '18.0',
'depends': ['base', 'sale'],
'data': ['security/security.xml'],
'images': ['static/description/banner.png'],
'installable': True,
'application': True,
'auto_install': False,
}

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
#################################################################################
#
# Odoo, Open Source Management Solution
# Copyright (C) 2022-today Ascetic Business Solution <www.asceticbs.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#################################################################################
from . import res_partner

View File

@ -0,0 +1,49 @@
from odoo import api,fields,models,_
from odoo.exceptions import ValidationError
class ResPartner(models.Model):
_inherit="res.partner"
def get_partner_list(self,partner_objs):
partner_list = ''
for partner in partner_objs:
partner_list = partner_list + ' || ' + partner.name
return partner_list
@api.onchange('name')
def onchange_name(self):
if self.name and self.env.user.has_group("abs_customer_validation.group_activate_customer_validation"):
if self.env['res.partner'].search([('name','=',self.name)]):
raise ValidationError(_('The record ' +self.name+' is already exist '))
@api.onchange('phone')
def onchange_phonenumber(self):
if self.phone and self.env.user.has_group("abs_customer_validation.group_activate_customer_validation"):
partner_objs = self.env['res.partner'].search([('phone','=',self.phone)])
if self.get_partner_list(partner_objs):
raise ValidationError(_('Phone number '+str(self.phone)+' is already exist in the following records:' + '\n' + self.get_partner_list(partner_objs)))
@api.onchange('mobile')
def onchange_mobilenumber(self):
if self.mobile and self.env.user.has_group("abs_customer_validation.group_activate_customer_validation"):
partner_objs = self.env['res.partner'].search([('mobile','=',self.mobile)])
if self.get_partner_list(partner_objs):
raise ValidationError(_('Mobile number '+str(self.mobile)+' is already exist in the following records:' + '\n' + self.get_partner_list(partner_objs)))
@api.onchange('email')
def onchange_email(self):
if self.email and self.env.user.has_group("abs_customer_validation.group_activate_customer_validation"):
partner_objs = self.env['res.partner'].search([('email','=',self.email)])
if self.get_partner_list(partner_objs):
raise ValidationError(_('Email number '+str(self.email)+' is already exist in the following records:' + '\n' + self.get_partner_list(partner_objs)))
@api.onchange('website')
def onchange_website(self):
if self.website and self.env.user.has_group("abs_customer_validation.group_activate_customer_validation"):
website_id = "http://"+str(self.website)
partner_objs = self.env['res.partner'].search([('website','=',website_id)])
if self.get_partner_list(partner_objs):
raise ValidationError(_('Website number '+str(self.website)+' is already exist in the following records:' + '\n' + self.get_partner_list(partner_objs)))

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="group_activate_customer_validation" model="res.groups">
<field name="name">Activate customer validation</field>
<field name="category_id" ref="base.module_category_hidden"/>
</record>
</data>
</openerp>

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

@ -0,0 +1,60 @@
<html>
<body>
<section class="oe_container oe_dark">
<div class="oe_row">
<div class="oe_row">
<h2 class="oe_slogan oe_span10">Notify about duplicate while creating partner</h2>
</div>
<div style="margin: 16px 8%;">
<p class='oe_mt32 text-justify' style="font-size: 15px;">
This module will help you to activate the validation on the partner. Check 'Activate customer validation' access group from the user. This validation helps to take preventive steps to stop creating the duplicate partners. Odoo will notify the user instantly with the list of records which are potentially duplicate while they are adding information (like Name, Phone, Mobile, Fax, Email, Website) on the customer. Please note, this moduel is not to stop creating duplicate partner but notify the user so they should be aware about possible duplication while creating the partner.
</p>
</div>
</div>
</section>
<section class="oe_container">
<h2 class="oe_slogan" style="margin-top:20px;">Need help?</h2>
<div style="margin: 16px 8%;">
<p class='oe_mt32 center-block' style="font-size: 15px;">
Contact this module maintainer for any question, need support or request for the new feature : <br/>
* Riken Bhorania <i class="fa fa-whatsapp"></i> +91 9427425799, <i class="fa fa-skype fa_custom"></i> <a href="skype:riken.bhorania?chat">riken.bhorania, </a> <i class="fa fa-envelope"></i> riken.bhorania@asceticbs.com <br/>
* Bhaumin Chorera <i class="fa fa-whatsapp"></i> +91 8530000384, <i class="fa fa-skype fa_custom"></i> <a href="skype:bhaumin.chorera?chat">bhaumin.chorera, </a> <i class="fa fa-envelope"></i> bhaumin.chorera@asceticbs.com <br/>
</p>
</div>
<div class="oe_slogan" style="margin-top:10px !important;">
<a class="btn btn-primary btn-lg mt8"
style="color: #FFFFFF !important;" href="http://www.asceticbs.com"><i
class="fa fa-envelope"></i> Website </a>
<a class="btn btn-primary btn-lg mt8" style="color: #FFFFFF !important;"
href="http://www.asceticbs.com/contact-us"><i
class="fa fa-phone"></i> Contact Us </a>
<a class="btn btn-primary btn-lg mt8" style="color: #FFFFFF !important;"
href="http://www.asceticbs.com/services"><i
class="fa fa-check-square"></i> Services </a>
<a class="btn btn-primary btn-lg mt8" style="color: #FFFFFF !important;"
href="https://apps.odoo.com/apps/modules/browse?author=Ascetic%20Business%20Solution"><i
class="fa fa-binoculars"></i> More Apps </a>
</div>
<div class="oe_slogan" style="margin-top:10px !important;">
<img src="company-logo.png" style="width: 190px; margin-bottom: 20px;" class="center-block">
</div>
<div class="oe_slogan" style="margin-top:10px !important;">
<a href="https://twitter.com/asceticbs" target="_blank"><i class="fa fa-2x fa-twitter" style="color:white;background: #00a0d1;width:35px;"></i></a></td>
<a href="https://www.linkedin.com/company/ascetic-business-solution-llp" target="_blank"><i class="fa fa-2x fa-linkedin" style="color:white;background: #31a3d6;width:35px;padding-left: 3px;"></i></a></td>
<a href="https://www.facebook.com/asceticbs" target="_blank"><i class="fa fa-2x fa-facebook" style="color:white;background: #3b5998;width:35px;padding-left: 8px;"></i></a></td>
<a href="https://www.youtube.com/channel/UCsozahEAndQ2whjcuDIBNZQ" target="_blank"><i class="fa fa-2x fa-youtube-play" style="color:white;background: #c53c2c;width:35px;padding-left: 3px;"></i></a></td>
</div>
</div>
</section>
</body>
</html>

View File

@ -0,0 +1,3 @@
from . import test_customer_validation

View File

@ -0,0 +1,72 @@
from odoo.tests import common, tagged
from odoo.exceptions import ValidationError
@tagged('post_install', '-at_install')
class TestCustomerValidation(common.TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user_no_group = cls.env['res.users'].sudo().create({
'name': 'User No Group', 'login': 'no_group', 'email': 'no@group.com'
})
cls.user_with_group = cls.env['res.users'].sudo().create({
'name': 'User With Group', 'login': 'with_group', 'email': 'with@group.com'
})
cls.group = cls.env.ref('abs_customer_validation.group_activate_customer_validation')
cls.group.sudo().users |= cls.user_with_group
cls.existing_partner1 = cls.env['res.partner'].sudo().create({
'name': 'Existing Partner 1', 'phone': '+1234567890', 'mobile': '+1987654321',
'email': 'test@example.com', 'website': 'http://test.com'
})
cls.existing_partner2 = cls.env['res.partner'].sudo().create({
'name': 'Existing Partner 2', 'phone': '+1234567890'
})
def test_onchange_name_no_group(self):
partner = self.env['res.partner'].with_user(self.user_no_group.id).new({'name': 'Existing Partner 1'})
partner.onchange_name() # لا خطأ
def test_onchange_name_with_group(self):
partner = self.env['res.partner'].with_user(self.user_with_group.id).new({'name': 'Existing Partner 1'})
with self.assertRaises(ValidationError):
partner.onchange_name()
def test_onchange_phone_no_group(self):
partner = self.env['res.partner'].with_user(self.user_no_group.id).new({'phone': '+1234567890'})
partner.onchange_phonenumber() # لا خطأ
def test_onchange_phone_with_group(self):
partner = self.env['res.partner'].with_user(self.user_with_group.id).new({'phone': '+1234567890'})
with self.assertRaises(ValidationError) as cm:
partner.onchange_phonenumber()
self.assertIn('Existing Partner 1', str(cm.exception))
self.assertIn('Existing Partner 2', str(cm.exception))
def test_onchange_phone_unique(self):
partner = self.env['res.partner'].with_user(self.user_with_group.id).new({'phone': '+9999999999'})
partner.onchange_phonenumber() # يمر
def test_onchange_mobile_with_group(self):
partner = self.env['res.partner'].with_user(self.user_with_group.id).new({'mobile': '+1987654321'})
with self.assertRaises(ValidationError):
partner.onchange_mobilenumber()
def test_onchange_email_with_group(self):
partner = self.env['res.partner'].with_user(self.user_with_group.id).new({'email': 'test@example.com'})
with self.assertRaises(ValidationError):
partner.onchange_email()
def test_onchange_website_with_group(self):
partner = self.env['res.partner'].with_user(self.user_with_group.id).new({'website': 'test.com'})
with self.assertRaises(ValidationError):
partner.onchange_website()
def test_empty_fields(self):
partner = self.env['res.partner'].with_user(self.user_with_group.id).new({})
partner.onchange_name()
partner.onchange_phonenumber()
partner.onchange_email() # كلها تمر

View File

@ -0,0 +1 @@
from . import models

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
{
'name': "Account Attachments",
'version': '18.0.1.0.0',
'category': 'Account',
'summary': 'Helps to view all documents attached to account',
'description': """account Attachments module allows user to view
all the documents attached to account.""",
'company': 'Expert',
'website': 'https://www.expert.com',
'depends': ['base','account'],
'data': [
'views/account_move_view.xml'
],
'license': 'AGPL-3',
'installable': True,
'auto_install': False,
'application': True,
}

View File

@ -0,0 +1,49 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * account_attachments
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-05 15:34+0000\n"
"PO-Revision-Date: 2026-01-05 15:34+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: account_attachments
#: model:ir.model.fields,field_description:account_attachments.field_account_bank_statement_line__attach_no
#: model:ir.model.fields,field_description:account_attachments.field_account_move__attach_no
msgid "Attach No"
msgstr "مرفق"
#. module: account_attachments
#: model_terms:ir.ui.view,arch_db:account_attachments.account_move_view
msgid "Documents"
msgstr "المرفقات"
#. module: account_attachments
#: model:ir.model,name:account_attachments.model_account_move
msgid "Journal Entry"
msgstr "قيد اليومية"
#. module: account_attachments
#: model_terms:ir.ui.view,arch_db:account_attachments.account_move_view
msgid "Model Info"
msgstr ""
#. module: account_attachments
#: model:ir.model.fields,field_description:account_attachments.field_account_bank_statement_line__res_id
#: model:ir.model.fields,field_description:account_attachments.field_account_move__res_id
msgid "Res"
msgstr ""
#. module: account_attachments
#: model:ir.model.fields,field_description:account_attachments.field_account_bank_statement_line__res_model
#: model:ir.model.fields,field_description:account_attachments.field_account_move__res_model
msgid "Res Model"
msgstr ""

View File

@ -0,0 +1 @@
from . import account_move

View File

@ -0,0 +1,133 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
class AccountMove(models.Model):
_inherit = 'account.move'
attach_no = fields.Integer(compute='get_attachments')
res_id = fields.Integer()
res_model = fields.Char()
def get_attachments(self):
print("=== get_attachments START ===")
Attachment = self.env['ir.attachment']
action = self.env['ir.actions.act_window']._for_xml_id('base.action_attachment')
if len(self) > 1:
print("Multiple records mode - len:", len(self))
all_models_ids = set()
for record in self:
related_pairs = set()
related_pairs.add((record._name, record.id))
# PO مباشر أو تلقائي من origin/ref
po_id = record.purchase_id.id if record.purchase_id else False
if not po_id and record.invoice_origin:
po = self.env['purchase.order'].search([('name', '=', record.invoice_origin)], limit=1)
if po:
po_id = po.id
print("Auto-found PO by origin:", po_id)
elif not po_id and record.ref:
po = self.env['purchase.order'].search([('name', '=', record.ref)], limit=1)
if po:
po_id = po.id
print("Auto-found PO by ref:", po_id)
if po_id:
po = self.env['purchase.order'].browse(po_id)
related_pairs.add((po._name, po.id))
print("Added PO:", (po._name, po.id))
# Request ID - آمن
if hasattr(po, 'request_id') and po.request_id:
related_pairs.add((po.request_id._name, po.request_id.id))
print("Added request:", (po.request_id._name, po.request_id.id))
else:
print("NO request_id")
# Requisition ID - آمن
if hasattr(po,
'requisition_id') and po.requisition_id and po.requisition_id._name == 'purchase.requisition':
related_pairs.add(('purchase.requisition', po.requisition_id.id))
print("Added requisition:", ('purchase.requisition', po.requisition_id.id))
else:
print("NO requisition_id")
# Confirmation IDs - آمن
if hasattr(po, 'confirmation_ids') and po.confirmation_ids:
for conf in po.confirmation_ids:
related_pairs.add(('budget.confirmation', conf.id))
print("Added confirmation:", ('budget.confirmation', conf.id))
else:
print("NO confirmation_ids")
print("Record related_pairs:", related_pairs)
all_models_ids.update(related_pairs)
# Build domain...
domain = []
pairs = list(all_models_ids)
if pairs:
domain = ['|'] * (len(pairs) - 1)
for model, res_id in pairs:
domain.extend(['&', ('res_model', '=', model), ('res_id', '=', res_id)])
action['domain'] = domain
action['context'] = {'default_res_model': self[0]._name, 'default_res_id': self[0].id}
for record in self:
record.attach_no = Attachment.search_count(
[('res_model', '=', record._name), ('res_id', '=', record.id)])
print("=== MULTIPLE END ===")
return action
# Single record - تلقائي
self.ensure_one()
print("SINGLE RECORD ID:", self.id)
print("invoice_origin:", self.invoice_origin, "ref:", self.ref)
related_pairs = [(self._name, self.id)]
# PO تلقائي
po_id = self.purchase_id.id if self.purchase_id else False
if not po_id and self.invoice_origin:
po = self.env['purchase.order'].search([('name', '=', self.invoice_origin)], limit=1)
if po: po_id = po.id
elif not po_id and self.ref:
po = self.env['purchase.order'].search([('name', '=', self.ref)], limit=1)
if po: po_id = po.id
if po_id:
po = self.env['purchase.order'].browse(po_id)
related_pairs.append((po._name, po.id))
print("Auto-linked PO:", po_id)
if hasattr(po, 'request_id') and po.request_id:
related_pairs.append((po.request_id._name, po.request_id.id))
if hasattr(po,
'requisition_id') and po.requisition_id and po.requisition_id._name == 'purchase.requisition':
related_pairs.append(('purchase.requisition', po.requisition_id.id))
if hasattr(po, 'confirmation_ids') and po.confirmation_ids:
for conf in po.confirmation_ids:
related_pairs.append(('budget.confirmation', conf.id))
# Domain
domain = []
if len(related_pairs) > 1:
domain = ['|'] * (len(related_pairs) - 1)
for model, res_id in related_pairs:
domain.extend(['&', ('res_model', '=', model), ('res_id', '=', res_id)])
else:
domain = [('res_model', '=', self._name), ('res_id', '=', self.id)]
action['domain'] = domain
action['context'] = {'default_res_model': self._name, 'default_res_id': self.id}
self.attach_no = Attachment.search_count(domain)
print("attach_no:", self.attach_no, "Domain covers:", related_pairs)
print("=== END ===")
return action

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -0,0 +1,22 @@
<odoo>
<record id="account_move_view" model="ir.ui.view">
<field name="name">account.move.inherit.form.attachment</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<button name="get_attachments" type="object"
class="oe_stat_button"
icon="fa-file-text-o">
<field name="attach_no" widget="statinfo" string="Documents"/>
</button>
</xpath>
<xpath expr="//page[@name='other_info']" position="inside">
<group invisible="1" name="model_info" string='Model Info'>
<field invisible="1" readonly="1" name='res_model'/>
<field invisible="1" readonly="1" name='res_id'/>
</group>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,2 @@
from . import models

View File

@ -0,0 +1,17 @@
{
'name': 'Fleet bridge',
'category': 'Odex30-Accounting/Odex30-Accounting',
'author': "Expert Co. Ltd.",
'website': "http://www.exp-sa.com",
'summary': 'Manage accounting with fleet features',
'version': '1.0',
'depends': ['account_fleet', 'odex30_account_accountant'],
'assets': {
'web.assets_backend': [
'odex30_account_accountant_fleet/static/src/components/**/*',
],
},
'license': 'OEEL-1',
'auto_install': True,
}

View File

@ -0,0 +1,49 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * odex30_account_accountant_fleet
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-05 17:12+0000\n"
"PO-Revision-Date: 2026-01-05 17:12+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: odex30_account_accountant_fleet
#: model:ir.model,name:odex30_account_accountant_fleet.model_bank_rec_widget
msgid "Bank reconciliation widget for a single statement line"
msgstr "أداة التسوية البنكية لبند كشف حساب واحد "
#. module: odex30_account_accountant_fleet
#: model:ir.model,name:odex30_account_accountant_fleet.model_account_move_line
msgid "Journal Item"
msgstr "عنصر اليومية"
#. module: odex30_account_accountant_fleet
#: model:ir.model,name:odex30_account_accountant_fleet.model_bank_rec_widget_line
msgid "Line of the bank reconciliation widget"
msgstr "بند أداة التسوية البنكية "
#. module: odex30_account_accountant_fleet
#: model:ir.model,name:odex30_account_accountant_fleet.model_account_tax
msgid "Tax"
msgstr "الضريبة"
#. module: odex30_account_accountant_fleet
#. odoo-javascript
#: code:addons/odex30_account_accountant_fleet/static/src/components/bank_reconciliation/bank_rec_form.xml:0
#: code:addons/odex30_account_accountant_fleet/static/src/components/bank_reconciliation/kanban.js:0
#: model:ir.model.fields,field_description:odex30_account_accountant_fleet.field_bank_rec_widget_line__vehicle_id
msgid "Vehicle"
msgstr ""
#. module: odex30_account_accountant_fleet
#: model:ir.model.fields,field_description:odex30_account_accountant_fleet.field_bank_rec_widget_line__vehicle_required
msgid "Vehicle Required"
msgstr ""

View File

@ -0,0 +1,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_move_line
from . import account_tax
from . import bank_rec_widget
from . import bank_rec_widget_line

View File

@ -0,0 +1,13 @@
from odoo import api, models
from odoo.tools import SQL
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
@api.model
def _get_extra_query_base_tax_line_mapping(self) -> SQL:
query = super()._get_extra_query_base_tax_line_mapping()
return SQL("%s AND COALESCE(base_line.vehicle_id, 0) = COALESCE(account_move_line.vehicle_id, 0)", query)

View File

@ -0,0 +1,30 @@
from odoo import models
class AccountTax(models.Model):
_inherit = 'account.tax'
def _prepare_base_line_for_taxes_computation(self, record, **kwargs):
results = super()._prepare_base_line_for_taxes_computation(record, **kwargs)
results['vehicle_id'] = self._get_base_line_field_value_from_record(record, 'vehicle_id', kwargs, self.env['fleet.vehicle'])
return results
def _prepare_tax_line_for_taxes_computation(self, record, **kwargs):
results = super()._prepare_tax_line_for_taxes_computation(record, **kwargs)
results['vehicle_id'] = self._get_base_line_field_value_from_record(record, 'vehicle_id', kwargs, self.env['fleet.vehicle'])
return results
def _prepare_base_line_grouping_key(self, base_line):
results = super()._prepare_base_line_grouping_key(base_line)
results['vehicle_id'] = base_line['vehicle_id'].id
return results
def _prepare_base_line_tax_repartition_grouping_key(self, base_line, base_line_grouping_key, tax_data, tax_rep_data):
results = super()._prepare_base_line_tax_repartition_grouping_key(base_line, base_line_grouping_key, tax_data, tax_rep_data)
results['vehicle_id'] = base_line_grouping_key['vehicle_id'] if not tax_rep_data['tax_rep'].use_in_tax_closing else False
return results
def _prepare_tax_line_repartition_grouping_key(self, tax_line):
results = super()._prepare_tax_line_repartition_grouping_key(tax_line)
results['vehicle_id'] = tax_line['vehicle_id'].id
return results

View File

@ -0,0 +1,18 @@
from odoo import models
class BankRecWidget(models.Model):
_inherit = 'bank.rec.widget'
def _lines_prepare_tax_line(self, tax_line_vals):
results = super()._lines_prepare_tax_line(tax_line_vals)
results['vehicle_id'] = tax_line_vals['vehicle_id']
return results
def _line_value_changed_vehicle_id(self, line):
self.ensure_one()
self._lines_turn_auto_balance_into_manual_line(line)
if line.flag != 'tax_line':
self._lines_recompute_taxes()

View File

@ -0,0 +1,34 @@
from odoo import api, fields, models
class BankRecWidgetLine(models.Model):
_inherit = 'bank.rec.widget.line'
vehicle_id = fields.Many2one(
comodel_name='fleet.vehicle',
compute='_compute_vehicle_id',
store=True,
readonly=False,
domain="[('company_id', '=', company_id)]"
)
vehicle_required = fields.Boolean(
compute='_compute_vehicle_required',
)
@api.depends('source_aml_id')
def _compute_vehicle_id(self):
for line in self:
if line.flag == 'aml':
line.vehicle_id = line.source_aml_id.vehicle_id
else:
line.vehicle_id = line.vehicle_id
def _compute_vehicle_required(self):
self.vehicle_required = False
def _get_aml_values(self, **kwargs):
return super()._get_aml_values(
**kwargs,
vehicle_id=self.vehicle_id.id,
)

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="odex30_account_accountant_fleet.BankRecRecordFormLineIds" t-inherit="account_accountant.BankRecRecordFormLineIds" t-inherit-mode="extension">
<xpath expr="//t[@name='col_taxes']" position="after">
<t t-if="column[0] === 'vehicle'" name="col_vehicle">
<td class="o_data_cell o_field_cell o_field_widget o_list_many2one"
t-att-class="{'o_invalid_cell': invalidFields.includes('vehicle')}"
field="vehicle_id"
t-att-title="line.data.vehicle_id[1]"
t-out="line.data.vehicle_id[1]"/>
</t>
</xpath>
</t>
<t t-name="odex30_account_accountant_fleet.BankRecRecordNotebookManualOperations" t-inherit="account_accountant.BankRecRecordNotebookManualOperations" t-inherit-mode="extension">
<xpath expr="//div[@name='suggestion']" position="before">
<div name="vehicle"
t-if="!['liquidity', 'new_batch'].includes(line.data.flag)"
class="o_wrap_field d-flex d-sm-contents flex-column mb-3 mb-sm-0">
<div class="o_cell o_wrap_label flex-grow-1 flex-sm-grow-0 w-100 text-break text-900">
<label class="o_form_label"
t-att-class="{'o_field_invalid': invalidFields.includes('vehicle'), 'o_form_label_readonly': reconciled}">Vehicle</label>
</div>
<div class="o_cell o_wrap_input flex-grow-1 flex-sm-grow-0 w-100 mb-1">
<div class="o_field_widget o_field_many2one"
t-att-class="{'o_field_invalid': invalidFields.includes('vehicle')}">
<Many2OneField
name="'vehicle_id'"
record="line"
canOpen="false"
readonly="reconciled"
canQuickCreate="false"
/>
</div>
</div>
</div>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,19 @@
/** @odoo-module **/
import { _t } from "@web/core/l10n/translation";
import { patch } from "@web/core/utils/patch";
import { BankRecKanbanController } from "@odex30_account_accountant/components/bank_reconciliation/kanban";
patch(BankRecKanbanController.prototype, {
getOne2ManyColumns() {
const columns = super.getOne2ManyColumns(...arguments);
const lineIdsRecords = this.state.bankRecRecordData.line_ids.records;
if (lineIdsRecords.some((r) => r.data.vehicle_id || r.data.vehicle_required)) {
const debit_col_index = columns.findIndex((col) => col[0] === "debit");
columns.splice(debit_col_index, 0, ["vehicle", _t("Vehicle")]);
}
return columns;
}
});

View File

@ -0,0 +1,2 @@
from . import test_account_fleet_tax_report

View File

@ -0,0 +1,62 @@
from odoo import Command
from odoo.addons.account.tests.test_account_move_line_tax_details import TestAccountTaxDetailsReport
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestAccountFleet(TestAccountTaxDetailsReport):
def test_tax_report_with_vehicle_split_repartition(self):
self.env.user.groups_id += self.env.ref('fleet.fleet_group_manager')
brand = self.env["fleet.vehicle.model.brand"].create({"name": "Audi"})
model = self.env["fleet.vehicle.model"].create({"brand_id": brand.id, "name": "A3"})
cars = self.env["fleet.vehicle"].create([
{"model_id": model.id, "plan_to_change_car": False},
{"model_id": model.id, "plan_to_change_car": False},
])
expense_account = self.company_data['default_account_expense']
asset_account = self.company_data['default_account_deferred_expense']
tax = self.env['account.tax'].create({
'name': 'Split Tax',
'amount': 10,
'invoice_repartition_line_ids': [
Command.create({'repartition_type': 'base', 'factor_percent': 100}),
Command.create({'repartition_type': 'tax', 'factor_percent': 50, 'account_id': expense_account.id}),
Command.create({'repartition_type': 'tax', 'factor_percent': 50, 'account_id': asset_account.id}),
],
'refund_repartition_line_ids': [
Command.create({'repartition_type': 'base', 'factor_percent': 100}),
Command.create({'repartition_type': 'tax', 'factor_percent': 50, 'account_id': expense_account.id}),
Command.create({'repartition_type': 'tax', 'factor_percent': 50, 'account_id': asset_account.id}),
],
})
bill = self.init_invoice('in_invoice', invoice_date='2025-10-16', post=False)
bill.write({
'invoice_line_ids': [
Command.create({
'product_id': self.product_a.id,
'account_id': expense_account.id,
'price_unit': 100,
'tax_ids': [Command.set(tax.ids)],
'vehicle_id': cars[0].id
}),
Command.create({
'product_id': self.product_a.id,
'account_id': expense_account.id,
'price_unit': 100,
'tax_ids': [Command.set(tax.ids)],
'vehicle_id': cars[1].id
}),
]
})
bill.action_post()
tax_details = self._get_tax_details()
self.assertEqual(len(tax_details), 2)
for line in tax_details:
self.assertEqual(line['tax_amount'], 5)

View File

@ -0,0 +1,3 @@
from . import models
from . import report

View File

@ -0,0 +1,27 @@
{
'name': 'Disallowed Expenses',
'category': 'Odex23-Accounting/Odex23-Accounting',
'author': "Expert Co. Ltd.",
'website': "http://www.exp-sa.com",
'summary': 'Manage disallowed expenses',
'description': 'Manage disallowed expenses',
'version': '1.0',
'depends': ['odex30_account_reports'],
'data': [
'data/odex30_account_disallowed_expenses_report.xml',
'security/ir.model.access.csv',
'security/odex30_account_disallowed_expenses_security.xml',
'views/odex30_account_account_views.xml',
'views/odex30_account_disallowed_expenses_category_views.xml',
'views/odex30_account_disallowed_expenses_report_views.xml',
],
'installable': True,
'auto_install': True,
'license': 'OEEL-1',
'assets': {
'web.assets_backend': [
'odex30_account_disallowed_expenses/static/src/components/**/*',
],
},
}

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="disallowed_expenses_report" model="account.report">
<field name="name">Disallowed Expenses Report</field>
<field name="filter_multi_company">selector</field>
<field name="filter_journals" eval="True"/>
<field name="filter_unfold_all" eval="True"/>
<field name="default_opening_date_filter">previous_year</field>
<field name="search_bar" eval="True"/>
<field name="custom_handler_model_id" ref="model_account_disallowed_expenses_report_handler"/>
<field name="column_ids">
<record id="disallowed_expenses_report_total_amount" model="account.report.column">
<field name="name">Total Amount</field>
<field name="expression_label">total_amount</field>
</record>
<record id="disallowed_expenses_report_rate" model="account.report.column">
<field name="name">Rate</field>
<field name="expression_label">rate</field>
<field name="figure_type">percentage</field>
</record>
<record id="disallowed_expenses_report_disallowed_amount" model="account.report.column">
<field name="name">Disallowed Amount</field>
<field name="expression_label">disallowed_amount</field>
</record>
</field>
</record>
</odoo>

View File

@ -0,0 +1,227 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * odex30_account_disallowed_expenses
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-05 16:46+0000\n"
"PO-Revision-Date: 2026-01-05 16:46+00:00\n"
"Last-Translator: Expert SA\n"
"Language-Team: Arabic Team\n"
"Language: ar\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=6; plural=n<3?n%10==1?0:n%10>=3?1:n%10==2?2:n==0?3:n==1?4:5:3;\n"
#. module: odex30_account_disallowed_expenses
#: model:ir.model,name:odex30_account_disallowed_expenses.model_account_account
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_disallowed_expenses_category__account_ids
msgid "Account"
msgstr "الحساب"
#. module: odex30_account_disallowed_expenses
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_disallowed_expenses_category__active
msgid "Active"
msgstr "نشط"
#. module: odex30_account_disallowed_expenses
#: model_terms:ir.actions.act_window,help:odex30_account_disallowed_expenses.action_odex30_account_disallowed_expenses_category_list
msgid "Add a Disallowed Expenses Category"
msgstr "إضافة فئة مصروفات ممنوعة"
#. module: odex30_account_disallowed_expenses
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_disallowed_expenses_rate__category_id
msgid "Category"
msgstr "الفئة"
#. module: odex30_account_disallowed_expenses
#: model_terms:ir.ui.view,arch_db:odex30_account_disallowed_expenses.view_odex30_account_disallowed_expenses_category_form
msgid "Category Name"
msgstr "اسم الفئة"
#. module: odex30_account_disallowed_expenses
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_disallowed_expenses_category__code
#: model_terms:ir.ui.view,arch_db:odex30_account_disallowed_expenses.view_odex30_account_disallowed_expenses_category_form
msgid "Code"
msgstr "الرمز"
#. module: odex30_account_disallowed_expenses
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_disallowed_expenses_category__company_id
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_disallowed_expenses_rate__company_id
msgid "Company"
msgstr "الشركة"
#. module: odex30_account_disallowed_expenses
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_disallowed_expenses_category__create_uid
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_disallowed_expenses_rate__create_uid
msgid "Created by"
msgstr "أُنشئ بواسطة"
#. module: odex30_account_disallowed_expenses
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_disallowed_expenses_category__create_date
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_disallowed_expenses_rate__create_date
msgid "Created on"
msgstr "تاريخ الإنشاء"
#. module: odex30_account_disallowed_expenses
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_disallowed_expenses_category__current_rate
msgid "Current Rate"
msgstr "المعدل الحالي"
#. module: odex30_account_disallowed_expenses
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_disallowed_expenses_rate__rate
msgid "Disallowed %"
msgstr "نسبة الممنوع %"
#. module: odex30_account_disallowed_expenses
#: model:account.report.column,name:odex30_account_disallowed_expenses.disallowed_expenses_report_disallowed_amount
msgid "Disallowed Amount"
msgstr "المبلغ الممنوع"
#. module: odex30_account_disallowed_expenses
#: model:ir.ui.menu,name:odex30_account_disallowed_expenses.menu_action_account_report_de
#: model_terms:ir.ui.view,arch_db:odex30_account_disallowed_expenses.view_account_form
msgid "Disallowed Expenses"
msgstr "المصروفات الممنوعة"
#. module: odex30_account_disallowed_expenses
#: model:ir.actions.act_window,name:odex30_account_disallowed_expenses.action_odex30_account_disallowed_expenses_category_list
#: model:ir.ui.menu,name:odex30_account_disallowed_expenses.menu_action_odex30_account_disallowed_expenses_category_list
msgid "Disallowed Expenses Categories"
msgstr "فئات المصروفات الممنوعة"
#. module: odex30_account_disallowed_expenses
#: model:ir.model,name:odex30_account_disallowed_expenses.model_account_disallowed_expenses_category
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_account__disallowed_expenses_category_id
msgid "Disallowed Expenses Category"
msgstr "فئة المصروفات الممنوعة"
#. module: odex30_account_disallowed_expenses
#: model:ir.model,name:odex30_account_disallowed_expenses.model_account_disallowed_expenses_report_handler
msgid "Disallowed Expenses Custom Handler"
msgstr "معالج مخصص للمصروفات الممنوعة"
#. module: odex30_account_disallowed_expenses
#: model:ir.model,name:odex30_account_disallowed_expenses.model_account_disallowed_expenses_rate
msgid "Disallowed Expenses Rate"
msgstr "معدل المصروفات الممنوعة"
#. module: odex30_account_disallowed_expenses
#: model:account.report,name:odex30_account_disallowed_expenses.disallowed_expenses_report
#: model:ir.actions.client,name:odex30_account_disallowed_expenses.action_account_report_de
msgid "Disallowed Expenses Report"
msgstr "تقرير المصروفات الممنوعة"
#. module: odex30_account_disallowed_expenses
#: model:ir.model.constraint,message:odex30_account_disallowed_expenses.constraint_account_disallowed_expenses_category_unique_code
msgid "Disallowed expenses category code should be unique."
msgstr "رمز فئة المصروفات الممنوعة يجب أن يكون فريداً."
#. module: odex30_account_disallowed_expenses
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_disallowed_expenses_category__display_name
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_disallowed_expenses_rate__display_name
msgid "Display Name"
msgstr "اسم العرض"
#. module: odex30_account_disallowed_expenses
#. odoo-python
#: code:addons/odex30_account_disallowed_expenses/report/odex30_account_disallowed_expenses_report.py:0
msgid "General Ledger"
msgstr "الدفتر الرئيسي"
#. module: odex30_account_disallowed_expenses
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_disallowed_expenses_category__id
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_disallowed_expenses_rate__id
msgid "ID"
msgstr "المعرف"
#. module: odex30_account_disallowed_expenses
#. odoo-python
#: code:addons/odex30_account_disallowed_expenses/report/odex30_account_disallowed_expenses_report.py:0
msgid "Journal Items"
msgstr "عناصر اليومية"
#. module: odex30_account_disallowed_expenses
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_disallowed_expenses_category__write_uid
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_disallowed_expenses_rate__write_uid
msgid "Last Updated by"
msgstr "آخر تحديث بواسطة"
#. module: odex30_account_disallowed_expenses
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_disallowed_expenses_category__write_date
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_disallowed_expenses_rate__write_date
msgid "Last Updated on"
msgstr "تاريخ آخر تحديث"
#. module: odex30_account_disallowed_expenses
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_disallowed_expenses_category__name
msgid "Name"
msgstr "الاسم"
#. module: odex30_account_disallowed_expenses
#. odoo-python
#: code:addons/odex30_account_disallowed_expenses/models/odex30_account_disallowed_expenses.py:0
msgid "No Rate"
msgstr "لا يوجد معدل"
#. module: odex30_account_disallowed_expenses
#: model:account.report.column,name:odex30_account_disallowed_expenses.disallowed_expenses_report_rate
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_disallowed_expenses_category__rate_ids
msgid "Rate"
msgstr "المعدل"
#. module: odex30_account_disallowed_expenses
#: model_terms:ir.ui.view,arch_db:odex30_account_disallowed_expenses.view_odex30_account_disallowed_expenses_category_form
msgid "Rates"
msgstr "المعدلات"
#. module: odex30_account_disallowed_expenses
#: model_terms:ir.ui.view,arch_db:odex30_account_disallowed_expenses.view_odex30_account_disallowed_expenses_category_form
#: model_terms:ir.ui.view,arch_db:odex30_account_disallowed_expenses.view_odex30_account_disallowed_expenses_category_tree
msgid "Related Account(s)"
msgstr "الحسابات المرتبطة"
#. module: odex30_account_disallowed_expenses
#: model_terms:ir.ui.view,arch_db:odex30_account_disallowed_expenses.view_odex30_account_disallowed_expenses_category_tree
msgid "Set Rates"
msgstr "تعيين المعدلات"
#. module: odex30_account_disallowed_expenses
#: model:ir.model.fields,help:odex30_account_disallowed_expenses.field_account_disallowed_expenses_category__active
msgid "Set active to false to hide the category without removing it."
msgstr "اضبط النشط على كاذب لإخفاء الفئة دون حذفها."
#. module: odex30_account_disallowed_expenses
#: model:ir.model.fields,field_description:odex30_account_disallowed_expenses.field_account_disallowed_expenses_rate__date_from
msgid "Start Date"
msgstr "تاريخ البدء"
#. module: odex30_account_disallowed_expenses
#. odoo-javascript
#: code:addons/odex30_account_disallowed_expenses/static/src/components/disallowed_expenses_report/warnings.xml:0
msgid "There are multiple disallowed expenses rates in this period"
msgstr "هناك معدلات متعددة للمصروفات الممنوعة في هذه الفترة"
#. module: odex30_account_disallowed_expenses
#. odoo-python
#: code:addons/odex30_account_disallowed_expenses/report/odex30_account_disallowed_expenses_report.py:0
msgid "Total"
msgstr "الإجمالي"
#. module: odex30_account_disallowed_expenses
#: model:account.report.column,name:odex30_account_disallowed_expenses.disallowed_expenses_report_total_amount
msgid "Total Amount"
msgstr "المبلغ الإجمالي"
#. module: odex30_account_disallowed_expenses
#: model_terms:ir.ui.view,arch_db:odex30_account_disallowed_expenses.view_odex30_account_disallowed_expenses_category_form
msgid "e.g. 1201"
msgstr "مثال: 1201"
#. module: odex30_account_disallowed_expenses
#: model_terms:ir.ui.view,arch_db:odex30_account_disallowed_expenses.view_odex30_account_disallowed_expenses_category_form
msgid "e.g. Non-Deductible Tax"
msgstr "مثال: ضريبة غير قابلة للخصم"

View File

@ -0,0 +1,3 @@
from . import odex30_account_account
from . import odex30_account_disallowed_expenses

View File

@ -0,0 +1,13 @@
from odoo import models, fields, api
class AccountAccount(models.Model):
_inherit = "account.account"
disallowed_expenses_category_id = fields.Many2one('account.disallowed.expenses.category', string='Disallowed Expenses Category', check_company=True)
@api.onchange('internal_group')
def _onchange_internal_group(self):
if self.internal_group not in ('income', 'expense'):
self.disallowed_expenses_category_id = None

View File

@ -0,0 +1,78 @@
from odoo import fields, models, api, _
from odoo.osv import expression
class AccountDisallowedExpensesCategory(models.Model):
_name = 'account.disallowed.expenses.category'
_description = "Disallowed Expenses Category"
name = fields.Char(string='Name', required=True, translate=True)
code = fields.Char(string='Code', required=True)
active = fields.Boolean(default=True, help="Set active to false to hide the category without removing it.")
rate_ids = fields.One2many('account.disallowed.expenses.rate', 'category_id', string='Rate')
company_id = fields.Many2one('res.company')
account_ids = fields.One2many('account.account', 'disallowed_expenses_category_id', check_company=True)
current_rate = fields.Char(compute='_compute_current_rate', string='Current Rate')
_sql_constraints = [
('unique_code', 'UNIQUE(code)', 'Disallowed expenses category code should be unique.')
]
@api.depends('current_rate', 'code')
def _compute_display_name(self):
for record in self:
rate = record.current_rate or _('No Rate')
name = f'{record.code} - {record.name} ({rate})'
record.display_name = name
@api.depends('rate_ids')
def _compute_current_rate(self):
rates = self._get_current_rates()
for rec in self:
rec.current_rate = ('%g%%' % rates[rec.id]) if rates.get(rec.id) else None
def _get_current_rates(self):
sql = """
SELECT
DISTINCT category_id,
first_value(rate) OVER (PARTITION BY category_id ORDER BY date_from DESC)
FROM account_disallowed_expenses_rate
WHERE date_from < CURRENT_DATE
AND category_id IN %(ids)s
"""
self.env.cr.execute(sql, {'ids': tuple(self.ids)})
return dict(self.env.cr.fetchall())
@api.model
def _search_display_name(self, operator, value):
if value and isinstance(value, str):
code_value = value.split(' ')[0]
is_negative = operator in expression.NEGATIVE_TERM_OPERATORS
positive_operator = expression.TERM_OPERATORS_NEGATION[operator] if is_negative else operator
domain = ['|', ('code', '=ilike', f'{code_value}%'), ('name', positive_operator, value)]
if is_negative:
domain = ['!', *domain]
return domain
return super()._search_display_name(operator, value)
def action_read_category(self):
self.ensure_one()
return {
'name': self.display_name,
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'account.disallowed.expenses.category',
'res_id': self.id,
}
class AccountDisallowedExpensesRate(models.Model):
_name = 'account.disallowed.expenses.rate'
_description = "Disallowed Expenses Rate"
_order = 'date_from desc'
rate = fields.Float(string='Disallowed %', required=True)
date_from = fields.Date(string='Start Date', required=True)
category_id = fields.Many2one('account.disallowed.expenses.category', string='Category', required=True, ondelete='cascade')
company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company)

View File

@ -0,0 +1,2 @@
from . import odex30_account_disallowed_expenses_report

View File

@ -0,0 +1,356 @@
from odoo import models, _
from odoo.tools import SQL, Query
class DisallowedExpensesCustomHandler(models.AbstractModel):
_name = 'account.disallowed.expenses.report.handler'
_inherit = 'account.report.custom.handler'
_description = 'Disallowed Expenses Custom Handler'
def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None):
results = self._get_query_results(options, primary_fields=['category_id'])
lines = []
totals = {
column_group_key: {key: 0.0 for key in ['total_amount', 'disallowed_amount']}
for column_group_key in options['column_groups']
}
for group_key, result in results.items():
current = self._parse_hierarchy_group_key(group_key)
lines.append((0, self._get_category_line(options, result, current, len(current))))
self._update_total_values(totals, options, result)
if (lines):
lines.append((0, self._get_total_line(report, options, totals)))
return lines
def _custom_options_initializer(self, report, options, previous_options):
super()._custom_options_initializer(report, options, previous_options=previous_options)
period_domain = [('date_from', '>=', options['date']['date_from']), ('date_from', '<=', options['date']['date_to'])]
rg = self.env['account.disallowed.expenses.rate']._read_group(
period_domain,
['category_id'],
having=[('__count', '>', 1)],
limit=1,
)
options['multi_rate_in_period'] = bool(rg)
def _customize_warnings(self, report, options, all_column_groups_expression_totals, warnings):
if options['multi_rate_in_period']:
warnings['odex30_account_disallowed_expenses.warning_multi_rate'] = {}
def _caret_options_initializer(self):
return {
'account.account': [
{'name': _("General Ledger"), 'action': 'caret_option_open_general_ledger'},
{'name': _("Journal Items"), 'action': 'open_journal_items'},
],
}
def open_journal_items(self, options, params):
ctx = {
'search_default_group_by_account': 1,
'search_default_posted': 0 if options.get('all_entries') else 1,
'date_from': options.get('date', {}).get('date_from'),
'date_to': options.get('date', {}).get('date_to'),
'expand': 1,
}
if options.get('date', {}).get('date_from'):
ctx['search_default_date_between'] = 1
else:
ctx['search_default_date_before'] = 1
domain = [('display_type', 'not in', ('line_section', 'line_note'))]
model_to_domain = {
'account.disallowed.expenses.category': 'account_id.disallowed_expenses_category_id',
'account.account': 'account_id',
'fleet.vehicle': 'vehicle_id',
}
vehicle_audit = False
account_audit = False
for markup, res_model, res_id in self.env['account.report']._parse_line_id(params.get('line_id')):
if model_to_domain.get(res_model):
domain.append((model_to_domain[res_model], '=', res_id))
if markup:
ctx['search_default_account_id'] = int(markup)
if res_model == 'fleet.vehicle':
vehicle_audit = True
if res_model == 'account.account':
account_audit = True
if options.get('vehicle_split') and account_audit and not vehicle_audit:
domain.append(('vehicle_id', '=', False))
return {
'name': 'Journal Items',
'view_mode': 'list',
'res_model': 'account.move.line',
'views': [(False, 'list')],
'type': 'ir.actions.act_window',
'domain': domain,
'context': ctx,
}
def _get_query(self, options, line_dict_id=None) -> tuple[SQL, SQL, SQL, SQL, SQL, SQL]:
company_ids = tuple(self.env['account.report'].get_report_company_ids(options))
current = self._parse_line_id(options, line_dict_id)
category_name = self.env['account.disallowed.expenses.category']._field_to_sql('category', 'name')
query = Query(self.env, alias='aml', table=SQL.identifier('account_move_line'))
query.add_join('LEFT JOIN', alias='account', table='account_account', condition=SQL('aml.account_id = account.id'))
account_code = self.env['account.account']._field_to_sql('account', 'code', query)
select = SQL(
"""
SELECT
SUM(aml.balance) AS total_amount,
ARRAY_AGG(%(account_name)s) account_name,
ARRAY_AGG(%(account_code)s) account_code,
ARRAY_AGG(category.id) category_id,
ARRAY_AGG(%(category_name)s) category_name,
ARRAY_AGG(category.code) category_code,
ARRAY_AGG(aml.company_id) company_id,
ARRAY_AGG(aml.account_id) account_id,
ARRAY_AGG(rate.rate) account_rate,
SUM(aml.balance * rate.rate) / 100 AS account_disallowed_amount
""",
account_name=self.env['account.account']._field_to_sql('account', 'name'),
account_code=account_code,
category_name=category_name,
)
from_ = SQL(
"""
FROM %(from_clause)s
JOIN account_move move ON aml.move_id = move.id
JOIN account_disallowed_expenses_category category ON account.disallowed_expenses_category_id = category.id
LEFT JOIN account_disallowed_expenses_rate rate ON rate.id = (
SELECT r2.id FROM account_disallowed_expenses_rate r2
LEFT JOIN account_disallowed_expenses_category c2 ON r2.category_id = c2.id
WHERE r2.date_from <= aml.date
AND c2.id = category.id
ORDER BY r2.date_from DESC LIMIT 1
)
""",
from_clause=query.from_clause,
)
where = SQL(
"""
WHERE aml.company_id in %(company_ids)s
AND aml.date >= %(date_from)s AND aml.date <= %(date_to)s
AND move.state != 'cancel'
%(category_condition)s
%(account_condition)s
%(account_rate_condition)s
%(not_all_entries_condition)s
""",
company_ids=company_ids,
date_from=options['date']['date_from'],
date_to=options['date']['date_to'],
category_condition=SQL("AND category.id = %s", current['category_id']) if current.get('category_id') else SQL(),
account_condition=SQL("AND aml.account_id = %s", current['account_id']) if current.get('account_id') else SQL(),
account_rate_condition=SQL("AND rate.rate = %s", current['account_rate']) if current.get('account_rate') else SQL(),
not_all_entries_condition=SQL("AND move.state = 'posted'") if not options.get('all_entries') else SQL(),
)
group_by = SQL(
"""GROUP BY category.id %s%s""",
current.get('category_id') and SQL(", account_id") or SQL(),
current.get('account_id') and options['multi_rate_in_period'] and SQL(", rate.rate") or SQL(),
)
order_by = SQL("ORDER BY category_id, account_id")
order_by_rate = SQL(", account_rate")
return select, from_, where, group_by, order_by, order_by_rate
def _parse_line_id(self, options, line_id):
current = {'category_id': None}
if not line_id:
return current
for dummy, model, record_id in self.env['account.report']._parse_line_id(line_id):
if model == 'account.disallowed.expenses.category':
current['category_id'] = record_id
if model == 'account.account':
current['account_id'] = record_id
if model == 'account.disallowed.expenses.rate':
current['account_rate'] = record_id
return current
def _build_line_id(self, options, current, level, parent=False, markup=None):
report = self.env['account.report'].browse(options['report_id'])
parent_line_id = None
line_id = report._get_generic_line_id('account.disallowed.expenses.category', current['category_id'])
if current.get('account_id'):
parent_line_id = line_id
line_id = report._get_generic_line_id('account.account', current['account_id'], parent_line_id=line_id)
if len(current) != level and not current.get('account_rate'):
parent_line_id = line_id
line_id = report._get_generic_line_id('account.account', current['account_id'], parent_line_id=line_id)
if current.get('account_rate'):
parent_line_id = line_id
line_id = report._get_generic_line_id('account.disallowed.expenses.rate', current['account_rate'], markup=markup, parent_line_id=line_id)
return parent_line_id if parent else line_id
def _get_query_results(self, options, line_dict_id=None, primary_fields=None, secondary_fields=None, selector=None):
grouped_results = {}
for column_group_key, column_group_options in self.env['account.report']._split_options_per_column_group(options).items():
select, from_, where, group_by, order_by, order_by_rate = self._get_query(column_group_options, line_dict_id)
select = SQL("%s, %s AS column_group_key", select, column_group_key)
self.env.cr.execute(SQL(' ').join([select, from_, where, group_by, order_by, order_by_rate]))
for results in self.env.cr.dictfetchall():
key = self._get_group_key(results, primary_fields, secondary_fields, selector)
grouped_results.setdefault(key, {})[column_group_key] = results
return grouped_results
def _get_group_key(self, results, primary_fields, secondary_fields, selector):
fields = []
if selector is None or self._get_single_value(results, selector):
fields = primary_fields
elif secondary_fields is not None:
fields = secondary_fields
group_key_list = []
for group_key in fields:
group_key_id = self._get_single_value(results, group_key)
if group_key_id:
group_key_list.append(group_key + '~' + (group_key_id and str(group_key_id) or ''))
return '|'.join(group_key_list)
def _parse_hierarchy_group_key(self, group_key):
return {
item: int(float(item_id))
for item, item_id
in [
full_id.split('~')
for full_id
in (group_key.split('|'))
]
}
def _report_expand_unfoldable_line_category_line(self, line_dict_id, groupby, options, progress, offset, unfold_all_batch_data=None):
results = self._get_query_results(options, line_dict_id, ['category_id', 'account_id'])
lines = []
for group_key, result in results.items():
current = self._parse_hierarchy_group_key(group_key)
level = len(self._parse_line_id(options, line_dict_id)) + 1
lines.append(self._get_account_line(options, result, current, level))
return {'lines': lines}
def _report_expand_unfoldable_line_account_line(self, line_dict_id, groupby, options, progress, offset, unfold_all_batch_data=None):
results = self._get_query_results(options, line_dict_id, ['category_id', 'account_id', 'account_rate'])
lines = []
for group_key, result in results.items():
current = self._parse_hierarchy_group_key(group_key)
level = len(self._parse_line_id(options, line_dict_id)) + 1
base_line_values = list(result.values())[0]
account_id = self._get_single_value(base_line_values, 'account_id')
lines.append(self._get_rate_line(options, result, current, level, account_id))
return {'lines': lines}
def _get_column_values(self, options, values, is_total_line=False):
column_values = []
report = self.env['account.report'].browse(options['report_id'])
for column in options['columns']:
vals = values.get(column['column_group_key'], {})
if vals and not is_total_line:
vals['rate'] = self._get_current_rate(vals)
vals['disallowed_amount'] = self._get_current_disallowed_amount(vals)
col_val = vals.get(column['expression_label'])
column_values.append(report._build_column_dict(
col_val,
column,
options=options,
digits=2 if column['figure_type'] == 'percentage' else None,
))
return column_values
def _update_total_values(self, total, options, values):
for column_group_key in options['column_groups']:
for key in total[column_group_key]:
total[column_group_key][key] += values.get(column_group_key, {}).get(key) or 0.0
def _get_total_line(self, report, options, totals):
return {
'id': report._get_generic_line_id(None, None, markup='total'),
'name': _('Total'),
'level': 1,
'columns': self._get_column_values(options, totals, is_total_line=True),
}
def _get_category_line(self, options, values, current, level):
base_line_values = list(values.values())[0]
return {
**self._get_base_line(options, current, level),
'name': '%s %s' % (base_line_values['category_code'][0], base_line_values['category_name'][0]),
'columns': self._get_column_values(options, values),
'level': level,
'unfoldable': True,
'expand_function': '_report_expand_unfoldable_line_category_line',
}
def _get_account_line(self, options, values, current, level):
base_line_values = list(values.values())[0]
unfoldable = options.get('multi_rate_in_period')
return {
**self._get_base_line(options, current, level),
'name': '%s %s' % (base_line_values['account_code'][0], base_line_values['account_name'][0]),
'columns': self._get_column_values(options, values),
'level': level,
'unfoldable': unfoldable,
'caret_options': False if unfoldable else 'account.account',
'account_id': base_line_values['account_id'][0],
'expand_function': unfoldable and '_report_expand_unfoldable_line_account_line',
}
def _get_rate_line(self, options, values, current, level, markup=None):
base_line_values = list(values.values())[0]
return {
**self._get_base_line(options, current, level, markup),
'name': f"{base_line_values['account_code'][0]} {base_line_values['account_name'][0]}",
'columns': self._get_column_values(options, values),
'level': level,
'unfoldable': False,
'caret_options': 'account.account',
'account_id': base_line_values['account_id'][0],
}
def _get_base_line(self, options, current, level, markup=None):
current_line_id = self._build_line_id(options, current, level, markup=markup)
return {
'id': current_line_id,
'parent_id': self._build_line_id(options, current, level, parent=True, markup=markup, ),
'unfolded': current_line_id in options.get('unfolded_lines') or options.get('unfold_all'),
}
def _get_single_value(self, values, key):
return all(values[key][0] == x for x in values[key]) and values[key][0]
def _get_current_rate(self, values):
return self._get_single_value(values, 'account_rate') or None
def _get_current_disallowed_amount(self, values):
return values['account_disallowed_amount']

View File

@ -0,0 +1,5 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
"access_odx30_account_disallowed_expenses_category","access.odex30.account.disallowed.expenses.category","model_account_disallowed_expenses_category","account.group_account_manager",1,1,1,1
"access_odx30_account_disallowed_expenses_category_readonly","access.odex30.account.disallowed.expenses.category.readonly","model_account_disallowed_expenses_category","account.group_account_readonly",1,0,0,0
"access_odx30_account_disallowed_expenses_rate","access.odex30.account.disallowed.expenses.rate","model_account_disallowed_expenses_rate","account.group_account_manager",1,1,1,1
"access_odx30_account_disallowed_expenses_rate_readonly","access.odex30.account.disallowed.expenses.rate.readonly","model_account_disallowed_expenses_rate","account.group_account_readonly",1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_odx30_account_disallowed_expenses_category access.odex30.account.disallowed.expenses.category model_account_disallowed_expenses_category account.group_account_manager 1 1 1 1
3 access_odx30_account_disallowed_expenses_category_readonly access.odex30.account.disallowed.expenses.category.readonly model_account_disallowed_expenses_category account.group_account_readonly 1 0 0 0
4 access_odx30_account_disallowed_expenses_rate access.odex30.account.disallowed.expenses.rate model_account_disallowed_expenses_rate account.group_account_manager 1 1 1 1
5 access_odx30_account_disallowed_expenses_rate_readonly access.odex30.account.disallowed.expenses.rate.readonly model_account_disallowed_expenses_rate account.group_account_readonly 1 0 0 0

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="odex25_account_disallowed_expenses_comp_rule" model="ir.rule">
<field name="name">Account disallowed expenses multi-country</field>
<field name="model_id" ref="model_account_disallowed_expenses_category"/>
<field name="global" eval="True"/>
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates>
<t t-name="odex30_account_disallowed_expenses.warning_multi_rate">
There are multiple disallowed expenses rates in this period
</t>
</templates>

View File

@ -0,0 +1,25 @@
<?xml version='1.0' encoding='utf-8'?>
<odoo>
<record id="view_account_form" model="ir.ui.view">
<field name="name">account.account.form</field>
<field name="model">account.account</field>
<field name="inherit_id" ref="account.view_account_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='tax_ids']" position="after">
<field name="disallowed_expenses_category_id" string="Disallowed Expenses"
invisible="internal_group != 'expense' and internal_group != 'income'"/>
</xpath>
</field>
</record>
<record id="view_account_search" model="ir.ui.view">
<field name="name">account.account.search</field>
<field name="model">account.account</field>
<field name="inherit_id" ref="account.view_account_search"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="disallowed_expenses_category_id"/>
</field>
</field>
</record>
</odoo>

View File

@ -0,0 +1,103 @@
<?xml version='1.0' encoding='utf-8'?>
<odoo>
<record id="odex30_account_disallowed_expenses_rate_tree" model="ir.ui.view">
<field name="name">account.disallowed.expenses.rate.list</field>
<field name="model">account.disallowed.expenses.rate</field>
<field name="arch" type="xml">
<list default_order="date_from" editable="bottom" create="1" delete="1">
<field name="date_from"/>
<field name="rate"/>
</list>
</field>
</record>
<record id="view_odex30_account_disallowed_expenses_category_tree" model="ir.ui.view">
<field name="name">account.disallowed.expenses.category.list</field>
<field name="model">account.disallowed.expenses.category</field>
<field name="arch" type="xml">
<list editable="bottom">
<field name="code"/>
<field name="name"/>
<field name="account_ids" string="Related Account(s)" widget="many2many_tags" domain="[('internal_group', 'in', ('expense', 'income')), ('disallowed_expenses_category_id', '=', None)]" options="{'no_create': True}"/>
<field name="company_id" optional="hide"/>
<field name="current_rate" invisible="not current_rate"/>
<button name="action_read_category" type="object" string="Set Rates" class="float-end btn-secondary"/>
</list>
</field>
</record>
<record id="view_odex30_account_disallowed_expenses_category_search" model="ir.ui.view">
<field name="name">account.disallowed.expenses.category.search</field>
<field name="model">account.disallowed.expenses.category</field>
<field name="arch" type="xml">
<search>
<field name="code"/>
<field name="name"/>
</search>
</field>
</record>
<record id="view_odex30_account_disallowed_expenses_category_form" model="ir.ui.view">
<field name="name">account.disallowed.expenses.category.form</field>
<field name="model">account.disallowed.expenses.category</field>
<field name="arch" type="xml">
<form>
<field name="account_ids" invisible="1"/>
<sheet>
<div>
<h1 style="font-size: 1.9rem;">
<div class="row">
<div class="col">
<label for="code" string="Code"/>
<div>
<field name="code" placeholder="e.g. 1201"/>
</div>
</div>
<div class="col col-md-10">
<label for="name" string="Category Name"/>
<div>
<field name="name"
placeholder="e.g. Non-Deductible Tax"
class="oe_inline"/>
</div>
</div>
</div>
</h1>
</div>
<group>
<group name="left_column">
<field name="company_id" options="{'no_create': True}"/>
<field name="account_ids" string="Related Account(s)" widget="many2many_tags" domain="[('internal_group', 'in', ('expense', 'income'))]" options="{'no_create': True}"/>
</group>
</group>
<notebook>
<page name="rates" string="Rates">
<field name="rate_ids" nolabel="1">
<list order="date_from desc" editable="bottom">
<field name="date_from" width="200px"/>
<field name="rate" width="200px"/>
<field name="company_id" groups="base.group_multi_company"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="action_odex30_account_disallowed_expenses_category_list" model="ir.actions.act_window">
<field name="name">Disallowed Expenses Categories</field>
<field name="res_model">account.disallowed.expenses.category</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Add a Disallowed Expenses Category
</p>
</field>
</record>
<menuitem action="action_odex30_account_disallowed_expenses_category_list"
id="menu_action_odex30_account_disallowed_expenses_category_list"
parent="account.account_management_menu"
groups="account.group_account_manager"/>
</odoo>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="action_account_report_de" model="ir.actions.client">
<field name="name">Disallowed Expenses Report</field>
<field name="tag">account_report</field>
<field name="context" eval="{'report_id': ref('odex30_account_disallowed_expenses.disallowed_expenses_report')}" />
</record>
<menuitem id="menu_action_account_report_de"
name="Disallowed Expenses"
action="action_account_report_de"
parent="account.account_reports_management_menu"
groups="account.group_account_manager"/>
</data>
</odoo>

View File

@ -0,0 +1,3 @@
from . import models
from . import report

View File

@ -0,0 +1,22 @@
{
'name': 'Disallowed Expenses on Fleets',
'category': 'Accounting/Accounting',
'summary': 'Manage disallowed expenses with fleets',
'version': '1.0',
'depends': ['odex30_account_accountant_fleet', 'odex30_account_disallowed_expenses'],
'data': [
'security/ir.model.access.csv',
'data/account_disallowed_expenses_fleet_report.xml',
'views/account_disallowed_expenses_category_views.xml',
'views/fleet_vehicle_views.xml',
],
'assets': {
'web.assets_backend': [
'odex30_account_disallowed_expenses_fleet/static/src/components/**/*',
],
},
'installable': True,
'auto_install': True,
'license': 'OEEL-1',
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="odex30_account_disallowed_expenses.disallowed_expenses_report" model="account.report">
<field name="name">Disallowed Expenses Report</field>
<field name="custom_handler_model_id" ref="model_account_disallowed_expenses_fleet_report_handler"/>
</record>
</odoo>

View File

@ -0,0 +1,144 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * account_disallowed_expenses_fleet
#
# Translators:
# Wil Odoo, 2024
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-09-25 09:26+0000\n"
"PO-Revision-Date: 2024-09-25 09:44+0000\n"
"Last-Translator: Wil Odoo, 2024\n"
"Language-Team: Arabic (https://app.transifex.com/odoo/teams/41243/ar/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Language: ar\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
#. module: account_disallowed_expenses_fleet
#. odoo-python
#: code:addons/account_disallowed_expenses_fleet/report/account_disallowed_expenses_report.py:0
msgid "Accounts missing a disallowed expense category"
msgstr "الحسابات التي ليست بها فئة نفقات غير مسموح بها "
#. module: account_disallowed_expenses_fleet
#: model:ir.model.fields,field_description:account_disallowed_expenses_fleet.field_fleet_disallowed_expenses_rate__company_id
msgid "Company"
msgstr "الشركة "
#. module: account_disallowed_expenses_fleet
#: model:ir.model.fields,field_description:account_disallowed_expenses_fleet.field_fleet_disallowed_expenses_rate__create_uid
msgid "Created by"
msgstr "أنشئ بواسطة"
#. module: account_disallowed_expenses_fleet
#: model:ir.model.fields,field_description:account_disallowed_expenses_fleet.field_fleet_disallowed_expenses_rate__create_date
msgid "Created on"
msgstr "أنشئ في"
#. module: account_disallowed_expenses_fleet
#: model:ir.model,name:account_disallowed_expenses_fleet.model_account_deferred_report_handler
msgid "Deferred Expense Report Custom Handler"
msgstr "أداة مخصصة لمعالجة تقرير النفقات المؤجلة "
#. module: account_disallowed_expenses_fleet
#: model:ir.model,name:account_disallowed_expenses_fleet.model_account_disallowed_expenses_category
msgid "Disallowed Expenses Category"
msgstr "فئة النفقات غير المسموح بها "
#. module: account_disallowed_expenses_fleet
#: model:ir.model,name:account_disallowed_expenses_fleet.model_account_disallowed_expenses_fleet_report_handler
msgid "Disallowed Expenses Fleet Custom Handler"
msgstr "الأداة المخصصة للتعامل مع نفقات الأسطول غير المسموح بها "
#. module: account_disallowed_expenses_fleet
#: model:ir.model.fields,field_description:account_disallowed_expenses_fleet.field_fleet_vehicle__rate_ids
#: model_terms:ir.ui.view,arch_db:account_disallowed_expenses_fleet.fleet_vehicle_view_form
msgid "Disallowed Expenses Rate"
msgstr "معدل النفقات غير المسموح به "
#. module: account_disallowed_expenses_fleet
#: model:ir.model.fields,field_description:account_disallowed_expenses_fleet.field_fleet_disallowed_expenses_rate__display_name
msgid "Display Name"
msgstr "اسم العرض "
#. module: account_disallowed_expenses_fleet
#: model:ir.model.fields,field_description:account_disallowed_expenses_fleet.field_fleet_disallowed_expenses_rate__id
msgid "ID"
msgstr "المُعرف"
#. module: account_disallowed_expenses_fleet
#. odoo-javascript
#: code:addons/account_disallowed_expenses_fleet/static/src/components/disallowed_expenses_report/warnings.xml:0
msgid ""
"It looks like Journal Items concerning vehicles are missing. Please check if"
msgstr ""
"يبدو أن عناصر دفتر اليومية المتعلقة بالمركبات مفقودة. يرجى التحقق مما إذا "
#. module: account_disallowed_expenses_fleet
#: model:ir.model,name:account_disallowed_expenses_fleet.model_account_move_line
msgid "Journal Item"
msgstr "عنصر اليومية"
#. module: account_disallowed_expenses_fleet
#: model:ir.model.fields,field_description:account_disallowed_expenses_fleet.field_fleet_disallowed_expenses_rate__write_uid
msgid "Last Updated by"
msgstr "آخر تحديث بواسطة"
#. module: account_disallowed_expenses_fleet
#: model:ir.model.fields,field_description:account_disallowed_expenses_fleet.field_fleet_disallowed_expenses_rate__write_date
msgid "Last Updated on"
msgstr "آخر تحديث في"
#. module: account_disallowed_expenses_fleet
#: model:ir.model,name:account_disallowed_expenses_fleet.model_bank_rec_widget_line
msgid "Line of the bank reconciliation widget"
msgstr "بند أداة التسوية البنكية "
#. module: account_disallowed_expenses_fleet
#: model:ir.model.fields,field_description:account_disallowed_expenses_fleet.field_account_disallowed_expenses_category__car_category
msgid "Make Vehicle Required"
msgstr "اجعل المركبة مطلوبة "
#. module: account_disallowed_expenses_fleet
#: model:ir.model.fields,field_description:account_disallowed_expenses_fleet.field_fleet_disallowed_expenses_rate__date_from
msgid "Start Date"
msgstr "تاريخ البدء "
#. module: account_disallowed_expenses_fleet
#: model:ir.model.fields,help:account_disallowed_expenses_fleet.field_account_disallowed_expenses_category__car_category
msgid "The vehicle becomes mandatory while booking any account move."
msgstr "تصبح المركبة ضرورية عند حجز أي حركة حساب. "
#. module: account_disallowed_expenses_fleet
#: model:ir.model,name:account_disallowed_expenses_fleet.model_fleet_vehicle
#: model:ir.model.fields,field_description:account_disallowed_expenses_fleet.field_fleet_disallowed_expenses_rate__vehicle_id
msgid "Vehicle"
msgstr "المركبة"
#. module: account_disallowed_expenses_fleet
#: model:ir.model,name:account_disallowed_expenses_fleet.model_fleet_disallowed_expenses_rate
msgid "Vehicle Disallowed Expenses Rate"
msgstr "معدل النفقات غير المسموح به للمركبات "
#. module: account_disallowed_expenses_fleet
#. odoo-javascript
#: code:addons/account_disallowed_expenses_fleet/static/src/components/disallowed_expenses_report/filter_extra_options.xml:0
msgid "Vehicle Split"
msgstr "مشاركة المركبة "
#. module: account_disallowed_expenses_fleet
#. odoo-javascript
#: code:addons/account_disallowed_expenses_fleet/static/src/components/disallowed_expenses_report/warnings.xml:0
msgid "need a Disallowed Expense Category."
msgstr "تحتاج إلى فئة نفقات غير مسموح بها. "
#. module: account_disallowed_expenses_fleet
#. odoo-javascript
#: code:addons/account_disallowed_expenses_fleet/static/src/components/disallowed_expenses_report/warnings.xml:0
msgid "these accounts"
msgstr "تلك الحسابات "

View File

@ -0,0 +1,6 @@
from . import odex30_account_deferred_reports
from . import odex30_account_disallowed_expenses
from . import account_move
from . import bank_rec_widget_line
from . import fleet_vehicle

View File

@ -0,0 +1,25 @@
from odoo import models, api
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
@api.depends('account_id.disallowed_expenses_category_id')
def _compute_need_vehicle(self):
for record in self:
record.need_vehicle = record.account_id.disallowed_expenses_category_id.sudo().car_category and record.move_id.move_type == 'in_invoice'
@api.model
def _get_deferred_lines_values(self, account_id, balance, ref, analytic_distribution, line):
deferred_lines_values = super()._get_deferred_lines_values(account_id, balance, ref, analytic_distribution, line)
return {
**deferred_lines_values,
'vehicle_id': int(line['vehicle_id'] or 0) or None,
}
@api.model
def _get_deferred_amounts_by_line_values(self, line):
values = super()._get_deferred_amounts_by_line_values(line)
values['vehicle_id'] = int(line['vehicle_id'] or 0) or None
return values

View File

@ -0,0 +1,11 @@
from odoo import api, models
class BankRecWidgetLine(models.Model):
_inherit = 'bank.rec.widget.line'
@api.depends('account_id')
def _compute_vehicle_required(self):
for line in self:
line.vehicle_required = line.account_id and line.account_id.disallowed_expenses_category_id.car_category

View File

@ -0,0 +1,18 @@
from odoo import models, fields
class FleetVehicle(models.Model):
_inherit = 'fleet.vehicle'
rate_ids = fields.One2many('fleet.disallowed.expenses.rate', 'vehicle_id', string='Disallowed Expenses Rate')
class FleetDisallowedExpensesRate(models.Model):
_name = 'fleet.disallowed.expenses.rate'
_description = 'Vehicle Disallowed Expenses Rate'
_order = 'date_from desc'
rate = fields.Float(string='%', required=True)
date_from = fields.Date(string='Start Date', required=True)
vehicle_id = fields.Many2one('fleet.vehicle', string='Vehicle', required=True)
company_id = fields.Many2one('res.company', string='Company', related='vehicle_id.company_id', readonly=True)

View File

@ -0,0 +1,30 @@
from odoo import models, api
from odoo.tools import SQL
class DeferredReportCustomHandler(models.AbstractModel):
_inherit = 'account.deferred.report.handler'
@api.model
def _get_select(self, options):
return super()._get_select(options) + [SQL("account_move_line.vehicle_id AS vehicle_id")]
@api.model
def _get_grouping_fields_deferred_lines(self, filter_already_generated=False, grouping_field='account_id'):
res = super()._get_grouping_fields_deferred_lines(filter_already_generated, grouping_field)
if filter_already_generated:
res += ('vehicle_id',)
return res
@api.model
def _get_grouping_fields_deferral_lines(self):
res = super()._get_grouping_fields_deferral_lines()
res += ('vehicle_id',)
return res
@api.model
def _get_current_key_totals_dict(self, lines_per_key, sign):
totals = super()._get_current_key_totals_dict(lines_per_key, sign)
totals['vehicle_id'] = lines_per_key[0]['vehicle_id']
return totals

View File

@ -0,0 +1,15 @@
from odoo import api, fields, models
class AccountDisallowedExpensesCategory(models.Model):
_inherit = 'account.disallowed.expenses.category'
car_category = fields.Boolean('Make Vehicle Required', help='The vehicle becomes mandatory while booking any account move.')
@api.depends('car_category')
def _compute_display_name(self):
super()._compute_display_name()
for category in self:
if category.car_category:
category.display_name = f'{category.code} - {category.name}'

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_disallowed_expenses_report

View File

@ -0,0 +1,289 @@
from odoo import models, _
from odoo.tools import SQL
class DisallowedExpensesFleetCustomHandler(models.AbstractModel):
_name = 'account.disallowed.expenses.fleet.report.handler'
_inherit = 'account.disallowed.expenses.report.handler'
_description = 'Disallowed Expenses Fleet Custom Handler'
def _get_custom_display_config(self):
return {
'templates': {
'AccountReportFilters': 'odex30_account_disallowed_expenses_fleet.DisallowedExpensesFleetReportFilters',
}
}
def _custom_options_initializer(self, report, options, previous_options):
super()._custom_options_initializer(report, options, previous_options=previous_options)
options['vehicle_split'] = previous_options.get('vehicle_split', True)
period_domain = [('date_from', '>=', options['date']['date_from']), ('date_from', '<=', options['date']['date_to'])]
rg = self.env['fleet.disallowed.expenses.rate']._read_group(
period_domain,
['vehicle_id'],
having=[('__count', '>', 1)],
limit=1,
)
options['multi_rate_in_period'] = options.get('multi_rate_in_period') or bool(rg)
def _customize_warnings(self, report, options, all_column_groups_expression_totals, warnings):
accounts = self.env['account.move.line']._read_group(
[
('date', '<=', options['date']['date_to']),
('date', '>=', options['date']['date_from']),
('parent_state', '=', 'posted'),
('account_type', '=', 'expense'),
('vehicle_id', '!=', None),
('account_id.disallowed_expenses_category_id', '=', None),
],
['account_id'],
)
if accounts:
warnings['odex30_account_disallowed_expenses_fleet.warning_missing_disallowed_category'] = {
'alert_type': 'warning',
'args': [account[0].id for account in accounts],
}
def _get_query(self, options, line_dict_id=None) -> tuple[SQL, SQL, SQL, SQL, SQL, SQL]:
select, from_, where, group_by, order_by, order_by_rate = super()._get_query(options, line_dict_id)
current = self._parse_line_id(options, line_dict_id)
select = SQL(
"""
%(select)s,
ARRAY_AGG(fleet_rate.rate) fleet_rate,
ARRAY_AGG(vehicle.id) vehicle_id,
ARRAY_AGG(vehicle.name) vehicle_name,
SUM(aml.balance * (
CASE WHEN fleet_rate.rate IS NOT NULL
THEN
CASE WHEN rate.rate IS NOT NULL
THEN
CASE WHEN fleet_rate.rate < rate.rate
THEN fleet_rate.rate
ELSE rate.rate
END
ELSE fleet_rate.rate
END
ELSE rate.rate
END)) / 100 AS fleet_disallowed_amount
""",
select=select,
)
from_ = SQL(
"""
%(from_)s
LEFT JOIN fleet_vehicle vehicle ON aml.vehicle_id = vehicle.id
LEFT JOIN fleet_disallowed_expenses_rate fleet_rate ON fleet_rate.id = (
SELECT r2.id FROm fleet_disallowed_expenses_rate r2
JOIN fleet_vehicle v2 ON r2.vehicle_id = v2.id
WHERE r2.date_from <= aml.date
AND v2.id = vehicle.id
ORDER BY r2.date_from DESC LIMIT 1
)
""",
from_=from_
)
where = SQL(
"""
%(where)s
%(vehicle_condition)s
%(vehicle_is_null)s
""",
where=where,
vehicle_condition=SQL("AND vehicle.id = %s", current['vehicle_id']) if current.get('vehicle_id') else SQL(),
vehicle_is_null=SQL("""AND vehicle.id IS NULL""") if current.get('account_id') and not current.get('vehicle_id') and options.get('vehicle_split') else SQL()
)
group_by = SQL("GROUP BY category.id")
if len(current) == 1 and current.get('category_id'):
if options.get('vehicle_split'):
group_by = SQL("%s, aml.vehicle_id, COALESCE(aml.vehicle_id, aml.account_id)", group_by)
order_by = SQL(" ORDER BY aml.vehicle_id, COALESCE(aml.vehicle_id, aml.account_id)")
else:
group_by = SQL("%s, account.id", group_by)
order_by = SQL("ORDER BY account.id")
elif current.get('vehicle_id') and not current.get('account_id'):
# Expanding a vehicle
group_by = SQL("%s, vehicle.id, account.id", group_by)
order_by = SQL("ORDER BY vehicle.id, account.id")
elif current.get('account_id') and options.get('multi_rate_in_period'):
# Expanding an account
if options.get('vehicle_split'):
group_by = SQL("%s, vehicle.id, rate.rate, fleet_rate.rate", group_by)
order_by = SQL("ORDER BY vehicle.id, rate.rate, fleet_rate.rate")
else:
group_by = SQL("%s, rate.rate, fleet_rate.rate", group_by)
order_by = SQL("ORDER BY rate.rate, fleet_rate.rate")
return select, from_, where, group_by, order_by, order_by_rate
def _parse_line_id(self, options, line_id):
current = {'category_id': None}
if not line_id:
return current
for dummy, model, record_id in self.env['account.report']._parse_line_id(line_id):
if model == 'account.disallowed.expenses.category':
current.update({'category_id': record_id})
if model == 'fleet.vehicle':
current.update({'vehicle_id': record_id})
if model == 'account.account':
current.update({'account_id': record_id})
if model == 'account.disallowed.expenses.rate':
if model == 'fleet.vehicle':
current.update({'fleet_rate': record_id})
else:
current.update({'account_rate': record_id})
return current
def _build_line_id(self, options, current, level, parent=False, markup=None):
report = self.env['account.report'].browse(options['report_id'])
parent_line_id = None
line_id = report._get_generic_line_id('account.disallowed.expenses.category', current['category_id'])
if current.get('vehicle_id') and options.get('vehicle_split'):
parent_line_id = line_id
line_id = report._get_generic_line_id('fleet.vehicle', current['vehicle_id'], parent_line_id=line_id)
if current.get('account_id'):
parent_line_id = line_id
line_id = report._get_generic_line_id('account.account', current['account_id'], parent_line_id=line_id)
if len(current) != level and not (current.get('account_rate') or current.get('fleet_rate')):
parent_line_id = line_id
line_id = report._get_generic_line_id('account.account', current['account_id'], parent_line_id=line_id)
if current.get('account_rate'):
parent_line_id = line_id
line_id = report._get_generic_line_id('account.disallowed.expenses.rate', current['account_rate'], markup=markup, parent_line_id=line_id)
if current.get('fleet_rate'):
parent_line_id = line_id
line_id = report._get_generic_line_id('fleet.disallowed.expenses.rate', current['fleet_rate'], markup=markup, parent_line_id=line_id)
return parent_line_id if parent else line_id
def _report_expand_unfoldable_line_category_line(self, line_dict_id, groupby, options, progress, offset, unfold_all_batch_data=None):
primary_fields = ['category_id', 'vehicle_id']
secondary_fields = ['category_id', 'account_id']
if options.get('vehicle_split'):
results = self._get_query_results(options, line_dict_id, primary_fields, secondary_fields, 'vehicle_id')
else:
results = self._get_query_results(options, line_dict_id, secondary_fields)
lines = []
unfoldable_lines = []
for group_key, result in results.items():
current = self._parse_hierarchy_group_key(group_key)
level = len(self._parse_line_id(options, line_dict_id)) + 1
if options.get('vehicle_split') and current.get('vehicle_id'):
current = self._filter_current(current, primary_fields)
line = self._disallowed_expenses_get_vehicle_line(options, result, current, level)
else:
current = self._filter_current(current, secondary_fields)
line = self._get_account_line(options, result, current, level)
if line.get('unfoldable'):
unfoldable_lines.append(line)
else:
lines.append(line)
return {'lines': lines + unfoldable_lines}
def _report_expand_unfoldable_line_account_line(self, line_dict_id, groupby, options, progress, offset, unfold_all_batch_data=None):
primary_fields = ['category_id', 'vehicle_id', 'account_id', 'fleet_rate']
secondary_fields = ['category_id', 'account_id', 'account_rate', 'fleet_rate']
if options.get('vehicle_split'):
results = self._get_query_results(options, line_dict_id, primary_fields, secondary_fields, 'vehicle_id')
else:
results = self._get_query_results(options, line_dict_id, secondary_fields)
lines = []
for group_key, result in results.items():
current = self._parse_hierarchy_group_key(group_key)
level = len(self._parse_line_id(options, line_dict_id)) + 1
if options.get('vehicle_split') and current.get('vehicle_id'):
current = self._filter_current(current, primary_fields)
else:
current = self._filter_current(current, secondary_fields)
base_line_values = list(result.values())[0]
account_id = self._get_single_value(base_line_values, 'account_id')
lines.append(self._get_rate_line(options, result, current, level, account_id))
return {'lines': lines}
def _report_expand_unfoldable_line_vehicle_line(self, line_dict_id, groupby, options, progress, offset, unfold_all_batch_data=None):
results = self._get_query_results(options, line_dict_id, ['category_id', 'vehicle_id', 'account_id'])
lines = []
for group_key, result in results.items():
current = self._parse_hierarchy_group_key(group_key)
level = len(self._parse_line_id(options, line_dict_id)) + 1
if options.get('vehicle_split') and current.get('fleet_rate'):
base_line_values = list(result.values())[0]
account_id = self._get_single_value(base_line_values, 'account_id')
lines.append(self._get_rate_line(options, result, current, level, account_id))
else:
lines.append(self._get_account_line(options, result, current, level))
return {'lines': lines}
def _disallowed_expenses_get_vehicle_line(self, options, values, current, level):
base_line_values = list(values.values())[0]
return {
**self._get_base_line(options, current, level),
'name': base_line_values['vehicle_name'][0],
'columns': self._get_column_values(options, values),
'level': level,
'unfoldable': True,
'caret_options': False,
'expand_function': '_report_expand_unfoldable_line_vehicle_line',
}
def _get_current_rate(self, values):
fleet_rate = self._get_single_value(values, 'fleet_rate')
account_rate = self._get_single_value(values, 'account_rate')
current_rate = None
if fleet_rate is not False:
if fleet_rate is not None:
if account_rate:
current_rate = min(account_rate, fleet_rate)
else:
current_rate = fleet_rate
elif account_rate:
current_rate = account_rate
return current_rate
def _get_current_disallowed_amount(self, values):
res = super()._get_current_disallowed_amount(values)
return values['fleet_disallowed_amount'] if any(values['vehicle_id']) else res
def _filter_current(self, current, fields):
return {key: val for key, val in current.items() if key in fields}
def action_open_accounts(self, options, params):
return {
'type': 'ir.actions.act_window',
'name': _("Accounts missing a disallowed expense category"),
'res_model': 'account.account',
'views': [(False, 'list'), (False, 'form')],
'domain': [('id', 'in', params['args'])],
}

View File

@ -0,0 +1,4 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
"access_fleet_disallowed_expenses_rate_account_user","access.fleet.disallowed.expenses.rate.account.user","model_fleet_disallowed_expenses_rate","account.group_account_readonly",1,0,0,0
"access_fleet_disallowed_expenses_rate_account_manager","access.fleet.disallowed.expenses.rate.account.manager","model_fleet_disallowed_expenses_rate","account.group_account_manager",1,1,1,1
"access_fleet_disallowed_expenses_rate_fleet_user","access.fleet.disallowed.expenses.rate.fleet.user","model_fleet_disallowed_expenses_rate","fleet.fleet_group_user",1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_fleet_disallowed_expenses_rate_account_user access.fleet.disallowed.expenses.rate.account.user model_fleet_disallowed_expenses_rate account.group_account_readonly 1 0 0 0
3 access_fleet_disallowed_expenses_rate_account_manager access.fleet.disallowed.expenses.rate.account.manager model_fleet_disallowed_expenses_rate account.group_account_manager 1 1 1 1
4 access_fleet_disallowed_expenses_rate_fleet_user access.fleet.disallowed.expenses.rate.fleet.user model_fleet_disallowed_expenses_rate fleet.fleet_group_user 1 0 0 0

View File

@ -0,0 +1,14 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { BankRecKanbanController } from "@odex30_account_accountant/components/bank_reconciliation/kanban";
patch(BankRecKanbanController.prototype, {
getBankRecLineInvalidFields(line){
const invalidFields = super.getBankRecLineInvalidFields(...arguments);
if (line.data.vehicle_required && !line.data.vehicle_id) {
invalidFields.push("vehicle");
}
return invalidFields;
}
});

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates>
<t t-name="odex30_account_disallowed_expenses_fleet.DisallowedExpensesFleetReportFilterExtraOptions" t-inherit="odex30_account_reports.AccountReportFilterExtraOptions">
<xpath expr="//DropdownItem[contains(@class, 'filter_show_all_hook')]" position="after">
<DropdownItem
class="{ 'selected': controller.options.vehicle_split }"
onSelected="() => this.filterClicked({ optionKey: 'vehicle_split'})"
closingMode="'none'"
>
Vehicle Split
</DropdownItem>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,8 @@
import { patch } from "@web/core/utils/patch";
import { AccountReportFilters } from "@odex30_account_reports/components/account_report/filters/filters";
patch(AccountReportFilters.prototype, {
get hasExtraOptionsFilter() {
return super.hasExtraOptionsFilter || "vehicle_split" in this.controller.options;
},
});

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates>
<t t-name="odex30_account_disallowed_expenses_fleet.DisallowedExpensesFleetReportFilters" t-inherit="odex30_account_reports.AccountReportFiltersCustomizable">
<xpath expr="//div[@id='filter_extra_options']" position="replace">
<t t-call="odex30_account_disallowed_expenses_fleet.DisallowedExpensesFleetReportFilterExtraOptions"/>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates>
<t t-name="odex30_account_disallowed_expenses_fleet.warning_missing_disallowed_category">
It looks like Journal Items concerning vehicles are missing. Please check if
<a t-on-click="(ev) => controller.reportAction(ev, 'action_open_accounts', warningParams)">these accounts</a>
need a Disallowed Expense Category.
</t>
</templates>

View File

@ -0,0 +1,5 @@
from . import test_deferred_fleet
from . import test_bank_rec_widget
from . import test_disallowed_expenses_fleet
from . import test_account_move_fleet

View File

@ -0,0 +1,66 @@
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo import Command, fields
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestAccountMoveFleet(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.batmobile = cls.env['fleet.vehicle'].create({
'model_id': cls.env['fleet.vehicle.model'].create({
'name': 'Batmobile',
'brand_id': cls.env['fleet.vehicle.model.brand'].create({
'name': 'Wayne Enterprises',
}).id,
'vehicle_type': 'car',
'default_fuel_type': 'hydrogen',
}).id,
'rate_ids': [Command.create({
'date_from': fields.Date.from_string('2022-01-01'),
'rate': 31.0,
})],
})
def test_account_move_line_vehicle_id(self):
self.company_data['default_tax_purchase'].invoice_repartition_line_ids.use_in_tax_closing = False
bill = self.env['account.move'].create([{
'move_type': 'in_invoice',
'date': fields.Date.from_string('2022-01-15'),
'invoice_date': fields.Date.from_string('2022-01-15'),
'invoice_line_ids': [
Command.create({
'name': 'test_line',
'account_id': self.company_data['default_account_expense'].id,
'quantity': 1,
'price_unit': 100.0,
'vehicle_id': self.batmobile.id,
'tax_ids': [Command.set(self.company_data['default_tax_purchase'].ids)],
}),
]
}])
self.assertFalse(bill.line_ids.filtered(lambda l: l.display_type in ('product', 'tax') and not l.vehicle_id))
bill.invoice_line_ids.write({'vehicle_id': False})
self.assertRecordValues(bill.line_ids.sorted('balance'), [
{
'balance': -115.0,
'tax_base_amount': 0.0,
'tax_ids': [],
'vehicle_id': False,
}, {
'balance': 15.0,
'tax_base_amount': 100.0,
'tax_ids': [],
'vehicle_id': False,
}, {
'balance': 100,
'tax_base_amount': 0,
'vehicle_id': False,
'tax_ids': self.company_data['default_tax_purchase'].ids,
},
])

View File

@ -0,0 +1,104 @@
from odoo import Command
from odoo.tests import tagged
from odoo.addons.odex30_account_accountant.tests.test_bank_rec_widget_common import TestBankRecWidgetCommon
@tagged('post_install', '-at_install')
class TestBankRecWidgetFleet(TestBankRecWidgetCommon):
def test_bank_rec_vehicle(self):
vehicle_exp_acc, other_exp_acc, tax_acc = self.env['account.account'].create([{
'code': code,
'name': name,
} for code, name in [
('611010', 'Vehicle Expenses'),
('611020', 'Other Expenses'),
('811000', 'Tax Account'),
]])
self.env['account.disallowed.expenses.category'].create({
'code': '23456',
'name': 'Robins DNA',
'car_category': True,
'account_ids': [Command.set(vehicle_exp_acc.ids)],
})
fleet_brand = self.env['fleet.vehicle.model.brand'].create({
'name': 'Odoo',
})
fleet_model = self.env['fleet.vehicle.model'].create({
'name': 'v16',
'brand_id': fleet_brand.id,
'vehicle_type': 'car',
})
robinmobile = self.env['fleet.vehicle'].create({
'model_id': fleet_model.id,
'license_plate': 'BE1234-2'
})
regular_tax = self.env['account.tax'].create({
'name': 'Regular',
'amount_type': 'percent',
'amount': 25.0,
'type_tax_use': 'sale',
'company_id': self.company_data['company'].id,
'invoice_repartition_line_ids': [
Command.create({'repartition_type': 'base'}),
Command.create({'factor_percent': 100, 'account_id': tax_acc.id, 'use_in_tax_closing': True}),
],
'refund_repartition_line_ids': [
Command.create({'repartition_type': 'base'}),
Command.create({'factor_percent': 100, 'account_id': tax_acc.id, 'use_in_tax_closing': True}),
],
})
car_tax = self.env['account.tax'].create({
'name': 'Car',
'amount_type': 'percent',
'amount': 25.0,
'type_tax_use': 'sale',
'company_id': self.company_data['company'].id,
'invoice_repartition_line_ids': [
Command.create({'repartition_type': 'base'}),
Command.create({'factor_percent': 50}),
Command.create({'factor_percent': 50, 'account_id': tax_acc.id, 'use_in_tax_closing': True}),
],
'refund_repartition_line_ids': [
Command.create({'repartition_type': 'base'}),
Command.create({'factor_percent': 50}),
Command.create({'factor_percent': 50, 'account_id': tax_acc.id, 'use_in_tax_closing': True}),
],
})
st_line = self._create_st_line(1000.0, partner_id=None, partner_name="The Driver")
wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({})
line = wizard.line_ids.filtered(lambda x: x.flag == 'auto_balance')
wizard._js_action_mount_line_in_edit(line.index)
line.account_id = other_exp_acc
wizard._line_value_changed_account_id(line)
self.assertRecordValues(line, [{
'account_id': other_exp_acc.id,
'vehicle_required': False,
}])
line.account_id = vehicle_exp_acc
wizard._line_value_changed_account_id(line)
self.assertRecordValues(line, [{
'account_id': vehicle_exp_acc.id,
'vehicle_required': True,
}])
line.vehicle_id = robinmobile
line.tax_ids = [Command.set(regular_tax.ids)]
wizard._line_value_changed_vehicle_id(line)
tax_lines = wizard.line_ids.filtered(lambda x: x.flag == 'tax_line')
self.assertRecordValues(tax_lines, [{
'account_id': tax_acc.id,
'vehicle_id': False,
}])
line.tax_ids = [Command.set(car_tax.ids)]
wizard._line_value_changed_tax_ids(line)
tax_lines = wizard.line_ids.filtered(lambda x: x.flag == 'tax_line')
self.assertRecordValues(tax_lines, [
# pylint: disable=C0326
{ 'account_id': vehicle_exp_acc.id, 'vehicle_id': robinmobile.id},
{ 'account_id': tax_acc.id, 'vehicle_id': False },
])

View File

@ -0,0 +1,154 @@
from odoo.addons.odex30_account_reports.tests.common import TestAccountReportsCommon
from odoo import Command, fields
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestDeferredFleet(TestAccountReportsCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.expense_accounts = [cls.env['account.account'].create({
'name': f'Expense {i}',
'code': f'EXP{i}',
'account_type': 'expense',
}) for i in range(3)]
cls.company.deferred_expense_journal_id = cls.company_data['default_journal_misc'].id
cls.company.deferred_expense_account_id = cls.company_data['default_account_deferred_expense'].id
cls.expense_lines = [
[cls.expense_accounts[0], 1000, '2023-01-01', '2023-04-30'],
[cls.expense_accounts[0], 1050, '2023-01-16', '2023-04-30'],
[cls.expense_accounts[1], 1225, '2023-01-01', '2023-04-15'],
[cls.expense_accounts[2], 1680, '2023-01-21', '2023-04-14'],
[cls.expense_accounts[2], 225, '2023-04-01', '2023-04-15'],
]
cls.batmobile, cls.batpod = cls.env['fleet.vehicle'].create([{
'model_id': cls.env['fleet.vehicle.model'].create({
'name': name,
'brand_id': cls.env['fleet.vehicle.model.brand'].create({
'name': 'Wayne Enterprises',
}).id,
'vehicle_type': vehicle_type,
'default_fuel_type': 'hydrogen',
}).id,
'rate_ids': [Command.create({
'date_from': fields.Date.from_string('2022-01-01'),
'rate': rate,
})],
} for name, vehicle_type, rate in [('Batmobile', 'car', 31.0), ('Batpod', 'bike', 56.0)]
])
def test_deferred_fleet_on_validation_mode(self):
move = self.env['account.move'].create({
'move_type': 'in_invoice',
'partner_id': self.partner_a.id,
'date': '2023-01-01',
'invoice_date': '2023-01-01',
'journal_id': self.company_data['default_journal_purchase'].id,
'invoice_line_ids': [
Command.create({
'product_id': self.product_a.id,
'quantity': 1,
'account_id': self.expense_accounts[0].id,
'price_unit': 1000,
'deferred_start_date': '2023-02-01',
'deferred_end_date': '2023-02-28',
'vehicle_id': self.batmobile.id,
}),
]
})
move.action_post()
self.assertRecordValues(move.deferred_move_ids.line_ids, [
{'vehicle_id': self.batmobile.id} for _ in range(4)
])
def test_deferred_fleet_manually_and_grouped_mode(self):
self.company.generate_deferred_expense_entries_method = 'manual'
self.company.deferred_expense_amount_computation_method = 'month'
def get_line(vehicle, account, amount):
return Command.create({
'quantity': 1,
'account_id': account.id,
'price_unit': amount,
'deferred_start_date': '2023-01-01',
'deferred_end_date': '2023-10-31',
'vehicle_id': vehicle.id if vehicle else False,
})
self.env['account.move'].create({
'move_type': 'in_invoice',
'partner_id': self.partner_a.id,
'date': '2023-01-01',
'invoice_date': '2023-01-01',
'journal_id': self.company_data['default_journal_purchase'].id,
'invoice_line_ids': [
get_line(self.batmobile, self.expense_accounts[0], 1000),
get_line(self.batpod, self.expense_accounts[0], 2000),
get_line(self.batpod, self.expense_accounts[1], 5000),
get_line(self.batpod, self.expense_accounts[1], 10000),
get_line(False, self.expense_accounts[1], 10000),
get_line(False, self.expense_accounts[1], 10000),
]
}).action_post()
options = self._generate_options(self.env.ref('odex30_account_reports.deferred_expense_report'), '2023-01-01', '2023-01-31')
deferral_entries = self.env['account.deferred.expense.report.handler']._generate_deferral_entry(options)
expected_values = [{
'account_id': self.expense_accounts[0].id,
'vehicle_id': self.batmobile.id,
'balance': -1000,
}, {
'account_id': self.expense_accounts[0].id,
'vehicle_id': self.batmobile.id,
'balance': 100,
}, {
'account_id': self.expense_accounts[0].id,
'vehicle_id': self.batpod.id,
'balance': -2000,
}, {
'account_id': self.expense_accounts[0].id,
'vehicle_id': self.batpod.id,
'balance': 200,
}, {
'account_id': self.expense_accounts[1].id,
'vehicle_id': self.batpod.id,
'balance': -15000,
}, {
'account_id': self.expense_accounts[1].id,
'vehicle_id': self.batpod.id,
'balance': 1500,
}, {
'account_id': self.expense_accounts[1].id,
'vehicle_id': False,
'balance': -20000,
}, {
'account_id': self.expense_accounts[1].id,
'vehicle_id': False,
'balance': 2000,
}, {
'account_id': self.company.deferred_expense_account_id.id,
'vehicle_id': self.batmobile.id,
'balance': 900,
}, {
'account_id': self.company.deferred_expense_account_id.id,
'vehicle_id': self.batpod.id,
'balance': 13500 + 1800,
}, {
'account_id': self.company.deferred_expense_account_id.id,
'vehicle_id': False,
'balance': 18000,
}]
for line, expected in zip(deferral_entries[0].line_ids, expected_values):
self.assertRecordValues(line, [{
'account_id': expected['account_id'],
'vehicle_id': expected['vehicle_id'],
'balance': expected['balance'],
}
])

View File

@ -0,0 +1,476 @@
from odoo.addons.odex30_account_reports.tests.common import TestAccountReportsCommon
from odoo import Command, fields
from odoo.tests import tagged, freeze_time
@freeze_time('2022-07-15')
@tagged('post_install', '-at_install')
class TestAccountDisallowedExpensesFleetReport(TestAccountReportsCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.dna_category = cls.env['account.disallowed.expenses.category'].create({
'code': '2345',
'name': 'DNA category',
'rate_ids': [
Command.create({
'date_from': fields.Date.from_string('2022-01-01'),
'rate': 60.0,
'company_id': cls.company_data['company'].id,
}),
Command.create({
'date_from': fields.Date.from_string('2022-04-01'),
'rate': 40.0,
'company_id': cls.company_data['company'].id,
}),
Command.create({
'date_from': fields.Date.from_string('2022-08-01'),
'rate': 23.0,
'company_id': cls.company_data['company'].id,
}),
],
})
cls.company_data['default_account_expense'].disallowed_expenses_category_id = cls.dna_category.id
cls.company_data['default_account_expense_2'] = cls.company_data['default_account_expense'].copy()
cls.company_data['default_account_expense_2'].disallowed_expenses_category_id = cls.dna_category.id
cls.batmobile, cls.batpod = cls.env['fleet.vehicle'].create([
{
'model_id': cls.env['fleet.vehicle.model'].create({
'name': name,
'brand_id': cls.env['fleet.vehicle.model.brand'].create({
'name': 'Wayne Enterprises',
}).id,
'vehicle_type': vehicle_type,
'default_fuel_type': 'hydrogen',
}).id,
'rate_ids': [Command.create({
'date_from': fields.Date.from_string('2022-01-01'),
'rate': rate,
})],
} for name, vehicle_type, rate in [('Batmobile', 'car', 31.0), ('Batpod', 'bike', 56.0)]
])
cls.env['fleet.disallowed.expenses.rate'].create({
'rate': 23.0,
'date_from': '2022-05-01',
'vehicle_id': cls.batmobile.id,
})
bill_1 = cls.env['account.move'].create({
'partner_id': cls.partner_a.id,
'move_type': 'in_invoice',
'date': fields.Date.from_string('2022-01-15'),
'invoice_date': fields.Date.from_string('2022-01-15'),
'invoice_line_ids': [
Command.create({
'name': 'Test',
'quantity': 1,
'price_unit': 100.0,
'tax_ids': [Command.set(cls.company_data['default_tax_purchase'].ids)],
'account_id': cls.company_data['default_account_expense'].id,
}),
Command.create({
'vehicle_id': cls.batmobile.id,
'quantity': 1,
'price_unit': 200.0,
'tax_ids': [Command.set(cls.company_data['default_tax_purchase'].ids)],
'account_id': cls.company_data['default_account_expense'].id,
}),
Command.create({
'vehicle_id': cls.batpod.id,
'quantity': 1,
'price_unit': 300.0,
'tax_ids': [Command.set(cls.company_data['default_tax_purchase'].ids)],
'account_id': cls.company_data['default_account_expense'].id,
}),
Command.create({
'vehicle_id': cls.batpod.id,
'quantity': 1,
'price_unit': 400.0,
'tax_ids': [Command.set(cls.company_data['default_tax_purchase'].ids)],
'account_id': cls.company_data['default_account_expense_2'].id,
}),
],
})
bill_2 = cls.env['account.move'].create({
'partner_id': cls.partner_a.id,
'move_type': 'in_invoice',
'date': fields.Date.from_string('2022-05-15'),
'invoice_date': fields.Date.from_string('2022-05-15'),
'invoice_line_ids': [
Command.create({
'name': 'Test',
'quantity': 1,
'price_unit': 400.0,
'tax_ids': [Command.set(cls.company_data['default_tax_purchase'].ids)],
'account_id': cls.company_data['default_account_expense'].id,
}),
Command.create({
'vehicle_id': cls.batmobile.id,
'quantity': 1,
'price_unit': 500.0,
'tax_ids': [Command.set(cls.company_data['default_tax_purchase'].ids)],
'account_id': cls.company_data['default_account_expense_2'].id,
}),
Command.create({
'vehicle_id': cls.batmobile.id,
'quantity': 1,
'price_unit': 600.0,
'tax_ids': [Command.set(cls.company_data['default_tax_purchase'].ids)],
'account_id': cls.company_data['default_account_expense'].id,
}),
],
})
bill_3 = cls.env['account.move'].create({
'partner_id': cls.partner_a.id,
'move_type': 'in_invoice',
'date': fields.Date.from_string('2022-08-15'),
'invoice_date': fields.Date.from_string('2022-08-15'),
'invoice_line_ids': [
Command.create({
'name': 'Test',
'quantity': 1,
'price_unit': 700.0,
'tax_ids': [Command.set(cls.company_data['default_tax_purchase'].ids)],
'account_id': cls.company_data['default_account_expense'].id,
}),
],
})
(bill_1 + bill_2 + bill_3).action_post()
def _setup_base_report(self, unfold=False, split=False):
report = self.env.ref('odex30_account_disallowed_expenses.disallowed_expenses_report')
default_options = {'unfold_all': unfold, 'vehicle_split': split}
options = self._generate_options(report, '2022-01-01', '2022-12-31', default_options)
self.env.company.totals_below_sections = False
return report, options
def _prepare_column_values(self, lines):
for line in lines:
line['name'] = line['name'].split(' \u2022 ')[0]
line['columns'].append({'name': line['level']})
def test_disallowed_expenses_report_unfold_all(self):
report, options = self._setup_base_report(unfold=True)
lines = report._get_lines(options)
self._prepare_column_values(lines)
self.assertLinesValues(
lines,
[ 0, 1, 2, 3, 4],
[
('2345 DNA category', 3200.0, '', 1088.0, 1),
('600000 Expenses', 2300.0, '', 749.0, 2),
('600000 Expenses', 700.0, '23.00%', 161.0, 3),
('600000 Expenses', 600.0, '23.00%', 138.0, 3),
('600000 Expenses', 400.0, '40.00%', 160.0, 3),
('600000 Expenses', 200.0, '31.00%', 62.0, 3),
('600000 Expenses', 300.0, '56.00%', 168.0, 3),
('600000 Expenses', 100.0, '60.00%', 60.0, 3),
('600002 Expenses (copy)', 900.0, '', 339.0, 2),
('600002 Expenses (copy)', 500.0, '23.00%', 115.0, 3),
('600002 Expenses (copy)', 400.0, '56.00%', 224.0, 3),
('Total', 3200.0, '', 1088.0, 1),
],
options,
)
def test_disallowed_expenses_report_unfold_all_with_vehicle_split(self):
report, options = self._setup_base_report(unfold=True, split=True)
lines = report._get_lines(options)
self._prepare_column_values(lines)
self.assertLinesValues(
lines,
[ 0, 1, 2, 3, 4],
[
('2345 DNA category', 3200.0, '', 1088.0, 1),
('Wayne Enterprises/Batmobile/No Plate', 1300.0, '', 315.0, 2),
('600000 Expenses', 800.0, '', 200.0, 3),
('600000 Expenses', 600.0, '23.00%', 138.0, 4),
('600000 Expenses', 200.0, '31.00%', 62.0, 4),
('600002 Expenses (copy)', 500.0, '23.00%', 115.0, 3),
('600002 Expenses (copy)', 500.0, '23.00%', 115.0, 4),
('Wayne Enterprises/Batpod/No Plate', 700.0, '56.00%', 392.0, 2),
('600000 Expenses', 300.0, '56.00%', 168.0, 3),
('600000 Expenses', 300.0, '56.00%', 168.0, 4),
('600002 Expenses (copy)', 400.0, '56.00%', 224.0, 3),
('600002 Expenses (copy)', 400.0, '56.00%', 224.0, 4),
('600000 Expenses', 1200.0, '', 381.0, 2),
('600000 Expenses', 700.0, '23.00%', 161.0, 3),
('600000 Expenses', 400.0, '40.00%', 160.0, 3),
('600000 Expenses', 100.0, '60.00%', 60.0, 3),
('Total', 3200.0, '', 1088.0, 1),
],
options,
)
def test_disallowed_expenses_report_comparison(self):
report, options = self._setup_base_report(unfold=True)
report.filter_period_comparison = True
options = self._update_comparison_filter(options, report, comparison_type='previous_period', number_period=1)
self.mr_freeze_account = self.env['account.account'].create({
'code': '611011',
'name': 'Frozen Account',
})
self.robins_dna = self.env['account.disallowed.expenses.category'].create({
'code': '2346',
'name': 'Robins DNA',
'account_ids': [Command.set(self.mr_freeze_account.id)],
'rate_ids': [
Command.create({
'date_from': fields.Date.from_string('2022-08-01'),
'rate': 50.0,
'company_id': self.company_data['company'].id,
}),
],
})
entry_data = [
('2022-08-15', 21.0),
('2022-07-15', 25.0),
('2021-08-15', 79.0),
]
self.env['account.move'].create([{
'move_type': 'entry',
'date': fields.Date.from_string(entry_date),
'line_ids': [
Command.create({
'account_id': self.mr_freeze_account.id,
'name': 'robin vs mr freeze',
'debit': entry_amount,
}),
Command.create({
'account_id': self.company_data['default_account_revenue'].id,
'name': 'robin vs mr freeze',
'credit': entry_amount,
}),
]
} for entry_date, entry_amount in entry_data]).action_post()
lines = report._get_lines(options)
self._prepare_column_values(lines)
self.assertLinesValues(
lines,
[ 0, 1, 2, 3, 4, 5, 6, 7],
[
('2345 DNA category', 3200.0, '', 1088.0, '', '', '', 1),
('600000 Expenses', 2300.0, '', 749.0, '', '', '', 2),
('600000 Expenses', 700.0, '23.00%', 161.0, '', '', '', 3),
('600000 Expenses', 600.0, '23.00%', 138.0, '', '', '', 3),
('600000 Expenses', 400.0, '40.00%', 160.0, '', '', '', 3),
('600000 Expenses', 200.0, '31.00%', 62.0, '', '', '', 3),
('600000 Expenses', 300.0, '56.00%', 168.0, '', '', '', 3),
('600000 Expenses', 100.0, '60.00%', 60.0, '', '', '', 3),
('600002 Expenses (copy)', 900.0, '', 339.0, '', '', '', 2),
('600002 Expenses (copy)', 500.0, '23.00%', 115.0, '', '', '', 3),
('600002 Expenses (copy)', 400.0, '56.00%', 224.0, '', '', '', 3),
('2346 Robins DNA', 46.0, '', 10.5, 79.0, '', '', 1),
('611011 Frozen Account', 46.0, '', 10.5, 79.0, '', '', 2),
('611011 Frozen Account', 21.0, '50.00%', 10.5, '', '', '', 3),
('611011 Frozen Account', 25.0, '', '', 79.0, '', '', 3),
('Total', 3246.0, '', 1098.5, 79.0, '', 0.0, 1),
],
options,
)
def test_disallowed_expenses_account_id_and_vehicle_id_confusion_regression_test(self):
self.env.cr.execute("SELECT currval('fleet_vehicle_id_seq'), currval('account_account_id_seq')")
fleet_vehicle_max_id, account_account_max_id = self.env.cr.fetchone()
if fleet_vehicle_max_id < account_account_max_id:
self.env.cr.execute("SELECT setval('fleet_vehicle_id_seq', %s)", [account_account_max_id])
elif fleet_vehicle_max_id > account_account_max_id:
self.env.cr.execute("SELECT setval('account_account_id_seq', %s)", [fleet_vehicle_max_id])
self.env.cr.execute("SELECT currval('fleet_vehicle_id_seq'), currval('account_account_id_seq')")
fleet_vehicle_max_id, account_account_max_id = self.env.cr.fetchone()
assert fleet_vehicle_max_id == account_account_max_id, "At this point the current id should be the same"
expense_account = self.company_data['default_account_expense'].copy({
'name': 'Super expense',
'code': '605555',
})
dna_category = self.env['account.disallowed.expenses.category'].create({
'code': 'bob',
'name': 'DNA category',
'rate_ids': [
Command.create({
'date_from': fields.Date.from_string('2022-01-01'),
'rate': 40.0,
}),
],
})
expense_account.disallowed_expenses_category_id = dna_category.id
vehicle = self.batmobile.copy()
assert expense_account.id == vehicle.id, "Those new records need to share the same id to reproduce the issue"
self.env['fleet.disallowed.expenses.rate'].create({
'rate': 25.0,
'date_from': '2022-01-01',
'vehicle_id': vehicle.id,
})
self.env['account.move'].search([('state', '=', 'posted')]).button_draft()
bill = self.env['account.move'].create({
'partner_id': self.partner_a.id,
'move_type': 'in_invoice',
'date': fields.Date.from_string('2022-01-15'),
'invoice_date': fields.Date.from_string('2022-01-15'),
'invoice_line_ids': [
Command.create({
'name': 'Test',
'quantity': 1,
'price_unit': 1000,
'account_id': expense_account.id,
}),
Command.create({
'vehicle_id': vehicle.id,
'quantity': 1,
'price_unit': 100.0,
'account_id': expense_account.id,
}),
],
})
bill.action_post()
# 3) The reporting test
report, options = self._setup_base_report(unfold=True, split=True)
lines = report._get_lines(options)
self._prepare_column_values(lines)
self.assertLinesValues(
# pylint: disable=C0326
lines,
# Name Total Amount Rate Disallowed Amount Level
[ 0, 1, 2, 3, 4],
[
('bob DNA category', 1_100.0, '', 425.0, 1),
('Wayne Enterprises/Batmobile/No Plate', 100.0, '25.00%', 25.0, 2),
('605555 Super expense', 100.0, '25.00%', 25.0, 3),
('605555 Super expense', 100.0, '25.00%', 25.0, 4),
('605555 Super expense', 1_000.0, '40.00%', 400.0, 2),
('605555 Super expense', 1_000.0, '40.00%', 400.0, 3),
('Total', 1_100.0, '', 425.0, 1),
],
options,
)
def test_lines_without_vehicle_should_be_regrouped_by_account(self):
super_expense_account = self.company_data['default_account_expense'].copy({
'name': 'Super expense',
'code': '605555',
})
bob_expense_account = self.company_data['default_account_expense'].copy({
'name': 'bob expense',
'code': '605556',
})
dna_category = self.env['account.disallowed.expenses.category'].create({
'code': 'bob',
'name': 'DNA category',
'rate_ids': [
Command.create({
'date_from': fields.Date.from_string('2022-01-01'),
'rate': 40.0,
}),
],
})
super_expense_account.disallowed_expenses_category_id = dna_category.id
bob_expense_account.disallowed_expenses_category_id = dna_category.id
vehicle = self.batmobile.copy()
self.env['fleet.disallowed.expenses.rate'].create({
'rate': 25.0,
'date_from': '2022-01-01',
'vehicle_id': vehicle.id,
})
# Remove any noise from the report
self.env['account.move'].search([('state', '=', 'posted')]).button_draft()
bill = self.env['account.move'].create({
'partner_id': self.partner_a.id,
'move_type': 'in_invoice',
'date': fields.Date.from_string('2022-01-15'),
'invoice_date': fields.Date.from_string('2022-01-15'),
'invoice_line_ids': [
Command.create({
'name': 'Test',
'quantity': 1,
'price_unit': 1_000,
'account_id': super_expense_account.id,
}),
Command.create({
'vehicle_id': vehicle.id,
'quantity': 1,
'price_unit': 100.0,
'account_id': super_expense_account.id,
}),
Command.create({
'name': 'Test',
'quantity': 1,
'price_unit': 10_000,
'account_id': bob_expense_account.id,
}),
Command.create({
'vehicle_id': vehicle.id,
'quantity': 1,
'price_unit': 100_000.0,
'account_id': bob_expense_account.id,
}),
],
})
bill.action_post()
report, options = self._setup_base_report(unfold=True, split=True)
lines = report._get_lines(options)
self._prepare_column_values(lines)
expected_lines = [
# pylint: disable=C0326
('bob DNA category', 111_100.0, '', 29_425.0, 1),
('Wayne Enterprises/Batmobile/No Plate', 100_100.0, '25.00%', 25_025.0, 2),
('605555 Super expense', 100.0, '25.00%', 25.0, 3),
('605555 Super expense', 100.0, '25.00%', 25.0, 4),
('605556 bob expense', 100_000.0, '25.00%', 25_000.0, 3),
('605556 bob expense', 100_000.0, '25.00%', 25_000.0, 4),
('605555 Super expense', 1_000.0, '40.00%', 400.0, 2),
('605555 Super expense', 1_000.0, '40.00%', 400.0, 3),
('605556 bob expense', 10_000.0, '40.00%', 4_000.0, 2),
('605556 bob expense', 10_000.0, '40.00%', 4_000.0, 3),
('Total', 111_100.0, '', 29_425.0, 1),
]
self.assertLinesValues(
# pylint: disable=C0326
lines,
# Name Total Amount Rate Disallowed Amount Level
[ 0, 1, 2, 3, 4],
expected_lines,
options,
)
# For each report line, ensure that the audited move lines have the same total amount.
for name, amount, _dummy, _dummy, level in expected_lines[:-1]: # 'Total' line can't be audited.
with self.subTest(name=name, amount=amount, level=level):
line_id = next(line['id'] for line in lines if (line['name'], line['columns'][0]['no_format'], line['level']) == (name, amount, level))
action = self.env[report.custom_handler_model_id.model].open_journal_items(options, {'line_id': line_id})
amls = self.env['account.move.line'].search(action['domain'])
self.assertEqual(sum(amls.mapped('balance')), amount, "The sum of the audited move lines should be equal to the amount of the corresponding report line.")

View File

@ -0,0 +1,25 @@
<?xml version='1.0' encoding='utf-8'?>
<odoo>
<record id="view_odex30_account_disallowed_expenses_category_form" model="ir.ui.view">
<field name="name">account.disallowed.expenses.category.form</field>
<field name="model">account.disallowed.expenses.category</field>
<field name="inherit_id" ref="odex30_account_disallowed_expenses.view_odex30_account_disallowed_expenses_category_form"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='left_column']" position="after">
<group name="right_column">
<field name='car_category'/>
</group>
</xpath>
</field>
</record>
<record id="view_odex30_account_disallowed_expenses_category_tree" model="ir.ui.view">
<field name="name">account.disallowed.expenses.category.list</field>
<field name="model">account.disallowed.expenses.category</field>
<field name="inherit_id" ref="odex30_account_disallowed_expenses.view_odex30_account_disallowed_expenses_category_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='account_ids']" position="after">
<field name="car_category" optional="hide" widget="boolean_toggle"/>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,21 @@
<?xml version='1.0' encoding='utf-8'?>
<odoo>
<record id="fleet_vehicle_view_form" model="ir.ui.view">
<field name="name">fleet.vehicle.form</field>
<field name="model">fleet.vehicle</field>
<field name="inherit_id" ref="fleet.fleet_vehicle_view_form"/>
<field name="arch" type="xml">
<group name="fiscality_first_group" position='after'>
<div name="disallowed_expense_rate_history">
<label for="rate_ids">Disallowed Expenses Rate</label>
<field name="rate_ids" nolabel='1' colspan="2">
<list editable="bottom">
<field name="date_from"/>
<field name="rate"/>
</list>
</field>
</div>
</group>
</field>
</record>
</odoo>

View File

@ -0,0 +1,2 @@
from . import models

View File

@ -0,0 +1,23 @@
{
'name' : 'Cash Basis Accounting Reports',
'summary': 'Add cash basis functionality for reports',
'category': 'Odex30-Accounting/Odex30-Accounting',
'description': """
Cash Basis for Accounting Reports
=================================
""",
'author': "Expert Co. Ltd.",
'website': "http://www.exp-sa.com",
'depends': ['odex30_account_reports'],
'data': [
'data/account_reports_data.xml',
'views/account_report_view.xml',
],
'assets': {
'web.assets_backend': [
'odex30_account_reports_cash_basis/static/src/components/**/*',
],
},
'installable': True,
'license': 'OEEL-1',
}

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="odex30_account_reports.profit_and_loss" model="account.report">
<field name="filter_cash_basis" eval="True"/>
</record>
<record id="odex30_account_reports.balance_sheet" model="account.report">
<field name="filter_cash_basis" eval="True"/>
</record>
<record id="odex30_account_reports.executive_summary" model="account.report">
<field name="filter_cash_basis" eval="True"/>
</record>
<record id="odex30_account_reports.general_ledger_report" model="account.report">
<field name="filter_cash_basis" eval="True"/>
</record>
<record id="odex30_account_reports.trial_balance_report" model="account.report">
<field name="filter_cash_basis" eval="True"/>
</record>
<template id="pdf_export_filter_extra_options_template" inherit_id="odex30_account_reports.pdf_export_filter_extra_options_template">
<xpath expr="//div[hasclass('col-3')]" position="attributes">
<attribute name='t-if'> (report.filter_show_draft and options['all_entries']) or
(report.filter_unreconciled and options['unreconciled']) or
(report.filter_cash_basis and options['report_cash_basis']) or
options.get('include_analytic_without_aml') or
options['rounding_unit'] in (k for k, v in options['rounding_unit_names'].items() if v[1])</attribute>
</xpath>
<xpath expr="//div[hasclass('col-9')]//t[@name='include_analytic']" position="after">
<t t-if="report.filter_cash_basis and options['report_cash_basis']">
<t t-set="label_cash_basis">Cash Basis</t>
<t t-set="extra_options" t-value="extra_options + [label_cash_basis]"/>
</t>
</xpath>
</template>
</odoo>

View File

@ -0,0 +1,62 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * odex30_account_reports_cash_basis
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-05 19:38+0000\n"
"PO-Revision-Date: 2026-01-05 19:38+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: odex30_account_reports_cash_basis
#: model:ir.model,name:odex30_account_reports_cash_basis.model_account_report
msgid "Accounting Report"
msgstr ""
#. module: odex30_account_reports_cash_basis
#. odoo-javascript
#: code:addons/odex30_account_reports_cash_basis/static/src/components/cash_basis_report/filters.js:0
msgid "Accrual Basis"
msgstr "على أساس الاستحقاق "
#. module: odex30_account_reports_cash_basis
#. odoo-javascript
#: code:addons/odex30_account_reports_cash_basis/static/src/components/cash_basis_report/filters.js:0
#: model:ir.model.fields,field_description:odex30_account_reports_cash_basis.field_account_report__filter_cash_basis
#: model_terms:ir.ui.view,arch_db:odex30_account_reports_cash_basis.pdf_export_filter_extra_options_template
msgid "Cash Basis"
msgstr "على أساس نقدي "
#. module: odex30_account_reports_cash_basis
#. odoo-javascript
#: code:addons/odex30_account_reports_cash_basis/static/src/components/cash_basis_report/filter_extra_options.xml:0
msgid "Cash Basis Method"
msgstr "طريقة الأساس النقدي"
#. module: odex30_account_reports_cash_basis
#: model:ir.model.fields,help:odex30_account_reports_cash_basis.field_account_report__filter_cash_basis
msgid "Display the option to switch to cash basis mode."
msgstr ""
#. module: odex30_account_reports_cash_basis
#: model:ir.model.fields,field_description:odex30_account_reports_cash_basis.field_account_bank_statement_line__impacting_cash_basis
#: model:ir.model.fields,field_description:odex30_account_reports_cash_basis.field_account_move__impacting_cash_basis
msgid "Impacting Cash Basis"
msgstr ""
#. module: odex30_account_reports_cash_basis
#: model:ir.model,name:odex30_account_reports_cash_basis.model_account_move
msgid "Journal Entry"
msgstr "قيد اليومية"
#. module: odex30_account_reports_cash_basis
#: model:ir.model,name:odex30_account_reports_cash_basis.model_account_move_line
msgid "Journal Item"
msgstr "عنصر اليومية"

View File

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_move
from . import account_move_line
from . import account_report

View File

@ -0,0 +1,41 @@
from odoo import fields, models
from odoo.tools import SQL
class AccountMove(models.Model):
_name = "account.move"
_inherit = "account.move"
impacting_cash_basis = fields.Boolean(store=False, search='_search_impacting_cash_basis')
def _search_impacting_cash_basis(self, operator, value):
sql = SQL("""(
WITH moves_with_receivable_payable AS (
SELECT DISTINCT aml.move_id as id
FROM account_move_line aml
JOIN account_account account ON aml.account_id = account.id
WHERE account.account_type IN ('asset_receivable', 'liability_payable')
),
reconciled_move_on_receivable_payable AS (
SELECT DISTINCT aml.move_id as id
FROM account_partial_reconcile part
JOIN account_move_line aml ON aml.id = part.debit_move_id OR aml.id = part.credit_move_id
JOIN account_account account ON aml.account_id = account.id
WHERE account.account_type IN ('asset_receivable', 'liability_payable')
)
SELECT DISTINCT move.id
FROM account_move move
LEFT JOIN account_journal journal ON journal.id = move.journal_id
LEFT JOIN moves_with_receivable_payable move_rp on move_rp.id = move.id
LEFT JOIN reconciled_move_on_receivable_payable rec_move on rec_move.id = move.id
WHERE
journal.type IN ('cash', 'bank')
OR
move_rp.id IS NULL
OR
rec_move.id IS NOT NULL
)""")
op = 'in' if (operator == '=') ^ (value is False) else 'not in'
return [('id', op, sql)]

View File

@ -0,0 +1,19 @@
from odoo import models
from odoo.tools import SQL
class AccountMoveLine(models.Model):
_name = "account.move.line"
_inherit = "account.move.line"
def _where_calc(self, domain, active_test=True):
query = super()._where_calc(domain, active_test)
if self.env.context.get('account_report_cash_basis'):
self.env['account.report']._prepare_lines_for_cash_basis()
if self.env.context.get('account_report_analytic_groupby'):
self.env['account.report']._prepare_lines_for_analytic_groupby_with_cash_basis()
query._tables['account_move_line'] = SQL.identifier('analytic_cash_basis_temp_account_move_line')
else:
query._tables['account_move_line'] = SQL.identifier('cash_basis_temp_account_move_line')
return query

View File

@ -0,0 +1,214 @@
from odoo import models, fields, api
from odoo.tools import SQL, Query
class AccountReport(models.Model):
_inherit = 'account.report'
filter_cash_basis = fields.Boolean(
string="Cash Basis",
compute=lambda x: x._compute_report_option_filter('filter_cash_basis', False), readonly=False, store=True, depends=['root_report_id', 'section_main_report_ids'],
help="Display the option to switch to cash basis mode."
)
def get_report_information(self, options):
info = super().get_report_information(options)
info['filters']['show_cash_basis'] = self.filter_cash_basis
return info
def _init_options_cash_basis(self, options, previous_options):
if self.filter_cash_basis:
options['report_cash_basis'] = previous_options.get('report_cash_basis', False)
def _init_options_readonly_query(self, options, previous_options):
super()._init_options_readonly_query(options, previous_options)
options['readonly_query'] = options['readonly_query'] and not options.get('report_cash_basis')
@api.model
def _prepare_lines_for_cash_basis(self):
self.env.cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name='cash_basis_temp_account_move_line'")
if self.env.cr.fetchone():
return
self.env.cr.execute("SELECT column_name FROM information_schema.columns WHERE table_name='account_move_line'")
changed_fields = ['date', 'amount_currency', 'amount_residual', 'balance', 'debit', 'credit']
unchanged_fields = list(set(f[0] for f in self.env.cr.fetchall()) - set(changed_fields))
selected_journals = tuple(self.env.context.get('journal_ids', []))
sql = """ -- Create a temporary table
CREATE TEMPORARY TABLE IF NOT EXISTS cash_basis_temp_account_move_line () INHERITS (account_move_line) ON COMMIT DROP;
INSERT INTO cash_basis_temp_account_move_line ({all_fields}) SELECT
{unchanged_fields},
"account_move_line".date,
"account_move_line".amount_currency,
"account_move_line".amount_residual,
"account_move_line".balance,
"account_move_line".debit,
"account_move_line".credit
FROM ONLY account_move_line
WHERE (
"account_move_line".journal_id IN (SELECT id FROM account_journal WHERE type in ('cash', 'bank'))
OR "account_move_line".move_id NOT IN (
SELECT DISTINCT aml.move_id
FROM ONLY account_move_line aml
JOIN account_account account ON aml.account_id = account.id
WHERE account.account_type IN ('asset_receivable', 'liability_payable')
)
)
{where_journals};
WITH payment_table AS (
SELECT
aml.move_id,
aml.account_id,
GREATEST(aml.date, aml2.date) AS date,
CASE WHEN (aml.balance = 0 OR sub_aml.total_per_account = 0)
THEN 0
ELSE part.amount / ABS(sub_aml.total_per_account)
END as matched_percentage,
CASE WHEN (aml.balance = 0 OR sub_aml_2.total_per_move = 0)
THEN 0
ELSE ABS(sub_aml.total_per_account) / ABS(sub_aml_2.total_per_move)
END as move_percentage
FROM account_partial_reconcile part
JOIN ONLY account_move_line aml ON aml.id = part.debit_move_id OR aml.id = part.credit_move_id
JOIN ONLY account_move_line aml2 ON
(aml2.id = part.credit_move_id OR aml2.id = part.debit_move_id)
AND aml.id != aml2.id
JOIN (
SELECT move_id, account_id, SUM(ABS(balance)) AS total_per_account
FROM ONLY account_move_line account_move_line
GROUP BY move_id, account_id
) sub_aml ON (aml.account_id = sub_aml.account_id AND aml.move_id=sub_aml.move_id)
JOIN (
SELECT move_id, SUM(ABS(balance)) AS total_per_move
FROM ONLY account_move_line aml_total
JOIN account_account account_total ON aml_total.account_id = account_total.id
WHERE account_total.account_type IN ('asset_receivable', 'liability_payable')
GROUP BY move_id
) sub_aml_2 ON (aml.move_id = sub_aml_2.move_id)
JOIN account_account account ON aml.account_id = account.id
WHERE account.account_type IN ('asset_receivable', 'liability_payable')
)
INSERT INTO cash_basis_temp_account_move_line ({all_fields}) SELECT
{unchanged_fields},
ref.date,
CASE WHEN "account".id = ref.account_id
THEN ref.matched_percentage * "account_move_line".amount_currency
ELSE ref.matched_percentage * "account_move_line".amount_currency * ref.move_percentage
END,
CASE WHEN "account".id = ref.account_id
THEN ref.matched_percentage * "account_move_line".amount_residual
ELSE ref.matched_percentage * "account_move_line".amount_residual * ref.move_percentage
END,
CASE WHEN "account".id = ref.account_id
THEN ref.matched_percentage * "account_move_line".balance
ELSE ref.matched_percentage * "account_move_line".balance * ref.move_percentage
END,
CASE WHEN "account".id = ref.account_id
THEN ref.matched_percentage * "account_move_line".debit
ELSE ref.matched_percentage * "account_move_line".debit * ref.move_percentage
END,
CASE WHEN "account".id = ref.account_id
THEN ref.matched_percentage * "account_move_line".credit
ELSE ref.matched_percentage * "account_move_line".credit * ref.move_percentage
END
FROM payment_table ref
JOIN ONLY account_move_line account_move_line ON "account_move_line".move_id = ref.move_id
JOIN account_account account ON "account".id = "account_move_line".account_id
WHERE NOT (
"account_move_line".journal_id IN (SELECT id FROM account_journal WHERE type in ('cash', 'bank'))
OR "account_move_line".move_id NOT IN (
SELECT DISTINCT aml.move_id
FROM ONLY account_move_line aml
JOIN account_account account ON aml.account_id = account.id
WHERE account.account_type IN ('asset_receivable', 'liability_payable')
)
)
AND ("account".id = ref.account_id OR "account".account_type NOT IN ('asset_receivable', 'liability_payable'))
{where_journals};
-- Create an composite index to avoid seq.scan
CREATE INDEX IF NOT EXISTS cash_basis_temp_account_move_line_composite_idx on cash_basis_temp_account_move_line(date, journal_id, company_id, parent_state);
-- Update statistics for correct planning
ANALYZE cash_basis_temp_account_move_line;
""".format(
all_fields=', '.join(f'"{f}"' for f in (unchanged_fields + changed_fields)),
unchanged_fields=', '.join([f'"account_move_line"."{f}"' for f in unchanged_fields]),
where_journals=selected_journals and 'AND "account_move_line".journal_id IN %(journal_ids)s' or ''
)
params = {
'journal_ids': selected_journals,
}
self.env.cr.execute(sql, params)
@api.model
def _prepare_lines_for_analytic_groupby_with_cash_basis(self):
self.env.cr.execute(
"SELECT 1 FROM information_schema.tables WHERE table_name='analytic_cash_basis_temp_account_move_line'")
if self.env.cr.fetchone():
return
line_fields = self.env['account.move.line'].fields_get()
self.env.cr.execute("SELECT column_name FROM information_schema.columns WHERE table_name='account_move_line'")
stored_fields = {f[0] for f in self.env.cr.fetchall() if f[0] in line_fields}
changed_equivalence_dict = {
"balance": SQL('CASE WHEN aml.balance != 0 THEN -aal.amount * cash_basis_aml.balance / aml.balance ELSE 0 END'),
"amount_currency": SQL('CASE WHEN aml.amount_currency != 0 THEN -aal.amount * cash_basis_aml.amount_currency / aml.amount_currency ELSE 0 END'),
"amount_residual": SQL('CASE WHEN aml.amount_residual != 0 THEN -aal.amount * cash_basis_aml.amount_residual / aml.amount_residual ELSE 0 END'),
"date": SQL('cash_basis_aml.date'),
"account_id": SQL('aal.general_account_id'),
"partner_id": SQL('aal.partner_id'),
"debit": SQL('CASE WHEN (aml.balance < 0) THEN -aal.amount * cash_basis_aml.balance / aml.balance ELSE 0 END'),
"credit": SQL('CASE WHEN (aml.balance > 0) THEN -aal.amount * cash_basis_aml.balance / aml.balance ELSE 0 END'),
}
selected_fields = []
for fname in stored_fields:
if fname in changed_equivalence_dict:
selected_fields.append(SQL('%s AS %s', changed_equivalence_dict[fname], SQL.identifier(fname)))
elif fname == 'analytic_distribution':
project_plan, other_plans = self.env['account.analytic.plan']._get_all_plans()
analytic_cols = SQL(', ').join(SQL.identifier('aal', n._column_name()) for n in (project_plan+other_plans))
selected_fields.append(SQL('to_jsonb(UNNEST(ARRAY_REMOVE(ARRAY[%s], NULL))) AS "analytic_distribution"', analytic_cols))
else:
selected_fields.append(SQL('aml.%s AS %s', SQL.identifier(fname), SQL.identifier(fname)))
query = SQL(
"""
-- Create a temporary table
CREATE TEMPORARY TABLE IF NOT EXISTS analytic_cash_basis_temp_account_move_line () inherits (account_move_line) ON COMMIT DROP;
INSERT INTO analytic_cash_basis_temp_account_move_line (%s)
SELECT %s
FROM ONLY cash_basis_temp_account_move_line cash_basis_aml
JOIN ONLY account_move_line aml ON aml.id = cash_basis_aml.id
JOIN account_analytic_line aal ON aml.id = aal.move_line_id;
-- Create a supporting index to avoid seq.scans
CREATE INDEX IF NOT EXISTS analytic_cash_basis_temp_account_move_line__composite_idx ON analytic_cash_basis_temp_account_move_line (analytic_distribution, journal_id, date, company_id);
-- Update statistics for correct planning
ANALYZE analytic_cash_basis_temp_account_move_line
""",
SQL(', ').join(SQL.identifier(field_name) for field_name in stored_fields),
SQL(', ').join(selected_fields),
)
self.env.cr.execute(query)
def _get_report_query(self, options, date_scope, domain=None) -> Query:
context_self = self.with_context(account_report_cash_basis=options.get('report_cash_basis'))
return super(AccountReport, context_self)._get_report_query(options, date_scope, domain=domain)
def open_document(self, options, params=None):
action = super().open_document(options, params)
action['context'].pop('cash_basis', '')
return action
def action_audit_cell(self, options, params):
action = super().action_audit_cell(options, params)
if options.get('report_cash_basis') and action['res_model'] == 'account.move.line':
action['domain'].append(('move_id.impacting_cash_basis', '=', True))
return action

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates>
<t t-name="odex30_account_reports_cash_basis.CashBasisReportFilterExtraOptions" t-inherit="odex30_account_reports.AccountReportFilterExtraOptions" t-inherit-mode="extension">
<xpath expr="//DropdownItem[contains(@class, 'filter_show_all_hook')]" position="after">
<t t-if="controller.groups.account_user and controller.filters.show_cash_basis">
<DropdownItem
class="{ 'selected': controller.options.report_cash_basis }"
onSelected="() => this.filterClicked({ optionKey: 'report_cash_basis', reload: true})"
closingMode="'none'"
>
Cash Basis Method
</DropdownItem>
</t>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,23 @@
import { _t } from "@web/core/l10n/translation";
import { patch } from "@web/core/utils/patch";
import { AccountReportFilters } from "@odex30_account_reports/components/account_report/filters/filters";
patch(AccountReportFilters.prototype, {
get selectedExtraOptions() {
let selectedExtraOptionsName = super.selectedExtraOptions;
if (this.controller.filters.show_cash_basis) {
const cashBasisFilterName = this.controller.options.report_cash_basis
? _t("Cash Basis")
: _t("Accrual Basis");
selectedExtraOptionsName = selectedExtraOptionsName
? `${selectedExtraOptionsName}, ${cashBasisFilterName}`
: cashBasisFilterName;
}
return selectedExtraOptionsName;
},
get hasExtraOptionsFilter() {
return super.hasExtraOptionsFilter || this.controller.filters.show_cash_basis;
},
});

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_account_reports_cash_basis

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="account_report_form" model="ir.ui.view">
<field name="name">account.report.form</field>
<field name="model">account.report</field>
<field name="inherit_id" ref="odex30_account_reports.account_report_form"/>
<field name="arch" type="xml">
<field name="filter_period_comparison" position="after">
<field name="filter_cash_basis"/>
</field>
</field>
</record>
</odoo>

View File

@ -0,0 +1,2 @@
from . import models

View File

@ -0,0 +1,25 @@
{
'name': 'Standard Audit File for Tax Base module',
'version': '1.0',
'category': 'Odex30-Accounting/Odex30-Accounting',
'author': "Expert Co. Ltd.",
'website': "http://www.exp-sa.com",
'description': """
Base module for SAF-T reporting
===============================
This is meant to be used with localization specific modules.
""",
'depends': [
'odex30_account_reports'
],
'data': [
'data/saft_report.xml',
],
'assets': {
'web.assets_backend': [
'odex30_account_saft/static/src/components/**/*',
],
},
'license': 'OEEL-1',
}

View File

@ -0,0 +1,255 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="address">
<StreetName t-if="partner_address.street" t-out="partner_address.street[:70]"/>
<AdditionalAddressDetail t-if="partner_address.street2" t-out="partner_address.street2[:70]"/>
<City t-out="partner_address.city"/>
<PostalCode t-out="partner_address.zip"/>
<Country t-if="partner_address.country_id" t-out="partner_address.country_id.code"/>
</template>
<template id="addresses_contacts">
<t t-set="partner_info" t-value="partner_detail_map[partner_id]"/>
<t t-set="partner" t-value="partner_info['partner']"/>
<Name t-out="partner.name[:70]"/>
<Address t-foreach="partner_info['addresses']" t-as="partner_address">
<t t-call="odex30_account_saft.address"/>
</Address>
<Contact t-foreach="partner_info['contacts']" t-as="partner_contact">
<ContactPerson>
<Title t-if="partner_contact.title" t-out="partner_contact.title.name"/>
<FirstName t-out="'NotUsed'"/>
<LastName t-out="partner_contact.name[:35]"/>
</ContactPerson>
<Telephone t-if="partner_contact.phone or partner_contact.mobile" t-out="(partner_contact.phone or partner_contact.mobile)[:18]"/>
<Email t-if="partner_contact.email" t-out="partner_contact.email[:70]"/>
<Website t-if="partner_contact.website" t-out="partner_contact.website"/>
</Contact>
<TaxRegistration t-if="partner.vat">
<TaxRegistrationNumber t-out="partner.vat"/>
</TaxRegistration>
</template>
<template id="company_header">
<RegistrationNumber t-if="company.company_registry" t-out="company.company_registry"/>
<t t-call="odex30_account_saft.addresses_contacts">
<t t-set="partner_id" t-value="company.partner_id.id"/>
</t>
<BankAccount t-foreach="company.bank_ids" t-as="partner_bank">
<t t-if="partner_bank.acc_type == 'iban'">
<IBANNumber t-out="partner_bank.acc_number"/>
</t>
<t t-else="">
<BankAccountNumber t-out="partner_bank.acc_number"/>
<BankAccountName t-if="partner_bank.bank_name" t-out="partner_bank.bank_name[:70]"/>
<SortCode t-if="partner_bank.bank_bic" t-out="partner_bank.bank_bic[:18]"/>
</t>
</BankAccount>
</template>
<template id="line_debit_credit_amount">
<DebitAmount t-if="line_vals['debit']">
<Amount t-out="format_float(line_vals['debit'])"/>
<t t-if="line_vals['currency_id'] != company.currency_id.id">
<CurrencyCode t-out="line_vals['currency_code']"/>
<CurrencyAmount t-out="format_float(abs(line_vals['amount_currency']))"/>
<ExchangeRate t-out="format_float(line_vals['rate'], digits=8)"/>
</t>
</DebitAmount>
<CreditAmount t-if="line_vals['credit']">
<Amount t-out="format_float(line_vals['credit'])"/>
<t t-if="line_vals['currency_id'] != company.currency_id.id">
<CurrencyCode t-out="line_vals['currency_code']"/>
<CurrencyAmount t-out="format_float(abs(line_vals['amount_currency']))"/>
<ExchangeRate t-out="format_float(line_vals['rate'], digits=8)"/>
</t>
</CreditAmount>
<!-- When both debit and credit are 0.0, we still need to display one or the other, depending on the move_type -->
<DebitAmount t-if="not line_vals.get('debit') and not line_vals.get('credit') and line_vals.get('move_type', '') in ('in_invoice', 'out_refund')">
<Amount t-out="format_float(line_vals['debit'])"/>
<t t-if="line_vals['currency_id'] != company.currency_id.id">
<CurrencyCode t-out="line_vals['currency_code']"/>
<CurrencyAmount t-out="format_float(abs(line_vals['amount_currency']))"/>
<ExchangeRate t-out="format_float(line_vals['rate'], digits=8)"/>
</t>
</DebitAmount>
<CreditAmount t-if="not line_vals.get('debit') and not line_vals.get('credit') and line_vals.get('move_type', '') in ('out_invoice', 'in_refund', 'entry')">
<Amount t-out="format_float(line_vals['credit'])"/>
<t t-if="line_vals['currency_id'] != company.currency_id.id">
<CurrencyCode t-out="line_vals['currency_code']"/>
<CurrencyAmount t-out="format_float(abs(line_vals['amount_currency']))"/>
<ExchangeRate t-out="format_float(line_vals['rate'], digits=8)"/>
</t>
</CreditAmount>
</template>
<template id="tax_information">
<TaxCode t-out="tax_vals['tax_id']"/>
<TaxPercentage t-if="tax_vals['tax_amount_type'] == 'percent'" t-out="tax_vals['tax_amount']"/>
<TaxBaseDescription t-out="tax_vals['tax_name'][:70]"/>
<TaxAmount>
<Amount t-out="format_float(sign * tax_vals['amount'])"/>
<t t-if="tax_vals['currency_id'] != company.currency_id.id">
<CurrencyCode t-out="tax_vals['currency_code']"/>
<CurrencyAmount t-out="format_float(sign * tax_vals['amount_currency'])"/>
<ExchangeRate t-out="format_float(tax_vals['rate'], digits=8)"/>
</t>
</TaxAmount>
</template>
<template id="saft_template">
<AuditFile t-attf-xmlns="#{xmlns}">
<Header>
<AuditFileVersion t-out="file_version"/>
<AuditFileCountry t-out="company.account_fiscal_country_id.code"/>
<AuditFileDateCreated t-out="today_str"/>
<SoftwareCompanyName>Odoo SA</SoftwareCompanyName>
<SoftwareID>Odoo</SoftwareID>
<SoftwareVersion t-out="software_version"/>
<Company>
<t t-call="odex30_account_saft.company_header"/>
</Company>
<DefaultCurrencyCode t-out="company.currency_id.name"/>
<SelectionCriteria>
<SelectionStartDate t-out="date_from"/>
<SelectionEndDate t-out="date_to"/>
</SelectionCriteria>
<TaxAccountingBasis t-out="accounting_basis"/>
</Header>
<MasterFiles>
<GeneralLedgerAccounts t-if="account_vals_list">
<Account t-foreach="account_vals_list" t-as="account_vals">
<t t-set="account" t-value="account_vals['account']"/>
<AccountID t-out="account.code"/>
<AccountDescription t-out="account.name[:256]"/>
<StandardAccountID t-out="account.code"/>
<t t-if="account_vals['saft_account_type']">
<AccountType t-esc="account_vals['saft_account_type']"/>
</t>
<t t-if="account_vals['opening_balance'] &lt; 0.0">
<OpeningCreditBalance t-out="format_float(-account_vals['opening_balance'])"/>
</t>
<t t-else="">
<OpeningDebitBalance t-out="format_float(account_vals['opening_balance'])"/>
</t>
<t t-if="account_vals['closing_balance'] &lt; 0.0">
<ClosingCreditBalance t-out="format_float(-account_vals['closing_balance'])"/>
</t>
<t t-else="">
<ClosingDebitBalance t-out="format_float(account_vals['closing_balance'])"/>
</t>
</Account>
</GeneralLedgerAccounts>
<Customers t-if="customer_vals_list">
<Customer t-foreach="customer_vals_list" t-as="partner_vals">
<t t-call="odex30_account_saft.addresses_contacts">
<t t-set="partner_id" t-value="partner_vals['partner'].id"/>
</t>
<CustomerID t-out="partner_vals['partner'].id"/>
<t t-if="partner_vals['opening_balance'] &lt; 0.0">
<OpeningCreditBalance t-out="format_float(-partner_vals['opening_balance'])"/>
</t>
<t t-else="">
<OpeningDebitBalance t-out="format_float(partner_vals['opening_balance'])"/>
</t>
<t t-if="partner_vals['closing_balance'] &lt; 0.0">
<ClosingCreditBalance t-out="format_float(-partner_vals['closing_balance'])"/>
</t>
<t t-else="">
<ClosingDebitBalance t-out="format_float(partner_vals['closing_balance'])"/>
</t>
</Customer>
</Customers>
<Suppliers t-if="supplier_vals_list">
<Supplier t-foreach="supplier_vals_list" t-as="partner_vals">
<t t-call="odex30_account_saft.addresses_contacts">
<t t-set="partner_id" t-value="partner_vals['partner'].id"/>
</t>
<SupplierID t-out="partner_vals['partner'].id"/>
<t t-if="partner_vals['opening_balance'] &lt; 0.0">
<OpeningCreditBalance t-out="format_float(-partner_vals['opening_balance'])"/>
</t>
<t t-else="">
<OpeningDebitBalance t-out="format_float(partner_vals['opening_balance'])"/>
</t>
<t t-if="partner_vals['closing_balance'] &lt; 0.0">
<ClosingCreditBalance t-out="format_float(-partner_vals['closing_balance'])"/>
</t>
<t t-else="">
<ClosingDebitBalance t-out="format_float(partner_vals['closing_balance'])"/>
</t>
</Supplier>
</Suppliers>
<TaxTable t-if="tax_vals_list">
<TaxTableEntry t-foreach="tax_vals_list" t-as="tax_vals">
<TaxCodeDetails>
<TaxCode t-out="tax_vals['id']"/>
<Description t-out="tax_vals['name'][:256]"/>
<t t-if="tax_vals['amount_type'] == 'percent'">
<TaxPercentage t-out="tax_vals['amount']"/>
</t>
<t t-else="">
<FlatTaxRate>
<Amount t-out="tax_vals['amount']"/>
</FlatTaxRate>
</t>
<Country t-out="company.account_fiscal_country_id.code"/>
</TaxCodeDetails>
</TaxTableEntry>
</TaxTable>
<Owners>
<Owner>
<t t-call="odex30_account_saft.company_header"/>
<OwnerID t-out="company.id"/>
</Owner>
</Owners>
</MasterFiles>
<GeneralLedgerEntries t-if="move_vals_list">
<NumberOfEntries t-out="len(move_vals_list)"/>
<TotalDebit t-out="format_float(total_debit_in_period)"/>
<TotalCredit t-out="format_float(total_credit_in_period)"/>
<Journal t-foreach="journal_vals_list" t-as="journal_vals">
<JournalID t-out="journal_vals['code']"/>
<Description t-out="journal_vals['name'][:256]"/>
<Type t-out="journal_vals['type'][:9]"/>
<Transaction t-foreach="journal_vals['move_vals_list']" t-as="move_vals">
<TransactionID t-out="move_vals['name']"/>
<Period t-out="format_date(move_vals['date'], '%m')"/>
<PeriodYear t-out="format_date(move_vals['date'], '%Y')"/>
<TransactionDate t-out="move_vals['date']"/>
<TransactionType t-out="move_vals['type'][:9]"/>
<Description t-out="move_vals['name'][:256]"/>
<SystemEntryDate t-out="format_date(move_vals['create_date'], '%Y-%m-%d')"/>
<GLPostingDate t-out="move_vals['date']"/>
<t t-if="move_vals['partner_id']">
<t t-set="partner_vals" t-value="partner_detail_map[move_vals['partner_id']]"/>
<CustomerID t-if="partner_vals['type'] == 'customer'" t-out="move_vals['partner_id']"/>
<SupplierID t-if="partner_vals['type'] == 'supplier'" t-out="move_vals['partner_id']"/>
</t>
<Line t-foreach="move_vals['line_vals_list']" t-as="line_vals">
<RecordID t-out="line_vals['id']"/>
<AccountID t-out="line_vals['account_code']"/>
<ValueDate t-out="move_vals['date']"/>
<SourceDocumentID t-out="move_vals['id']"/>
<t t-if="line_vals['partner_id']">
<t t-set="partner_vals" t-value="partner_detail_map[line_vals['partner_id']]"/>
<CustomerID t-if="partner_vals['type'] == 'customer'" t-out="line_vals['partner_id']"/>
<SupplierID t-if="partner_vals['type'] == 'supplier'" t-out="line_vals['partner_id']"/>
</t>
<Description t-out="(line_vals['name'] or move_vals['name'])[:256]"/>
<t t-call="odex30_account_saft.line_debit_credit_amount"/>
<TaxInformation t-foreach="line_vals.get('tax_detail_vals_list', [])" t-as="tax_vals">
<t t-set="sign" t-value="-1 if line_vals['credit'] else 1"/>
<t t-call="odex30_account_saft.tax_information"/>
</TaxInformation>
</Line>
</Transaction>
</Journal>
</GeneralLedgerEntries>
</AuditFile>
</template>
</odoo>

View File

@ -0,0 +1,109 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * odex30_account_saft
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-05 20:02+0000\n"
"PO-Revision-Date: 2026-01-05 20:02+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: odex30_account_saft
#. odoo-python
#: code:addons/odex30_account_saft/models/account_general_ledger.py:0
msgid "Define Contact(s)"
msgstr "تحديد جهات الاتصال "
#. module: odex30_account_saft
#: model:ir.model,name:odex30_account_saft.model_account_general_ledger_report_handler
msgid "General Ledger Custom Handler"
msgstr ""
#. module: odex30_account_saft
#. odoo-python
#: code:addons/odex30_account_saft/models/account_general_ledger.py:0
msgid "Missing company details."
msgstr "هناك تفاصيل غير موجودة لدى الشركة. "
#. module: odex30_account_saft
#: model_terms:ir.ui.view,arch_db:odex30_account_saft.saft_template
msgid "Odoo"
msgstr ""
#. module: odex30_account_saft
#: model_terms:ir.ui.view,arch_db:odex30_account_saft.saft_template
msgid "Odoo SA"
msgstr ""
#. module: odex30_account_saft
#. odoo-python
#: code:addons/odex30_account_saft/models/account_general_ledger.py:0
msgid "Partners to be checked"
msgstr "الشركاء الذين يجب التحقق منهم "
#. module: odex30_account_saft
#. odoo-python
#: code:addons/odex30_account_saft/models/account_general_ledger.py:0
msgid "Please define one or more Contacts belonging to your company."
msgstr "يرجى تحديد جهة اتصال واحدة أو أكثر تابعة لشركتك."
#. module: odex30_account_saft
#. odoo-javascript
#: code:addons/odex30_account_saft/static/src/components/general_ledger/filters/warnings.xml:0
msgid "Please define the following for"
msgstr "يرجى تحديد ما يلي لـ"
#. module: odex30_account_saft
#. odoo-python
#: code:addons/odex30_account_saft/models/account_general_ledger.py:0
msgid "SAF-T is only compatible with one column group."
msgstr "ملف التدقيق القياسي للضريبة متوافق فقط مع مجموعة عمود واحدة. "
#. module: odex30_account_saft
#. odoo-python
#: code:addons/odex30_account_saft/models/account_general_ledger.py:0
msgid "Some partners are missing at least one address (Zip/City)."
msgstr "بعض الشركاء ينقصهم عنوان واحد على الأقل (الرمز البريدي/المدينة). "
#. module: odex30_account_saft
#. odoo-python
#: code:addons/odex30_account_saft/models/account_general_ledger.py:0
msgid "View Company Partner"
msgstr "عرض شريك الشركة "
#. module: odex30_account_saft
#. odoo-python
#: code:addons/odex30_account_saft/models/account_general_ledger.py:0
msgid "View Partners"
msgstr "عرض الشركاء "
#. module: odex30_account_saft
#. odoo-python
#: code:addons/odex30_account_saft/models/account_general_ledger.py:0
msgid "the Company ID"
msgstr "معرّف الشركة "
#. module: odex30_account_saft
#. odoo-python
#: code:addons/odex30_account_saft/models/account_general_ledger.py:0
msgid "the city or zip code"
msgstr "المدينة أو الرمز البريدي "
#. module: odex30_account_saft
#. odoo-python
#: code:addons/odex30_account_saft/models/account_general_ledger.py:0
msgid "the phone or mobile number"
msgstr "رقم الهاتف الثابت أو الهاتف المحمول "
#. module: odex30_account_saft
#. odoo-javascript
#: code:addons/odex30_account_saft/static/src/components/general_ledger/filters/warnings.xml:0
msgid "your company"
msgstr "شركتك "

View File

@ -0,0 +1,2 @@
from . import account_general_ledger

View File

@ -0,0 +1,375 @@
from collections import defaultdict
from odoo.exceptions import UserError
from odoo.tools import float_repr, SQL
from odoo import api, fields, models, release, _
class GeneralLedgerCustomHandler(models.AbstractModel):
_inherit = 'account.general.ledger.report.handler'
def _customize_warnings(self, report, options, all_column_groups_expression_totals, warnings):
company = self.env.company
args = []
if not company.company_registry:
args.append(_('the Company ID'))
if not (company.phone or company.mobile):
args.append(_('the phone or mobile number'))
if not (company.zip or company.city):
args.append(_('the city or zip code'))
if args:
warnings['odex30_account_saft.company_data_warning'] = {
'alert_type': 'warning',
'args': _(', ').join(args),
}
def action_fill_company_details(self, options, params):
return {
'type': 'ir.actions.act_window',
'name': _('Missing company details.'),
'res_model': 'res.company',
'views': [(False, 'form')],
'res_id': self.env.company.id,
}
def _saft_get_account_type(self, account_type):
return False
@api.model
def _saft_fill_report_general_ledger_accounts(self, report, options, values):
res = {
'account_vals_list': [],
}
accounts_results = self._query_values(report, options)
rslts_array = tuple((account, res_col_gr[options['single_column_group']]) for account, res_col_gr in accounts_results)
init_bal_res = self._get_initial_balance_values(report, tuple(account.id for account, results in rslts_array), options)
initial_balances_map = {}
initial_balance_gen = ((account, init_bal_dict.get(options['single_column_group'])) for account, init_bal_dict in init_bal_res.values())
for account, initial_balance in initial_balance_gen:
initial_balances_map[account.id] = initial_balance
for account, results in rslts_array:
account_init_bal = initial_balances_map[account.id]
account_un_earn = results.get('unaffected_earnings', {})
account_balance = results.get('sum', {})
opening_balance = account_init_bal.get('balance', 0.0) + account_un_earn.get('balance', 0.0)
closing_balance = account_balance.get('balance', 0.0) + account_un_earn.get('balance', 0.0)
res['account_vals_list'].append({
'account': account,
'account_type': dict(self.env['account.account']._fields['account_type']._description_selection(self.env))[account.account_type],
'saft_account_type': self._saft_get_account_type(account.account_type),
'opening_balance': opening_balance,
'closing_balance': closing_balance,
})
values.update(res)
def _saft_fill_report_general_ledger_entries(self, report, options, values):
res = {
'total_debit_in_period': 0.0,
'total_credit_in_period': 0.0,
'journal_vals_list': [],
'move_vals_list': [],
'tax_detail_per_line_map': {},
}
query = report._get_report_query(options, 'strict_range')
tax_name = self.env['account.tax']._field_to_sql('tax', 'name')
journal_name = self.env['account.journal']._field_to_sql('journal', 'name')
uom_name = self.env['uom.uom']._field_to_sql('uom', 'name')
product_name = self.env['product.template']._field_to_sql('product_template', 'name')
account_code = self.env['account.account']._field_to_sql('account', 'code')
query = SQL(
'''
SELECT
account_move_line.id,
account_move_line.display_type,
account_move_line.date,
account_move_line.name,
account_move_line.account_id,
account_move_line.partner_id,
account_move_line.currency_id,
account_move_line.amount_currency,
account_move_line.debit,
account_move_line.credit,
account_move_line.balance,
account_move_line.tax_line_id,
account_move_line.quantity,
account_move_line.price_unit,
account_move_line.product_id,
account_move_line.product_uom_id,
account_move.id AS move_id,
account_move.name AS move_name,
account_move.move_type AS move_type,
account_move.create_date AS move_create_date,
account_move.invoice_date AS move_invoice_date,
account_move.invoice_origin AS move_invoice_origin,
account_move.statement_line_id AS move_statement_line_id,
tax.id AS tax_id,
%(tax_name)s AS tax_name,
tax.amount AS tax_amount,
tax.amount_type AS tax_amount_type,
journal.id AS journal_id,
journal.code AS journal_code,
%(journal_name)s AS journal_name,
journal.type AS journal_type,
account.account_type AS account_type,
%(account_code)s AS account_code,
currency.name AS currency_code,
%(product_name)s AS product_name,
product.default_code AS product_default_code,
%(uom_name)s AS product_uom_name
FROM %(table_references)s
JOIN account_move ON account_move.id = account_move_line.move_id
JOIN account_journal journal ON journal.id = account_move_line.journal_id
JOIN account_account account ON account.id = account_move_line.account_id
JOIN res_currency currency ON currency.id = account_move_line.currency_id
LEFT JOIN product_product product ON product.id = account_move_line.product_id
LEFT JOIN product_template product_template ON product_template.id = product.product_tmpl_id
LEFT JOIN uom_uom uom ON uom.id = account_move_line.product_uom_id
LEFT JOIN account_tax tax ON tax.id = account_move_line.tax_line_id
WHERE %(search_condition)s
ORDER BY account_move_line.date, account_move_line.id
''',
tax_name=tax_name,
journal_name=journal_name,
account_code=account_code,
product_name=product_name,
uom_name=uom_name,
table_references=query.from_clause,
search_condition=query.where_clause,
)
self._cr.execute(query)
journal_vals_map = {}
move_vals_map = {}
inbound_types = self.env['account.move'].get_inbound_types(include_receipts=True)
while True:
batched_line_vals = self._cr.dictfetchmany(10**4)
if not batched_line_vals:
break
for line_vals in batched_line_vals:
line_vals['rate'] = abs(line_vals['amount_currency']) / abs(line_vals['balance']) if line_vals['balance'] else 1.0
line_vals['tax_detail_vals_list'] = []
journal_vals_map.setdefault(line_vals['journal_id'], {
'id': line_vals['journal_id'],
'name': line_vals['journal_name'],
'code': line_vals['journal_code'],
'type': line_vals['journal_type'],
'move_vals_map': {},
})
journal_vals = journal_vals_map[line_vals['journal_id']]
move_vals = {
'id': line_vals['move_id'],
'name': line_vals['move_name'],
'type': line_vals['move_type'],
'sign': -1 if line_vals['move_type'] in inbound_types else 1,
'invoice_date': line_vals['move_invoice_date'],
'invoice_origin': line_vals['move_invoice_origin'],
'date': line_vals['date'],
'create_date': line_vals['move_create_date'],
'partner_id': line_vals['partner_id'],
'journal_type': line_vals['journal_type'],
'statement_line_id': line_vals['move_statement_line_id'],
'line_vals_list': [],
}
move_vals_map.setdefault(line_vals['move_id'], move_vals)
journal_vals['move_vals_map'].setdefault(line_vals['move_id'], move_vals)
computed_line_name = f"[{line_vals['product_default_code']}] {line_vals['product_name']}" if line_vals['product_default_code'] else line_vals['product_name'] or ''
line_vals['name'] = computed_line_name if not line_vals['name'] else line_vals['name']
move_vals = move_vals_map[line_vals['move_id']]
move_vals['line_vals_list'].append(line_vals)
# Track the total debit/period of the whole period.
res['total_debit_in_period'] += line_vals['debit']
res['total_credit_in_period'] += line_vals['credit']
res['tax_detail_per_line_map'][line_vals['id']] = line_vals
# Fill 'journal_vals_list'.
for journal_vals in journal_vals_map.values():
journal_vals['move_vals_list'] = list(journal_vals.pop('move_vals_map').values())
res['journal_vals_list'].append(journal_vals)
res['move_vals_list'] += journal_vals['move_vals_list']
values.update(res)
@api.model
def _saft_fill_report_tax_details_values(self, report, options, values):
tax_vals_map = {}
query = report._get_report_query(options, 'strict_range')
tax_details_query = self.env['account.move.line']._get_query_tax_details(query.from_clause, query.where_clause)
tax_name = self.env['account.tax']._field_to_sql('tax', 'name')
tax_description = self.env['account.tax']._field_to_sql('tax', 'description')
self._cr.execute(SQL('''
SELECT
tax_detail.base_line_id,
tax_line.currency_id,
tax.id AS tax_id,
tax.type_tax_use AS tax_type,
tax.amount_type AS tax_amount_type,
%(tax_name)s AS tax_name,
%(tax_description)s AS tax_description,
tax.amount AS tax_amount,
tax.create_date AS tax_create_date,
SUM(tax_detail.tax_amount) AS amount,
SUM(tax_detail.tax_amount) AS amount_currency
FROM (%(tax_details_query)s) AS tax_detail
JOIN account_move_line tax_line ON tax_line.id = tax_detail.tax_line_id
JOIN account_tax tax ON tax.id = tax_detail.tax_id
WHERE SIGN(tax_detail.tax_amount) = SIGN(tax_detail.base_amount)
GROUP BY tax_detail.base_line_id, tax_line.currency_id, tax.id
''', tax_name=tax_name, tax_description=tax_description, tax_details_query=tax_details_query))
for tax_vals in self._cr.dictfetchall():
line_vals = values['tax_detail_per_line_map'][tax_vals['base_line_id']]
line_vals['tax_detail_vals_list'].append({
**tax_vals,
'rate': line_vals['rate'],
'currency_code': line_vals['currency_code'],
})
tax_vals_map.setdefault(tax_vals['tax_id'], {
'id': tax_vals['tax_id'],
'name': tax_vals['tax_name'],
'description': tax_vals['tax_description'],
'amount': tax_vals['tax_amount'],
'amount_type': tax_vals['tax_amount_type'],
'type': tax_vals['tax_type'],
'create_date': tax_vals['tax_create_date']
})
# Fill 'tax_vals_list'.
values['tax_vals_list'] = list(tax_vals_map.values())
@api.model
def _saft_fill_report_partner_ledger_values(self, report, options, values):
res = {
'customer_vals_list': [],
'supplier_vals_list': [],
'partner_detail_map': defaultdict(lambda: {
'type': False,
'addresses': [],
'contacts': [],
}),
}
# Fill 'customer_vals_list' and 'supplier_vals_list'
query = report._get_report_query(options, 'from_beginning', domain=[
('account_id.account_type', 'in', ('asset_receivable', 'liability_payable')),
('partner_id', '!=', False)
])
query.groupby = SQL.identifier(query.table, "partner_id")
query.having = SQL(
"MIN(date) FILTER (WHERE date >= %(date_from)s AND date <= %(date_to)s) IS NOT NULL",
date_from=options['date']['date_from'],
date_to=options['date']['date_to'],
)
balance_result = self.env.execute_query(query.select(
SQL.identifier(query.table, "partner_id"),
SQL("COALESCE(SUM(balance) FILTER (WHERE date < %s), 0) AS opening_balance", options['date']['date_from']),
SQL("COALESCE(SUM(balance), 0) AS closing_balance"),
))
all_partners = self.env['res.partner'].browse([partner_id for partner_id, *__ in balance_result])
for partner_id, opening_balance, closing_balance in balance_result:
partner = self.env['res.partner'].browse(partner_id).with_prefetch(all_partners._prefetch_ids)
balance = closing_balance - opening_balance
partner_type = 'customer' if balance >= 0.0 else 'supplier'
res['partner_detail_map'][partner_id]['type'] = partner_type
res[partner_type + '_vals_list'].append({
'partner': partner,
'opening_balance': opening_balance,
'closing_balance': closing_balance,
})
# Fill 'partner_detail_map'.
all_partners |= values['company'].partner_id
partner_addresses_map = defaultdict(dict)
partner_contacts_map = defaultdict(lambda: self.env['res.partner'])
def _track_address(current_partner, partner):
if partner.zip and partner.city or (options.get('saft_allow_empty_address') and partner != values['company'].partner_id):
address_key = (partner.zip, partner.city)
partner_addresses_map[current_partner][address_key] = partner
def _track_contact(current_partner, partner):
partner_contacts_map[current_partner] |= partner
for partner in all_partners:
_track_address(partner, partner)
if partner.is_company:
children = partner.child_ids.filtered(lambda p: p.type == 'contact' and p.active and not p.is_company).sorted('id')
if partner == values['company'].partner_id:
if not children:
values['errors']['missing_company_contact'] = {
'message': _('Please define one or more Contacts belonging to your company.'),
'action_text': _('View Company Partner'),
'action': partner._get_records_action(name=_("Define Contact(s)")),
'level': 'danger',
}
for child in children:
_track_contact(partner, child)
elif children:
_track_contact(partner, children[0])
else:
_track_contact(partner, partner)
no_partner_address = self.env['res.partner']
for partner in all_partners:
res['partner_detail_map'][partner.id].update({
'partner': partner,
'addresses': list(partner_addresses_map[partner].values()),
'contacts': partner_contacts_map[partner],
})
if not res['partner_detail_map'][partner.id]['addresses']:
no_partner_address |= partner
if no_partner_address:
values['errors']['missing_partner_zip_city'] = {
'message': _('Some partners are missing at least one address (Zip/City).'),
'action_text': _('View Partners'),
'action': no_partner_address._get_records_action(name=_("Partners to be checked")),
}
values.update(res)
@api.model
def _saft_prepare_report_values(self, report, options):
def format_float(amount, digits=2):
return float_repr(amount or 0.0, precision_digits=digits)
def format_date(date_str, formatter):
date_obj = fields.Date.to_date(date_str)
return date_obj.strftime(formatter)
if len(options["column_groups"]) > 1:
raise UserError(_("SAF-T is only compatible with one column group."))
report._init_currency_table(options)
company = self.env.company
options["single_column_group"] = tuple(options["column_groups"].keys())[0]
template_values = {
'company': company,
'xmlns': '',
'file_version': 'undefined',
'accounting_basis': 'undefined',
'today_str': fields.Date.to_string(fields.Date.context_today(self)),
'software_version': release.version,
'date_from': options['date']['date_from'],
'date_to': options['date']['date_to'],
'format_float': format_float,
'format_date': format_date,
'errors': {},
}
self._saft_fill_report_general_ledger_accounts(report, options, template_values)
self._saft_fill_report_general_ledger_entries(report, options, template_values)
self._saft_fill_report_tax_details_values(report, options, template_values)
self._saft_fill_report_partner_ledger_values(report, options, template_values)
return template_values

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates>
<t t-name="odex30_account_saft.company_data_warning">
Please define the following for
<a type="action" t-on-click="(ev) => controller.reportAction(ev, 'action_fill_company_details', warningParams)">
your company
</a>
:
<t t-out="warningParams.args"/>
.
</t>
</templates>

View File

@ -0,0 +1,2 @@
from . import common

View File

@ -0,0 +1,18 @@
from odoo.addons.odex30_account_reports.tests.common import TestAccountReportsCommon
from odoo.addons.odex30_account_reports.models.account_report import AccountReportFileDownloadException
class TestSaftReport(TestAccountReportsCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
company = cls.company_data['company']
cls.ReportException = AccountReportFileDownloadException
cls.ReportModel = cls.env.ref('odex30_account_reports.general_ledger_report')
cls.ReportHandlerModel = cls.env[cls.ReportModel.custom_handler_model_name]
cls.report_handler = cls.ReportHandlerModel.with_company(company)
def _generate_options(self, date_from='2023-10-01', date_to='2023-10-31'):
return super()._generate_options(self.ReportModel, date_from, date_to)