model
This commit is contained in:
parent
9a432e947e
commit
fcbfb11b56
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
from . import models
|
||||
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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)))
|
||||
|
||||
|
||||
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
from . import test_customer_validation
|
||||
|
||||
|
|
@ -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() # كلها تمر
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 ""
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import account_move
|
||||
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
from . import models
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 ""
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
from . import test_account_fleet_tax_report
|
||||
|
|
@ -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)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
from . import models
|
||||
from . import report
|
||||
|
|
@ -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/**/*',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 "مثال: ضريبة غير قابلة للخصم"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
from . import odex30_account_account
|
||||
from . import odex30_account_disallowed_expenses
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
from . import odex30_account_disallowed_expenses_report
|
||||
|
|
@ -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']
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
from . import models
|
||||
from . import report
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 "تلك الحسابات "
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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}'
|
||||
|
|
@ -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
|
||||
|
|
@ -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'])],
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
},
|
||||
])
|
||||
|
|
@ -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 },
|
||||
])
|
||||
|
|
@ -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'],
|
||||
}
|
||||
])
|
||||
|
|
@ -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.")
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
from . import models
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 "عنصر اليومية"
|
||||
|
|
@ -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
|
||||
|
|
@ -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)]
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
|
@ -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
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
from . import models
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -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'] < 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'] < 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'] < 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'] < 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'] < 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'] < 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>
|
||||
|
|
@ -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 "شركتك "
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
from . import account_general_ledger
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
from . import common
|
||||
|
|
@ -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)
|
||||
Loading…
Reference in New Issue