from odoo import api, fields, models, _ from odoo.exceptions import UserError from odoo.tools import float_compare from odoo.tools.misc import format_date, get_lang from collections import defaultdict class AccountMove(models.Model): _inherit = 'account.move' takaful_sponsorship_id = fields.Many2one('takaful.sponsorship', compute='_compute_takaful_sponsorship_id', store=True) is_refund_sponsorship = fields.Boolean(string='Is Refund Sponsorship', default=False) sponsorship_scheduling_line = fields.Many2one('sponsorship.scheduling.line') payment_details_line = fields.Many2one('payment.details.lines') sponsorship_id = fields.Many2one('takaful.sponsorship', string='Sponsorship', readonly=True, ) payment_id = fields.Many2one('account.payment', string='Payment', copy=False) esterdad_id = fields.Many2one('esterdad.wizard') previous_payment_ids = fields.Many2many(related='esterdad_id.payment_ids') sponsorship_cancel_reason = fields.Text(readonly=True) def action_view_esterdad_id(self): self.ensure_one() return { 'type': 'ir.actions.act_window', 'res_model': 'esterdad.wizard', 'view_mode': 'form', 'view_id': False, # or specify a particular form view if you have one 'target': 'current', # or 'new' if you want a popup wizard 'res_id': self.esterdad_id.id, # open this record directly 'context': { 'create': False }, } @api.depends('state') def _compute_takaful_sponsorship_id(self): for move in self: if not move.takaful_sponsorship_id and move.move_type == 'entry': partials = move.line_ids.matched_debit_ids | move.line_ids.matched_credit_ids takaful_sponsorship_ids = (partials.debit_move_id.move_id.takaful_sponsorship_id | partials.credit_move_id.move_id.takaful_sponsorship_id) move.takaful_sponsorship_id = takaful_sponsorship_ids[-1] if takaful_sponsorship_ids else False else: move.takaful_sponsorship_id = move.takaful_sponsorship_id def action_move_line_create(self): ''' Confirm the vouchers given in ids and create the journal entries for each of them ''' res = super(AccountMove, self).action_move_line_create() if res: self.write({'payment_id': self.move_id.line_ids.mapped('payment_id').id}) @api.onchange('payment_journal_id') def on_change_payment_journal_id(self): for rec in self: rec.account_id = rec.payment_journal_id.default_debit_account_id def action_register_payment(self): ''' Override the payment registration behavior. ''' # Perform any custom logic before calling the original method # Call the original method to preserve the default behavior action = super(AccountMove, self).action_register_payment() for move in self: if move.is_refund_sponsorship: # Check if it's a refund # Add any additional custom logic after calling the original method action['context'].update({ 'default_takaful_sponsorship_id': move.takaful_sponsorship_id.id, 'default_journal_id': move.payment_details_line.journal_id.id, 'default_is_refund_sponsorship': True, }) return action @api.depends( 'line_ids.matched_debit_ids.debit_move_id.move_id.payment_id.is_matched', 'line_ids.matched_debit_ids.debit_move_id.move_id.line_ids.amount_residual', 'line_ids.matched_debit_ids.debit_move_id.move_id.line_ids.amount_residual_currency', 'line_ids.matched_credit_ids.credit_move_id.move_id.payment_id.is_matched', 'line_ids.matched_credit_ids.credit_move_id.move_id.line_ids.amount_residual', 'line_ids.matched_credit_ids.credit_move_id.move_id.line_ids.amount_residual_currency', 'line_ids.debit', 'line_ids.credit', 'line_ids.currency_id', 'line_ids.amount_currency', 'line_ids.amount_residual', 'line_ids.amount_residual_currency', 'line_ids.payment_id.state', 'line_ids.full_reconcile_id', 'sponsorship_scheduling_line.status' ) def _compute_amount(self): # Call the original method to preserve Odoo's logic super(AccountMove, self)._compute_amount() for move in self.filtered("takaful_sponsorship_id"): scheduling_amount = move.sponsorship_scheduling_line.amount if move.payment_state == 'paid': # refund_amount = move.invoice_line_ids.price_unit refund_amount = sum(move.invoice_line_ids.mapped("price_unit")) refunded_scheduling_amount = move.sponsorship_scheduling_line.refunded_amount total_refunded_amount = refund_amount + refunded_scheduling_amount if refunded_scheduling_amount == scheduling_amount: move.sponsorship_scheduling_line.status = 'fully_refund' move.sponsorship_scheduling_line.refunded_amount = scheduling_amount move.payment_details_line.is_fully_refund = True elif refunded_scheduling_amount < scheduling_amount: move.sponsorship_scheduling_line.status = 'partial_refund' move.sponsorship_scheduling_line.refunded_amount += refund_amount move.payment_details_line.is_partial_refund = True else: if not isinstance(scheduling_amount, (int, float)): raise UserError( _("Total amount should be less or equal %s") % scheduling_amount ) has_partial_refund = False has_fully_refund = False has_paid = False has_unpaid = False for line in move.takaful_sponsorship_id.sponsorship_scheduling_line_ids: if line.status == "partial_refund": has_partial_refund = True elif line.status == "fully_refund": has_fully_refund = True elif line.status == "paid": has_paid = True elif line.status == "unpaid": has_unpaid = True if has_partial_refund: move.takaful_sponsorship_id.state = 'partial_refund' elif has_fully_refund and not (has_partial_refund and has_paid and has_unpaid): move.takaful_sponsorship_id.state = 'fully_refund' elif has_fully_refund and (has_partial_refund or has_paid or has_unpaid): move.takaful_sponsorship_id.state = 'partial_refund' def _post(self, soft=True): """Post/Validate the documents. Posting the documents will give it a number, and check that the document is complete (some fields might not be required if not posted but are required otherwise). If the journal is locked with a hash table, it will be impossible to change some fields afterwards. :param soft (bool): if True, future documents are not immediately posted, but are set to be auto posted automatically at the set accounting date. Nothing will be performed on those documents before the accounting date. :return Model: the documents that have been posted """ if soft: future_moves = self.filtered(lambda move: move.date > fields.Date.context_today(self)) future_moves.auto_post = True for move in future_moves: msg = _('This move will be posted at the accounting date: %(date)s', date=format_date(self.env, move.date)) move.message_post(body=msg) to_post = self - future_moves else: to_post = self # `user_has_group` won't be bypassed by `sudo()` since it doesn't change the user anymore. # if not self.env.su and not self.env.user.has_group('account.group_account_invoice'): # raise AccessError(_("You don't have the access rights to post an invoice.")) for move in to_post: if move.partner_bank_id and not move.partner_bank_id.active: raise UserError( _("The recipient bank account link to this invoice is archived.\nSo you cannot confirm the invoice.")) if move.state == 'posted': raise UserError(_('The entry %s (id %s) is already posted.') % (move.name, move.id)) if not move.line_ids.filtered(lambda line: not line.display_type): raise UserError(_('You need to add a line before posting.')) if move.auto_post and move.date > fields.Date.context_today(self): date_msg = move.date.strftime(get_lang(self.env).date_format) raise UserError(_("This move is configured to be auto-posted on %s", date_msg)) if not move.partner_id: if move.is_sale_document(): raise UserError( _("The field 'Customer' is required, please complete it to validate the Customer Invoice.")) elif move.is_purchase_document(): raise UserError( _("The field 'Vendor' is required, please complete it to validate the Vendor Bill.")) if move.is_invoice(include_receipts=True) and float_compare(move.amount_total, 0.0, precision_rounding=move.currency_id.rounding) < 0: raise UserError( _("You cannot validate an invoice with a negative total amount. You should create a credit note instead. Use the action menu to transform it into a credit note or refund.")) if move.line_ids.account_id.filtered(lambda account: account.deprecated): raise UserError(_("A line of this move is using a deprecated account, you cannot post it.")) # Handle case when the invoice_date is not set. In that case, the invoice_date is set at today and then, # lines are recomputed accordingly. # /!\ 'check_move_validity' must be there since the dynamic lines will be recomputed outside the 'onchange' # environment. if not move.invoice_date: if move.is_sale_document(include_receipts=True): move.invoice_date = fields.Date.context_today(self) move.with_context(check_move_validity=False)._onchange_invoice_date() elif move.is_purchase_document(include_receipts=True): raise UserError(_("The Bill/Refund date is required to validate this document.")) # When the accounting date is prior to the tax lock date, move it automatically to the next available date. # /!\ 'check_move_validity' must be there since the dynamic lines will be recomputed outside the 'onchange' # environment. if (move.company_id.tax_lock_date and move.date <= move.company_id.tax_lock_date) and ( move.line_ids.tax_ids or move.line_ids.tax_tag_ids): move.date = move._get_accounting_date(move.invoice_date or move.date, True) move.with_context(check_move_validity=False)._onchange_currency() for move in to_post: # Fix inconsistencies that may occure if the OCR has been editing the invoice at the same time of a user. We force the # partner on the lines to be the same as the one on the move, because that's the only one the user can see/edit. wrong_lines = move.is_invoice() and move.line_ids.filtered( lambda aml: aml.partner_id != move.commercial_partner_id and not aml.display_type) if wrong_lines: wrong_lines.partner_id = move.commercial_partner_id.id # Create the analytic lines in batch is faster as it leads to less cache invalidation. to_post.mapped('line_ids').create_analytic_lines() to_post.write({ 'state': 'posted', 'posted_before': True, }) for move in to_post: move.message_subscribe([p.id for p in [move.partner_id] if p not in move.sudo().message_partner_ids]) for move in to_post: if move.is_sale_document() \ and move.journal_id.sale_activity_type_id \ and (move.journal_id.sale_activity_user_id or move.invoice_user_id).id not in ( self.env.ref('base.user_root').id, False): move.activity_schedule( date_deadline=min((date for date in move.line_ids.mapped('date_maturity') if date), default=move.date), activity_type_id=move.journal_id.sale_activity_type_id.id, summary=move.journal_id.sale_activity_note, user_id=move.journal_id.sale_activity_user_id.id or move.invoice_user_id.id, ) customer_count, supplier_count = defaultdict(int), defaultdict(int) for move in to_post: if move.is_sale_document(): customer_count[move.partner_id] += 1 elif move.is_purchase_document(): supplier_count[move.partner_id] += 1 for partner, count in customer_count.items(): (partner | partner.commercial_partner_id)._increase_rank('customer_rank', count) for partner, count in supplier_count.items(): (partner | partner.commercial_partner_id)._increase_rank('supplier_rank', count) # Trigger action for paid invoices in amount is zero to_post.filtered( lambda m: m.is_invoice(include_receipts=True) and m.currency_id.is_zero(m.amount_total) ).action_invoice_paid() # Force balance check since nothing prevents another module to create an incorrect entry. # This is performed at the very end to avoid flushing fields before the whole processing. to_post._check_balanced() return to_post class AccountMoveLines(models.Model): _inherit = 'account.move.line' takaful_sponsorship_id = fields.Many2one('takaful.sponsorship', related='move_id.takaful_sponsorship_id', store=True)