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 @@ + + + + + + + + \ 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.