odex25_standard/odex25_project/project_base/models/project_invoice.py

416 lines
20 KiB
Python

# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
class ProjectInvoice(models.Model):
_name = "project.invoice"
_inherit = ['mail.thread']
_description = "Project Invoice Request"
name = fields.Char(string='Description',tracking=True,)
phase_id = fields.Many2one('project.phase', string="Stage")
invoice_id = fields.Many2one('account.move', string="Invoice")
amount = fields.Float(string="Invoice Amount", compute="compute_amount", store=True,tracking=True,)
to_invoice = fields.Boolean(string="To invoice", default=False)
project_id = fields.Many2one('project.project', string="Project")
sale_order_id = fields.Many2one('sale.order', related='project_id.sale_order_id', store=True)
company_id = fields.Many2one(related='project_id.company_id', string='Company', store=True, readonly=True)
state = fields.Selection([
('draft', 'Draft'),
('confirm', 'Confirmed'),
('request', 'Requested'),
('done', 'Done'),
('cancel', 'Cancelled'),
],tracking=True, string='Status', readonly=True, copy=False, index=True, default='draft')
plan_date = fields.Date(string='Due Date',tracking=True,)
# related to invoice_date
actual_date = fields.Date(string='Issue Date', compute_sudo=True , compute='_compute_indo_invoice_id')
plan_payment_date = fields.Date(string='Payment Date',tracking=True,)
# related date of first payment created from invoice
actual_payment_date = fields.Date(string='Actual Payment date', compute='get_first_payment_date')
project_invline_ids = fields.One2many('project.invoice.line', 'project_invoice_id', string='Lines',
domain=[('is_downpayment', '=', False)])
project_downinv_ids = fields.One2many('project.invoice.line', 'project_invoice_id', string='Lines',
domain=[('is_downpayment', '=', True)])
currency_id = fields.Many2one(related="project_id.currency_id", store=True)
payment_amount = fields.Monetary(string='Paid Amount',store=True, compute="_compute_payment_amount")
residual_amount = fields.Monetary('Remaining Amount',store=True, compute_sudo=True , compute='_compute_indo_invoice_id')
invoice_type = fields.Selection([('project', 'Project'), ('consultant','Consultant'),
('variation_order', 'Variation Order')], string='Invoice Type', default='project')
has_downpayment = fields.Boolean(compute="_check_downpayment")
payment_state = fields.Selection(selection=[
('not_paid', 'Not Paid'),
('in_payment', 'In Payment'),
('paid', 'Paid'),
('partial', 'Partially Paid'),
('reversed', 'Reversed'),
('invoicing_legacy', 'Invoicing App Legacy')],compute_sudo=True , compute='_compute_indo_invoice_id')
project_type = fields.Selection(related='project_id.type')
allowed_internal_user_ids = fields.Many2many('res.users', 'project_invoice_allowed_internal_users_rel',
string="Allowed Internal Users", default=lambda self: self.env.user, domain=[('share', '=', False)])
allowed_portal_user_ids = fields.Many2many('res.users', 'project_invoice_allowed_portal_users_rel', string="Allowed Portal Users", domain=[('share', '=', True)])
@api.depends('invoice_id','invoice_id.invoice_payments_widget','name')
def _compute_indo_invoice_id(self):
for record in self:
record.actual_date = record.invoice_id.invoice_date
record.residual_amount = record.invoice_id.amount_residual
record.payment_state = record.invoice_id.payment_state
@api.depends('project_id', 'project_id.is_down_payment', 'project_downinv_ids')
def _check_downpayment(self):
for rec in self:
rec.has_downpayment = False
if rec.project_downinv_ids:
rec.has_downpayment = True
if rec.project_id and rec.project_id.is_down_payment:
rec.has_downpayment = True
@api.constrains('plan_date', 'plan_payment_date')
def _check_plan_dates(self):
for rec in self:
if rec.plan_date and rec.plan_payment_date:
if rec.plan_payment_date < rec.plan_date:
raise ValidationError(_("Planned Collection date cannot be earlier than Planned Issue date."))
def name_get(self):
result = []
for record in self.sudo():
name = '%s' % (record.name and record.name or record.phase_id.name or '/')
result.append((record.id, name))
return result
@api.onchange('phase_id')
def _onchange_phase(self):
for rec in self:
if not rec.name:
rec.name = rec.phase_id.display_name
def get_first_payment_date(self):
invoice_payments = []
payment = self.env['account.payment'].sudo().search([])
for rec in self:
payment = payment.filtered(lambda x: rec.invoice_id.id in x.reconciled_invoice_ids.ids)
if payment:
payment_date = payment.mapped('date')
rec.actual_payment_date = min(payment_date)
else:
rec.actual_payment_date = False
@api.depends('invoice_id', 'invoice_id.amount_residual', 'invoice_id.invoice_payments_widget','name')
def _compute_payment_amount(self):
for rec in self:
rec.payment_amount = rec.invoice_id.amount_total - rec.residual_amount
def create_invoice(self):
action = self.env["ir.actions.actions"]._for_xml_id("account.action_move_out_invoice_type")
form_view = [(self.env.ref('account.view_move_form').id, 'form')]
# if self.invoice_type == 'project':
# for line in self.project_invline_ids:
# line.order_line_id.phase_qty = line.product_uom_qty
# for line in self.project_downinv_ids:
# line.order_line_id.phase_qty = line.product_uom_qty > 0 and (line.product_uom_qty*-1) or line.product_uom_qty
# invoice_id = self.project_id.sale_order_id.\
# with_context(due_date=self.plan_payment_date, analytic_account_id=self.project_id.analytic_account_id.id).\
# _create_invoices(final=False)
# else:
invoice_vals = self.with_company(self.company_id)._prepare_invoice()
invoice_line_vals = []
for line in self.project_invline_ids:
invoice_line_vals.append(
(0, 0, line._prepare_invoice_line()
))
invoice_vals['invoice_line_ids'] = invoice_line_vals
invoice_id = self.env['account.move'].sudo().with_context(default_move_type='out_invoice').create(invoice_vals)
if abs(sum(self.project_downinv_ids.mapped('price_total'))) > abs(
sum(self.project_invline_ids.mapped('price_total'))):
raise ValidationError(_("Downpayment amount can't be greater than invoiced amount"))
self.invoice_id = invoice_id.id
# self.project_id.purchase_order_id.invoice_id = invoice_id.id
# if self.project_id.type == 'expense' and self.project_id.purchase_order_id:
# self.project_id.purchase_order_id.invoice_ids |= invoice_id
# self.project_id.purchase_order_id.invoice_ids | = [4,invoice_id.id]
self.state = 'done'
user_ids = self.env.ref('account.group_account_manager').users
self.env['mail.message'].create({
'message_type': "notification",
'body': _("Invoice request created for project %s and need your action") % self.project_id.project_no,
'subject': _("Invoice Request "),
'partner_ids': [(6, 0, user_ids.mapped('partner_id').ids)],
'notification_ids': [(0, 0, {'res_partner_id': user.partner_id.id, 'notification_type': 'inbox'})
for user in user_ids if user_ids],
'model': self._name,
'res_id': self.id,
'author_id': self.env.user.partner_id and self.env.user.partner_id.id
})
action['views'] = form_view
action['res_id'] = invoice_id.id
return action
def _prepare_invoice(self):
"""
Prepare the dict of values to create the new invoice for a variation order. This method may be
overridden to implement custom invoice generation (making sure to call super() to establish
a clean extension chain).
"""
invoice_vals = {}
self.ensure_one()
journal = self.env['account.move'].sudo().with_context(default_move_type='out_invoice')._get_default_journal()
if not journal:
raise UserError(_('Please define an accounting sales journal for the company %s (%s).') % (
self.company_id.name, self.company_id.id))
if self.project_id.type == 'revenue':
journal = self.env['account.move'].sudo().with_context(default_move_type='out_invoice')._get_default_journal()
invoice_vals = {
'move_type': 'out_invoice',
'currency_id': self.currency_id.id,
'ref': self.project_id.project_no,
'partner_id': self.invoice_type == 'consultant' and self.project_id.consultant_id.id or self.project_id.partner_id.id ,
'partner_shipping_id': self.project_id.partner_id.id,
'partner_bank_id': self.company_id.partner_id.bank_ids.filtered(
lambda bank: bank.company_id.id in (self.company_id.id, False))[:1].id,
'journal_id': journal.id,
'invoice_line_ids': [],
'company_id': self.company_id.id,
'invoice_date_due': self.plan_payment_date,
}
elif self.project_id.type == 'expense':
journal = self.env['account.move'].sudo().with_context(default_move_type='in_invoice')._get_default_journal()
invoice_vals = {
'move_type': 'in_invoice',
'currency_id': self.currency_id.id,
'ref': self.project_id.project_no,
'partner_id': self.invoice_type == 'consultant' and self.project_id.consultant_id.id or self.project_id.partner_id.id ,
'partner_shipping_id': self.project_id.partner_id.id,
'partner_bank_id': self.company_id.partner_id.bank_ids.filtered(
lambda bank: bank.company_id.id in (self.company_id.id, False))[:1].id,
'journal_id': journal.id,
'invoice_line_ids': [],
'purchase_id': self.invoice_type== 'consultant' and False or self.project_id.purchase_order_id.id ,
'company_id': self.company_id.id,
'invoice_date_due': self.plan_payment_date,
}
return invoice_vals
def action_confirm(self):
self.ensure_one()
self._set_qty_invoiced()
if not self.plan_date:
raise UserError(_("Kindly Enter Planned Issue Date For this Invoice Request"))
self.state = 'confirm'
# for rec in self:
# return rec.message_post(body=f'Invoice Data /: {rec.name},{rec.state}')
def action_request(self):
self.ensure_one()
if self.project_id.status not in ['open']:
raise ValidationError(_("You cannot Request Invoice for Project that is not in Open status!"))
self._set_qty_invoiced()
self.state = 'request'
def _set_qty_invoiced(self):
for rec in self:
for line in rec.project_invline_ids:
line.qty_invoiced = line.order_line_id.qty_invoiced
def action_cancel(self):
self.ensure_one()
self.state = 'cancel'
def action_set_to_draft(self):
self.ensure_one()
if self.invoice_id and self.invoice_id.state!='draft':
raise UserError(_("Kindly The invoice is not in draft state, so it cannot be unlinked."))
if self.invoice_id:
self.invoice_id.sudo().write({'posted_before':False})
self.invoice_id.sudo().unlink()
self.state = 'draft'
def action_get_invoice(self):
self.ensure_one()
view_id = False
if self.invoice_type == 'variation_order':
view_id = self.env.ref('project_base.project_invoice_vo_form_view').id
action_window = {
"type": "ir.actions.act_window",
"res_model": "project.invoice",
"name": "Invoice",
'view_mode': 'form',
'view_id': view_id,
'res_id': self.id,
"context": {"create": False, 'active_id': self.id, 'active_ids': self.ids, 'id': self.id},
"target": 'new',
}
return action_window
@api.depends('project_invline_ids', 'project_invline_ids.product_uom_qty', 'project_downinv_ids',
'project_downinv_ids.product_uom_qty')
def compute_amount(self):
for rec in self:
rec.amount = sum(rec.project_invline_ids.mapped('price_total')) - abs(
sum(rec.project_downinv_ids.mapped('price_total')))
class ProjectInvoiceLine(models.Model):
_name = "project.invoice.line"
_description = "Project Invoice Line"
_rec_name = "product_id"
project_invoice_id = fields.Many2one('project.invoice', string='Project Invoice', required=True, ondelete='cascade',
index=True, copy=False)
product_id = fields.Many2one('product.product', string='Product')
product_uom_qty = fields.Float(string='Percentage', digits='Product Unit of Measure', required=True, default=0.0)
amount = fields.Monetary("Amount")
product_uom = fields.Many2one('uom.uom', string='Unit of Measure', )
price_unit = fields.Float('Unit Price', digits='Project Amount',store=True, compute="get_price_unit_value")
discount = fields.Float(string='Discount (%)', digits='Discount')
price_subtotal = fields.Monetary(string='Subtotal', store=True)
price_tax = fields.Float(string='Total Tax', compute="_compute_amount", store=True)
price_total = fields.Monetary('Total with Taxes', compute="_compute_amount", store=True)
tax_id = fields.Many2many('account.tax', string='Taxes')
currency_id = fields.Many2one('res.currency', string='Currency', related="project_invoice_id.currency_id",
store=True)
order_line_id = fields.Many2one('sale.order.line', string="Sale Line")
is_downpayment = fields.Boolean(related="order_line_id.is_downpayment", string="Is a down payment", store=True)
qty_invoiced = fields.Float(string='Invoiced Quantity', readonly=True, digits='Product Unit of Measure')
name = fields.Char()
account_id = fields.Many2one(comodel_name='account.account',)
@api.depends("project_invoice_id")
def get_price_unit_value(self):
for rec in self:
rec.price_unit = rec.project_invoice_id.project_id.contract_value_untaxed
@api.onchange("price_unit")
def get_project_invoice_id(self):
for rec in self:
rec.product_id = rec.project_invoice_id.project_id.purchase_line_id.product_id
rec.tax_id = rec.project_invoice_id.project_id.purchase_line_id.taxes_id
@api.onchange('amount')
def _onchange_amount(self):
if self.amount:
if self.price_unit > 0:
self.product_uom_qty = self.amount / self.price_unit
else:
self.product_uom_qty = 0
@api.onchange('product_uom_qty')
def _onchange_qty(self):
if self.product_uom_qty:
self.amount = self.product_uom_qty * self.price_unit
@api.onchange('order_line_id')
def _onchange_order_line_id(self):
''' set all invoice line field.
'''
if self.order_line_id:
self.product_id = self.order_line_id.product_id.id
self.product_uom_qty = 1
self.product_uom = self.order_line_id.product_uom.id
self.price_unit = self.project_invoice_id.project_id.sale_order_amount
self.tax_id = self.order_line_id.tax_id.ids
self.qty_invoiced = self.order_line_id.qty_invoiced
@api.depends('product_uom_qty', 'discount', 'price_unit', 'tax_id')
def _compute_amount(self):
"""
Compute the amounts of the IV line.
"""
for line in self:
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
taxes = line.tax_id.compute_all(price, line.currency_id, line.product_uom_qty, product=line.product_id)
line.update({
'price_tax': sum(t.get('amount', 0.0) for t in taxes.get('taxes', [])),
'price_total': taxes['total_included'],
# 'price_subtotal': taxes['total_excluded'],
})
@api.onchange('product_uom_qty','price_unit')
def onchange_price_subtotal(self):
self.price_subtotal = self.price_unit*self.product_uom_qty
@api.onchange('price_subtotal','price_unit')
def onchange_product_uom_qty(self):
if self.price_unit!=0:
self.product_uom_qty = self.price_subtotal/self.price_unit
def _prepare_invoice_line(self, **optional_values):
"""
Prepare the dict of values to create the new invoice line for a project invoice line.
:param qty: float quantity to invoice
:param optional_values: any parameter that should be added to the returned invoice line
"""
self.ensure_one()
# Convert to string to avoid TypeError
name = str(self.name) if self.name else ''
project_name = str(self.project_invoice_id.project_id.name) if self.project_invoice_id and self.project_invoice_id.project_id.name else ''
res = {
'name': name + '/' + project_name,
'account_id': self.project_invoice_id.project_id.category_id.account_id and self.project_invoice_id.project_id.category_id.account_id.id or False,
'product_id': self.product_id.id,
'product_uom_id': self.product_uom.id,
'quantity': self.product_uom_qty,
'discount': self.discount,
'price_unit': self.price_unit,
'tax_ids': [(6, 0, self.tax_id.ids)],
'analytic_account_id': self.project_invoice_id.project_id.analytic_account_id.id}
if self.project_invoice_id.project_id.purchase_order_id and self.project_invoice_id.project_id.type == 'expense' :
res['analytic_account_id'] = self.project_invoice_id.project_id.purchase_line_id.account_analytic_id.id
res['purchase_line_id'] = self.project_invoice_id.project_id.purchase_line_id.id
# res['account_id'] = self.project_invoice_id.project_id.purchase_line_id.id
return res
@api.constrains('product_id', 'product_uom_qty', 'amount')
def check_product_uom_qty(self):
for rec in self.filtered(lambda l: not l.is_downpayment):
total_qty = sum(rec.order_line_id.project_invoiceline_ids.mapped('product_uom_qty'))
total_amount = sum(rec.order_line_id.project_invoiceline_ids.mapped('amount'))
if total_qty > rec.order_line_id.product_uom_qty:
if total_amount > rec.order_line_id.price_subtotal:
raise ValidationError(
_("The total quantities/Amounts of project invoices must be equal quantities/Amounts in contract item: %s ") % (
rec.order_line_id.name))
for rec in self.filtered(lambda l: l.is_downpayment):
total_amount = 0.0
for line in rec.order_line_id.project_invoiceline_ids:
total_amount += abs(line.amount)
if total_amount > rec.order_line_id.price_unit:
raise ValidationError(
_("The total amount of Downpayment must not exceed it's amount in contract item: %s ") % (
rec.order_line_id.name))
class AccountMove(models.Model):
_inherit = "account.move"
project_invoice_id = fields.Many2one('project.project', string='Project Invoice')
sale_order_id = fields.Many2one('sale.order', 'Sale')
class AccountMove(models.Model):
_inherit = "account.move.line"
@api.onchange('product_id')
def onchange_product_downpayment(self):
downpayment_line = self.move_id.sale_order_id.mapped('order_line').filtered(lambda l: l.is_downpayment)
if downpayment_line and self.product_id == downpayment_line.product_id:
self.sale_line_ids = [(4, downpayment_line.id)]