diff --git a/odex25_accounting/account_financial_report/static/src/css/report.css b/odex25_accounting/account_financial_report/static/src/css/report.css
index 0c9cb741f..b1764add3 100644
--- a/odex25_accounting/account_financial_report/static/src/css/report.css
+++ b/odex25_accounting/account_financial_report/static/src/css/report.css
@@ -1,137 +1,92 @@
-/* @font-face {
- font-family: Amiri;
- src: url('/account_financial_report/static/fonts/Amiri-Regular.ttf') format('opentype');
-} */
-
-
.act_as_table {
display: table !important;
background-color: white;
- /* border-style: solid; */
- /* border-radius: 10px; */
}
-.act_as_row {
+.act_as_row {
display: table-row !important;
page-break-inside: avoid;
}
.act_as_cell {
display: table-cell !important;
page-break-inside: avoid;
- border: solid 1px lightGrey !important;
}
-
-/*.act_as_cell.amount{
- border: solid 1px lightGrey !important;
-}*/
-
.act_as_thead {
display: table-header-group !important;
}
.act_as_tbody {
display: table-row-group !important;
}
-.list_table, .data_table, .totals_table{
+.list_table,
+.data_table,
+.totals_table {
width: 100% !important;
- table-layout: fixed !important;
}
-.act_as_row.labels, .act_as_cell.labels {
- background-color:#875A7B; !important;
- color: white;
+.act_as_row.labels {
+ background-color: #f0f0f0 !important;
}
-.list_table, .data_table, .totals_table, .list_table .act_as_row {
- border-left:1px;
- border-right:1px;
- text-align:center;
- font-size:10px;
- padding-right:3px;
- padding-left:3px;
- padding-top:2px;
- padding-bottom:2px;
- /* border-collapse:collapse; */
+.list_table,
+.data_table,
+.totals_table,
+.list_table .act_as_row {
+ border-left: 0px;
+ border-right: 0px;
+ text-align: center;
+ font-size: 10px;
+ padding-right: 3px;
+ padding-left: 3px;
+ padding-top: 2px;
+ padding-bottom: 2px;
+ border-collapse: collapse;
}
.totals_table {
font-weight: bold;
text-align: center;
}
-.list_table .act_as_row.labels, .list_table .act_as_cell.labels, .list_table .act_as_row.initial_balance, .list_table .act_as_row.lines {
- /* border-color:black !important; */
- /* border-bottom:1px solid lightGrey !important; */
+.list_table .act_as_row.labels,
+.list_table .act_as_row.initial_balance,
+.list_table .act_as_row.lines {
+ border-color: grey !important;
+ border-bottom: 1px solid lightGrey !important;
}
-.data_table .act_as_cell{
- text-align: center;
- padding: 5px;
- text-align: center;
- font-size: 14px !important;
-}
-.data_table .act_as_row{
- transition: all .2s ease-in;
-}
-.data_table .act_as_row:hover{
- background-color: #f8f8f8;
- cursor: pointer;
-}
-.data_table .act_as_row.labels:hover, .data_table .act_as_cell.labels:hover{
- cursor: none;
-}
-.data_table .act_as_row:last-child{
- /* border:none; */
-}
-.data_table .lines .act_as_cell{
- padding: 10px 0 10px 0 !important;
+.data_table .act_as_cell {
+ border: 1px solid lightGrey;
text-align: center;
}
-.data_table .act_as_row .act_as_cell:first-child{
- font-weight: bold;
-}
-.data_table .act_as_row .act_as_cell:nth-child(2){
- text-align: center;
- padding-left: 10px !important;
-}
-.data_table .act_as_row .act_as_cell:last-child{
- /* border-right: none; */
-}
-.data_table .act_as_thead .labels .act_as_cell{
- /* border-style: solid; */
- /* border-bottom: 5px solid; */
-}
-.data_table .act_as_row.labels .act_as_cell:last-child {
- /* border-top-right-radius: 10px; */
-}
-.data_table .act_as_row.labels .act_as_cell:first-child {
- /* border-top-left-radius: 10px; */
-}
-.data_table .act_as_cell, .list_table .act_as_cell, .totals_table .act_as_cell {
+.data_table .act_as_cell,
+.list_table .act_as_cell,
+.totals_table .act_as_cell {
word-wrap: break-word;
}
-.data_table .act_as_row.labels, .totals_table .act_as_row.labels, .act_as_cell.labels{
+.data_table .act_as_row.labels,
+.totals_table .act_as_row.labels {
font-weight: bold;
}
.initial_balance .act_as_cell {
- font-style:italic;
+ font-style: italic;
}
.account_title {
- font-size:14px;
- font-weight:bold;
+ font-size: 11px;
+ font-weight: bold;
}
.account_title.labels {
- background-color:#F0F0F0 !important;
+ background-color: #f0f0f0 !important;
}
.act_as_cell.amount {
- word-wrap:normal;
- text-align:center;
+ word-wrap: normal;
+ text-align: right;
}
.act_as_cell.left {
- text-align:left;
+ text-align: left;
}
.act_as_cell.right {
- text-align:right;
+ text-align: right;
}
-.list_table .act_as_cell{
+/*.list_table .act_as_cell {*/
/* border-right:1px solid lightGrey; uncomment to active column lines */
-}
+/*}*/
.list_table .act_as_cell.first_column {
padding-left: 0px;
-/* border-left:1px solid lightGrey; uncomment to active column lines */
+ /* border-left:1px solid lightGrey; uncomment to active column lines */
}
.overflow_ellipsis {
text-overflow: ellipsis;
@@ -139,7 +94,7 @@
white-space: nowrap;
}
.custom_footer {
- font-size:7px !important;
+ font-size: 7px !important;
}
.page_break {
page-break-inside: avoid;
@@ -150,69 +105,9 @@
}
.o_account_financial_reports_page {
- background-color: @odoo-view-background-color;
- color: @odoo-main-text-color;
padding-top: 10px;
width: 90%;
margin-right: auto;
margin-left: auto;
-}
-
-
-/*New Style*/
-.data_table{
- /* border-radius: 10px !important; */
-}
-
-h4{
- font-weight: bold;
- padding-bottom: 30px !important;
- padding-top: 30px !important;
-}
-
-.act_as_cell {
- text-align: center;
-}
-.act_as_thead {
- /* border-style: solid; */
-}
-
-.act_as_thead .act_as_cell {
- /* border-style: solid; */
-}
-
-.first_column {
- text-align: center;
-}
-
-.splitted_outter{
-
-}
-
-.splitted_inner{
- display: table-cell !important;
-}
-
-.data_lines .act_as_cell{
- border: 1px solid black;
-}
-
-table.bordered_table,table.bordered_table th, table.bordered_table td {
- border: 1px solid black;
- border-collapse: collapse;
- text-align: center;
- color: #000;
- }
-
- table.bordered_table tr {
- height: 30px;
- }
-
-table.bordered_table th{
- background-color: #875A7B;
- color: white;
-}
-
-table.bordered_table td a{
- color: #000;
+ font-family: Helvetica, Arial;
}
diff --git a/odex25_accounting/account_payment_distribution/__init__.py b/odex25_accounting/account_payment_distribution/__init__.py
new file mode 100644
index 000000000..0650744f6
--- /dev/null
+++ b/odex25_accounting/account_payment_distribution/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/odex25_accounting/account_payment_distribution/__manifest__.py b/odex25_accounting/account_payment_distribution/__manifest__.py
new file mode 100644
index 000000000..304d6f503
--- /dev/null
+++ b/odex25_accounting/account_payment_distribution/__manifest__.py
@@ -0,0 +1,16 @@
+{
+ 'name': 'Odex Payment Distribution',
+ 'version': '1.0',
+ 'description': 'Distribute payment on several accounts',
+ 'summary': 'Distribute payment on several accounts',
+ 'author': 'Abdurrahman Saber',
+ 'website': '',
+ 'license': 'LGPL-3',
+ 'category': 'Accounting',
+ 'depends': ['account'],
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'reports/template.xml',
+ 'views/account_payment_views.xml'
+ ],
+}
\ No newline at end of file
diff --git a/odex25_accounting/account_payment_distribution/models/__init__.py b/odex25_accounting/account_payment_distribution/models/__init__.py
new file mode 100644
index 000000000..7d7690bc1
--- /dev/null
+++ b/odex25_accounting/account_payment_distribution/models/__init__.py
@@ -0,0 +1 @@
+from . import account_payment_line, account_payment, account_move
diff --git a/odex25_accounting/account_payment_distribution/models/account_move.py b/odex25_accounting/account_payment_distribution/models/account_move.py
new file mode 100644
index 000000000..5b41d9efb
--- /dev/null
+++ b/odex25_accounting/account_payment_distribution/models/account_move.py
@@ -0,0 +1,10 @@
+from odoo import models, api
+
+class AccountMove(models.Model):
+ _inherit = 'account.move'
+
+ def write(self, vals):
+ res = super().write(vals)
+ if 'line_ids' in vals and self.payment_id:
+ self.payment_id._compute_payment_line_ids()
+ return res
diff --git a/odex25_accounting/account_payment_distribution/models/account_payment.py b/odex25_accounting/account_payment_distribution/models/account_payment.py
new file mode 100644
index 000000000..b7f406dc6
--- /dev/null
+++ b/odex25_accounting/account_payment_distribution/models/account_payment.py
@@ -0,0 +1,164 @@
+from odoo import models, fields, api, _
+from odoo.exceptions import ValidationError
+
+
+class AccountPayment(models.Model):
+ _inherit = 'account.payment'
+
+ payment_line_ids = fields.One2many('account.payment.line', 'payment_id')
+ partner_type = fields.Selection(
+ selection_add=[
+ ('many_entries', 'Many Entries'),
+ ('account', 'Account'),
+ ('employee', 'employee'),
+ ],
+ ondelete={
+ 'many_entries': 'set default',
+ 'account': 'set default',
+ 'employee': 'set default',
+ }
+ )
+
+ sutible_account_ids = fields.Many2many(
+ 'account.account',
+ string="Suitable Accounts",
+ compute='_compute_sutible_account_ids',
+ store=False
+ )
+
+ destination_account_id = fields.Many2one(
+ 'account.account',
+ domain="[('id', 'in', sutible_account_ids)]"
+ )
+
+ @api.depends('partner_type', 'company_id', 'partner_id', 'payment_type')
+ def _compute_sutible_account_ids(self):
+ for r in self:
+ if r.partner_type == 'account':
+ # إظهار جميع حسابات الشركة
+ company_id = r.company_id.id or self.env.company.id
+ domain = [('company_id', '=', company_id)]
+ r.sutible_account_ids = self.env['account.account'].search(domain)
+ elif r.partner_type == 'employee' and r.payment_type == 'outbound' and r.partner_id:
+ # حالة دفع للموظف - استخدام حساب المدين
+ employee_partner = r.partner_id
+ if employee_partner.property_account_receivable_id:
+ r.sutible_account_ids = employee_partner.property_account_receivable_id
+ else:
+ r.sutible_account_ids = self.env['account.account'].browse()
+ elif r.partner_type == 'employee' and r.payment_type == 'inbound' and r.partner_id:
+ # حالة استلام من الموظف - استخدام حساب الدائن
+ employee_partner = r.partner_id
+ if employee_partner.property_account_payable_id:
+ r.sutible_account_ids = employee_partner.property_account_payable_id
+ else:
+ r.sutible_account_ids = self.env['account.account'].browse()
+ elif r.partner_type in ['customer', 'supplier']:
+ # عرض جميع حسابات الشركة (السلوك الافتراضي كما كان من قبل)
+ company_id = r.company_id.id or self.env.company.id
+ domain = [('company_id', '=', company_id)]
+ r.sutible_account_ids = self.env['account.account'].search(domain)
+ else:
+ r.sutible_account_ids = self.env['account.account'].browse()
+ @api.constrains('payment_line_ids')
+ def _check_payment_line_ids(self):
+ for rec in self:
+ if rec.partner_type == 'many_entries' and not rec.payment_line_ids:
+ raise ValidationError(_('At least one distribution line is required for Many Entries partner type'))
+
+ @api.onchange('payment_line_ids')
+ def _compute_payment_amount_from_distribution(self):
+ self.ensure_one()
+ self.amount = sum(self.payment_line_ids.mapped('amount'))
+
+ def _compute_payment_line_ids(self):
+ for rec in self:
+ if rec.partner_type == 'many_entries':
+ lines_vals_list = []
+ total_amount = 0
+
+ for line in rec.move_id.line_ids.filtered(lambda l: l.credit != 0):
+ lines_vals_list.append(self._get_payment_line_vals_from_aml(line))
+ total_amount += line.credit
+
+ rec.write({
+ 'payment_line_ids': [(5, 0)] + [(0, 0, line) for line in lines_vals_list],
+ 'amount': total_amount
+ })
+ else:
+ rec.payment_line_ids = False
+
+ def _get_payment_line_vals_from_aml(self, move_line_id):
+ return {
+ 'account_id': move_line_id.account_id.id,
+ 'label': move_line_id.name,
+ 'partner_id': move_line_id.partner_id.id,
+ 'analytic_account_id': move_line_id.analytic_account_id.id,
+ 'analytic_tag_ids': [(6, 0, move_line_id.analytic_tag_ids.ids)],
+ 'amount': move_line_id.credit
+ }
+
+ def _prepare_move_line_default_vals(self, write_off_line_vals=None):
+ res = super()._prepare_move_line_default_vals(write_off_line_vals)
+
+ if self.partner_type == 'many_entries':
+ counterpart_line = None
+ liquidity_accounts = [
+ self.journal_id.payment_debit_account_id.id,
+ self.journal_id.payment_credit_account_id.id
+ ]
+
+ # Find the counterpart line (the line that is NOT the liquidity line)
+ for line in res:
+ if line.get('account_id') not in liquidity_accounts:
+ counterpart_line = line
+ break
+
+ if not counterpart_line:
+ return res
+
+ res.remove(counterpart_line)
+
+ counterpart_lines = self._prepare_counterpart_lines(counterpart_line)
+ res.extend(counterpart_lines)
+
+ return res
+
+ def _prepare_counterpart_lines(self, counterpart_line):
+ result = []
+ for line in self.payment_line_ids:
+ # Determine debit/credit based on payment type
+ # For inbound payments: debit the accounts (receiving money)
+ # For outbound payments: credit the accounts (paying money)
+ if self.payment_type == 'inbound':
+ line_debit = line.amount
+ line_credit = 0.0
+ amount_currency = line.amount
+ else: # outbound
+ line_debit = 0.0
+ line_credit = line.amount
+ amount_currency = -line.amount
+
+ result.append({
+ 'account_id': line.account_id.id,
+ 'partner_id': line.partner_id.id,
+ 'debit': line_debit,
+ 'credit': line_credit,
+ 'currency_id': self.currency_id.id,
+ 'amount_currency': amount_currency,
+ 'name': line.label or counterpart_line['name'],
+ 'date_maturity': self.date,
+ 'analytic_account_id': line.analytic_account_id.id,
+ 'analytic_tag_ids': [(6, 0, line.analytic_tag_ids.ids)]
+ })
+ return result
+
+ def _prepare_payment_display_name(self):
+ res = super()._prepare_payment_display_name()
+ res.update({
+ 'inbound-many_entries': _('Customer Many Entries'),
+ 'outbound-many_entries': _('Vendor Many Entries'),
+ 'inbound-account': _('Customer Account'),
+ 'outbound-account': _('Vendor Account'),
+ })
+ return res
diff --git a/odex25_accounting/account_payment_distribution/models/account_payment_line.py b/odex25_accounting/account_payment_distribution/models/account_payment_line.py
new file mode 100644
index 000000000..227ce6f46
--- /dev/null
+++ b/odex25_accounting/account_payment_distribution/models/account_payment_line.py
@@ -0,0 +1,56 @@
+from odoo import models, fields, api, _
+from odoo.exceptions import ValidationError
+
+class AccountPaymentLine(models.Model):
+ _name = 'account.payment.line'
+ _description = 'Payment Line'
+
+ account_id = fields.Many2one('account.account', required=True, string='Account')
+ payment_id = fields.Many2one('account.payment', required=True, ondelete='cascade')
+ label = fields.Char(string='Label')
+ partner_id = fields.Many2one('res.partner', string='Partner')
+ analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account')
+ analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags')
+ amount = fields.Monetary(currency_field='currency_id', string='Amount', required=True)
+ currency_id = fields.Many2one(related='payment_id.currency_id', readonly=True)
+
+ # حقول إضافية لتحسين UX
+ account_code = fields.Char(related='account_id.code', readonly=True, string='Account Code')
+ account_name = fields.Char(related='account_id.name', readonly=True, string='Account Name')
+ partner_name = fields.Char(related='partner_id.name', readonly=True, string='Partner Name')
+
+ @api.constrains('amount')
+ def _check_amount(self):
+ for rec in self:
+ if rec.amount <= 0:
+ raise ValidationError(_('Line amount must be greater than 0!'))
+
+ @api.onchange('account_id')
+ def _onchange_account_id(self):
+ """تحديد الشريك تلقائياً بناءً على الحساب"""
+ if self.account_id and self.payment_id:
+ # إذا كان الحساب له شريك افتراضي، نستخدمه
+ if hasattr(self.account_id, 'partner_id') and self.account_id.partner_id:
+ self.partner_id = self.account_id.partner_id
+ elif self.payment_id.partner_id:
+ # وإلا نستخدم شريك الدفع
+ self.partner_id = self.payment_id.partner_id
+
+ @api.onchange('partner_id')
+ def _onchange_partner_id(self):
+ """تحديث التسمية عند تغيير الشريك"""
+ if self.partner_id and not self.label:
+ payment_type_label = _('Payment to') if self.payment_id.payment_type == 'outbound' else _('Receipt from')
+ self.label = f"{payment_type_label} {self.partner_id.name}"
+
+ def name_get(self):
+ """تحسين عرض السطور في الواجهة"""
+ result = []
+ for record in self:
+ name = f"[{record.account_id.code}] {record.account_id.name}"
+ if record.partner_id:
+ name += f" - {record.partner_id.name}"
+ if record.amount:
+ name += f" ({record.amount:,.2f})"
+ result.append((record.id, name))
+ return result
diff --git a/odex25_accounting/account_payment_distribution/reports/template.xml b/odex25_accounting/account_payment_distribution/reports/template.xml
new file mode 100644
index 000000000..639c5ce34
--- /dev/null
+++ b/odex25_accounting/account_payment_distribution/reports/template.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+ | Invoice Date |
+ Invoice Number |
+ Reference |
+ Original Amount |
+ Amount Paid |
+ Balance |
+
+
+
+
+
+
+
+ |
+ |
+ |
+ |
+
+
+ |
+ |
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/odex25_accounting/account_payment_distribution/security/ir.model.access.csv b/odex25_accounting/account_payment_distribution/security/ir.model.access.csv
new file mode 100644
index 000000000..b67c3c1e9
--- /dev/null
+++ b/odex25_accounting/account_payment_distribution/security/ir.model.access.csv
@@ -0,0 +1,2 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_account_payment_line,account.payment.line,model_account_payment_line,,1,1,1,1
diff --git a/odex25_accounting/account_payment_distribution/views/account_payment_views.xml b/odex25_accounting/account_payment_distribution/views/account_payment_views.xml
new file mode 100644
index 000000000..f8dd75539
--- /dev/null
+++ b/odex25_accounting/account_payment_distribution/views/account_payment_views.xml
@@ -0,0 +1,47 @@
+
+
+
+
+ view.account.payment.form
+ account.payment
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {'readonly': ['|', ('state', '!=', 'draft'),
+ ('partner_type', '=', 'many_entries')]}
+ 1
+
+
+
+
+
+ {'readonly': ['|', '|', ('state', '!=', 'draft'),
+ ('is_internal_transfer', '=', True), ('partner_type', '=', 'many_entries')],
+ 'required': [('partner_type', 'not in', ['many_entries'])],
+ 'invisible': [('partner_type', '=', 'many_entries')]}
+ 0
+
+
+ {'invisible': [('partner_type', '=', 'many_entries')]}
+
+
+
+
+
\ No newline at end of file
diff --git a/odex25_accounting/odex25_account_accountant/models/account_payment.py b/odex25_accounting/odex25_account_accountant/models/account_payment.py
index a4f057239..5c5322750 100644
--- a/odex25_accounting/odex25_account_accountant/models/account_payment.py
+++ b/odex25_accounting/odex25_account_accountant/models/account_payment.py
@@ -1,11 +1,69 @@
# -*- coding: utf-8 -*-
-from odoo import models, fields, _
+from odoo import models, fields, _,api
from odoo.exceptions import UserError
class AccountPayment(models.Model):
_inherit = "account.payment"
+ # partner_type = fields.Selection(
+ # selection_add=[
+ # ('account', 'Account'),
+ # ('employee', 'Employee'),
+ # ('multi_account', 'Multi account'),
+ # ],
+ # ondelete={
+ # 'account': 'set default',
+ # 'employee': 'set default',
+ # 'multi_account': 'set default',
+ # },
+ # tracking=True
+ # )
+ #
+ # destination_account_id = fields.Many2one(
+ # comodel_name='account.account',
+ # string='Destination Account',
+ # store=True, readonly=False,
+ # compute='_compute_destination_account_id',
+ # domain="[('company_id', '=', company_id)]",
+ # check_company=True)
+
+ # @api.depends('journal_id', 'partner_id', 'partner_type', 'is_internal_transfer')
+ # def _compute_destination_account_id(self):
+ # self.destination_account_id = False
+ # for pay in self:
+ # if pay.is_internal_transfer:
+ # pay.destination_account_id = pay.journal_id.company_id.transfer_account_id
+ # elif pay.partner_type == 'customer':
+ # # Receive money from invoice or send money to refund it.
+ # if pay.partner_id:
+ # pay.destination_account_id = pay.partner_id.with_company(
+ # pay.company_id).property_account_receivable_id
+ # else:
+ # pay.destination_account_id = self.env['account.account'].search([
+ # ('company_id', '=', pay.company_id.id),
+ # ('internal_type', '=', 'receivable'),
+ # ('deprecated', '=', False),
+ # ], limit=1)
+ # elif pay.partner_type == 'supplier':
+ # # Send money to pay a bill or receive money to refund it.
+ # if pay.partner_id:
+ # pay.destination_account_id = pay.partner_id.with_company(pay.company_id).property_account_payable_id
+ # else:
+ # pay.destination_account_id = self.env['account.account'].search([
+ # ('company_id', '=', pay.company_id.id),
+ # ('internal_type', '=', 'payable'),
+ # ('deprecated', '=', False),
+ # ], limit=1)
+ #
+ # @api.depends('journal_id', 'partner_id', 'partner_type', 'is_internal_transfer')
+ # def _compute_destination_account_id(self):
+ # for pay in self:
+ # if pay.partner_type == 'account':
+ # pay.destination_account_id = pay.destination_account_id
+ #
+ # return super()._compute_destination_account_id()
+
def action_open_manual_reconciliation_widget(self):
''' Open the manual reconciliation widget for the current payment.
:return: A dictionary representing an action.