mirror of
https://github.com/OCA/pms.git
synced 2025-01-29 00:17:45 +02:00
525 lines
25 KiB
Python
525 lines
25 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import time
|
|
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
|
|
from odoo import api, fields, models, _
|
|
import odoo.addons.decimal_precision as dp
|
|
from odoo.exceptions import UserError, ValidationError
|
|
from datetime import timedelta
|
|
|
|
|
|
class FolioAdvancePaymentInv(models.TransientModel):
|
|
_name = "folio.advance.payment.inv"
|
|
_description = "Folios Advance Payment Invoice"
|
|
|
|
@api.model
|
|
def _get_advance_payment_method(self):
|
|
return 'all'
|
|
|
|
@api.model
|
|
def _default_product_id(self):
|
|
product_id = self.env['ir.config_parameter'].sudo().get_param('sale.default_deposit_product_id')
|
|
return self.env['product.product'].browse(int(product_id))
|
|
|
|
@api.model
|
|
def _get_default_folio(self):
|
|
if self._context.get('default_reservation_id'):
|
|
folio_ids = self._context.get('default_folio_id', [])
|
|
else:
|
|
folio_ids = self._context.get('active_ids', [])
|
|
|
|
folios = self.env['hotel.folio'].browse(folio_ids)
|
|
return folios
|
|
|
|
@api.model
|
|
def _get_default_reservation(self):
|
|
if self._context.get('default_reservation_id'):
|
|
reservations = self.env['hotel.reservation'].browse(self._context.get('active_ids', []))
|
|
else:
|
|
folios = self._get_default_folio()
|
|
reservations = self.env['hotel.reservation']
|
|
for folio in folios:
|
|
reservations |= folio.room_lines
|
|
return reservations
|
|
|
|
@api.model
|
|
def _get_default_partner_invoice(self):
|
|
folios = self._get_default_folio()
|
|
if folios[0].tour_operator_id:
|
|
return folios[0].tour_operator_id
|
|
return folios[0].partner_invoice_id
|
|
|
|
@api.model
|
|
def _default_deposit_account_id(self):
|
|
return self._default_product_id().property_account_income_id
|
|
|
|
@api.model
|
|
def _default_deposit_taxes_id(self):
|
|
return self._default_product_id().taxes_id
|
|
|
|
advance_payment_method = fields.Selection([
|
|
('all', 'Invoiceable lines (deduct down payments)'),
|
|
('percentage', 'Down payment (percentage)'),
|
|
('fixed', 'Down payment (fixed amount)')
|
|
], string='What do you want to invoice?', default=_get_advance_payment_method,
|
|
required=True)
|
|
auto_invoice = fields.Boolean('Auto Payment Invoice',
|
|
default=True,
|
|
help='Automatic validation and link payment to invoice')
|
|
count = fields.Integer(compute='_count', store=True, string='# of Orders')
|
|
folio_ids = fields.Many2many("hotel.folio", string="Folios",
|
|
help="Folios grouped",
|
|
default=_get_default_folio)
|
|
reservation_ids = fields.Many2many("hotel.reservation", string="Rooms",
|
|
help="Folios grouped",
|
|
default=_get_default_reservation)
|
|
group_folios = fields.Boolean('Group Folios')
|
|
partner_invoice_id = fields.Many2one('res.partner',
|
|
string='Invoice Address', required=True,
|
|
default=_get_default_partner_invoice,
|
|
help="Invoice address for current Invoice.")
|
|
line_ids = fields.One2many('line.advance.inv',
|
|
'advance_inv_id',
|
|
string="Invoice Lines")
|
|
#Advance Payment
|
|
product_id = fields.Many2one('product.product', string="Product",
|
|
domain=[('type', '=', 'service')], default=_default_product_id)
|
|
amount = fields.Float('Down Payment Amount',
|
|
digits=dp.get_precision('Account'),
|
|
help="The amount to be invoiced in advance, taxes excluded.")
|
|
deposit_account_id = fields.Many2one("account.account", string="Income Account",
|
|
domain=[('deprecated', '=', False)],
|
|
help="Account used for deposits",
|
|
default=_default_deposit_account_id)
|
|
deposit_taxes_id = fields.Many2many("account.tax", string="Customer Taxes",
|
|
help="Taxes used for deposits",
|
|
default=_default_deposit_taxes_id)
|
|
|
|
@api.depends('folio_ids')
|
|
def _count(self):
|
|
for record in self:
|
|
record.update({'count': len(self.folio_ids)})
|
|
|
|
@api.onchange('advance_payment_method')
|
|
def onchange_advance_payment_method(self):
|
|
if self.advance_payment_method == 'percentage':
|
|
return {'value': {'amount': 0}}
|
|
return {}
|
|
|
|
@api.multi
|
|
def _create_invoice(self, folio, service, amount):
|
|
inv_obj = self.env['account.invoice']
|
|
ir_property_obj = self.env['ir.property']
|
|
|
|
account_id = False
|
|
if self.product_id.id:
|
|
account_id = self.product_id.property_account_income_id.id \
|
|
or self.product_id.categ_id.property_account_income_categ_id.id
|
|
if not account_id:
|
|
inc_acc = ir_property_obj.get('property_account_income_categ_id', 'product.category')
|
|
account_id = folio.fiscal_position_id.map_account(inc_acc).id if inc_acc else False
|
|
if not account_id:
|
|
raise UserError(
|
|
_('There is no income account defined for this product: "%s". You may have to install a chart of account from Accounting app, settings menu.') %
|
|
(self.product_id.name,))
|
|
|
|
if self.amount <= 0.00:
|
|
raise UserError(_('The value of the down payment amount must be positive.'))
|
|
context = {'lang': folio.partner_id.lang}
|
|
if self.advance_payment_method == 'percentage':
|
|
amount = folio.amount_untaxed * self.amount / 100
|
|
name = _("Down payment of %s%%") % (self.amount,)
|
|
else:
|
|
amount = self.amount
|
|
name = _('Down Payment')
|
|
del context
|
|
taxes = self.product_id.taxes_id.filtered(
|
|
lambda r: not folio.company_id or r.company_id == folio.company_id)
|
|
if folio.fiscal_position_id and taxes:
|
|
tax_ids = folio.fiscal_position_id.map_tax(taxes).ids
|
|
else:
|
|
tax_ids = taxes.ids
|
|
|
|
invoice = inv_obj.create({
|
|
'name': folio.client_order_ref or folio.name,
|
|
'origin': folio.name,
|
|
'type': 'out_invoice',
|
|
'reference': False,
|
|
'folio_ids': [(6, 0, [folio.id])],
|
|
'account_id': folio.partner_id.property_account_receivable_id.id,
|
|
'partner_id': folio.partner_invoice_id.id,
|
|
'invoice_line_ids': [(0, 0, {
|
|
'name': name,
|
|
'origin': folio.name,
|
|
'account_id': account_id,
|
|
'price_unit': amount,
|
|
'quantity': 1.0,
|
|
'discount': 0.0,
|
|
'uom_id': self.product_id.uom_id.id,
|
|
'product_id': self.product_id.id,
|
|
'service_ids': [(6, 0, [service.id])],
|
|
'invoice_line_tax_ids': [(6, 0, tax_ids)],
|
|
'account_analytic_id': folio.analytic_account_id.id or False,
|
|
})],
|
|
'currency_id': folio.pricelist_id.currency_id.id,
|
|
'payment_term_id': folio.payment_term_id.id,
|
|
'fiscal_position_id': folio.fiscal_position_id.id \
|
|
or folio.partner_id.property_account_position_id.id,
|
|
'team_id': folio.team_id.id,
|
|
'user_id': folio.user_id.id,
|
|
'comment': folio.note,
|
|
})
|
|
invoice.compute_taxes()
|
|
invoice.message_post_with_view(
|
|
'mail.message_origin_link',
|
|
values={'self': invoice, 'origin': folio},
|
|
subtype_id=self.env.ref('mail.mt_note').id)
|
|
return invoice
|
|
|
|
@api.model
|
|
def _validate_invoices(self, invoice):
|
|
if self.auto_invoice:
|
|
invoice.action_invoice_open()
|
|
payment_ids = self.folio_ids.mapped('payment_ids.id')
|
|
domain = [
|
|
('account_id', '=', invoice.account_id.id),
|
|
('payment_id', 'in', payment_ids),
|
|
('reconciled', '=', False),
|
|
'|', ('amount_residual', '!=', 0.0),
|
|
('amount_residual_currency', '!=', 0.0)
|
|
]
|
|
if invoice.type in ('out_invoice', 'in_refund'):
|
|
domain.extend([('credit', '>', 0), ('debit', '=', 0)])
|
|
type_payment = _('Outstanding credits')
|
|
else:
|
|
domain.extend([('credit', '=', 0), ('debit', '>', 0)])
|
|
type_payment = _('Outstanding debits')
|
|
info = {'title': '', 'outstanding': True, 'content': [], 'invoice_id': invoice.id}
|
|
lines = self.env['account.move.line'].search(domain)
|
|
currency_id = invoice.currency_id
|
|
for line in lines:
|
|
invoice.assign_outstanding_credit(line.id)
|
|
return True
|
|
|
|
@api.multi
|
|
def create_invoices(self):
|
|
inv_obj = self.env['account.invoice']
|
|
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
|
folios = self.folio_ids
|
|
|
|
|
|
if not self.partner_invoice_id or not self.partner_invoice_id.vat:
|
|
vat_error = _("We need the VAT of the customer")
|
|
raise ValidationError(vat_error)
|
|
|
|
if self.advance_payment_method == 'all':
|
|
inv_data = self._prepare_invoice()
|
|
invoice = inv_obj.create(inv_data)
|
|
for line in self.line_ids:
|
|
line.invoice_line_create(invoice.id, line.qty)
|
|
else:
|
|
# Create deposit product if necessary
|
|
if not self.product_id:
|
|
vals = self._prepare_deposit_product()
|
|
self.product_id = self.env['product.product'].sudo().create(vals)
|
|
self.env['ir.config_parameter'].sudo().set_param(
|
|
'sale.default_deposit_product_id', self.product_id.id)
|
|
|
|
service_obj = self.env['hotel.service']
|
|
for folio in folios:
|
|
if self.advance_payment_method == 'percentage':
|
|
amount = folio.amount_untaxed * folio.amount_total / 100
|
|
else:
|
|
amount = self.amount
|
|
if self.product_id.invoice_policy != 'order':
|
|
raise UserError(_('The product used to invoice a down payment should have an invoice policy set to "Ordered quantities". Please update your deposit product to be able to create a deposit invoice.'))
|
|
if self.product_id.type != 'service':
|
|
raise UserError(_("The product used to invoice a down payment should be of type 'Service'. Please use another product or update this product."))
|
|
taxes = self.product_id.taxes_id.filtered(
|
|
lambda r: not folio.company_id or r.company_id == folio.company_id)
|
|
if folio.fiscal_position_id and taxes:
|
|
tax_ids = folio.fiscal_position_id.map_tax(taxes).ids
|
|
else:
|
|
tax_ids = taxes.ids
|
|
context = {'lang': folio.partner_id.lang}
|
|
service_line = service_obj.create({
|
|
'name': _('Advance: %s') % (time.strftime('%m %Y'),),
|
|
'price_unit': amount,
|
|
'product_qty': 0.0,
|
|
'folio_id': folio.id,
|
|
'discount': 0.0,
|
|
'product_uom': self.product_id.uom_id.id,
|
|
'product_id': self.product_id.id,
|
|
'tax_id': [(6, 0, tax_ids)],
|
|
})
|
|
del context
|
|
invoice = self._create_invoice(folio, service_line, amount)
|
|
invoice.compute_taxes()
|
|
if not invoice.invoice_line_ids:
|
|
raise UserError(_('There is no invoiceable line.'))
|
|
# If invoice is negative, do a refund invoice instead
|
|
if invoice.amount_total < 0:
|
|
invoice.type = 'out_refund'
|
|
for line in invoice.invoice_line_ids:
|
|
line.quantity = -line.quantity
|
|
# Use additional field helper function (for account extensions)
|
|
for line in invoice.invoice_line_ids:
|
|
line._set_additional_fields(invoice)
|
|
# Necessary to force computation of taxes. In account_invoice, they are triggered
|
|
# by onchanges, which are not triggered when doing a create.
|
|
invoice.compute_taxes()
|
|
self._validate_invoices(invoice)
|
|
invoice.message_post_with_view('mail.message_origin_link',
|
|
values={'self': invoice, 'origin': folios},
|
|
subtype_id=self.env.ref('mail.mt_note').id)
|
|
if self._context.get('open_invoices', False):
|
|
return folios.open_invoices_folio()
|
|
return {'type': 'ir.actions.act_window_close'}
|
|
|
|
def _prepare_deposit_product(self):
|
|
return {
|
|
'name': 'Down payment',
|
|
'type': 'service',
|
|
'invoice_policy': 'order',
|
|
'property_account_income_id': self.deposit_account_id.id,
|
|
'taxes_id': [(6, 0, self.deposit_taxes_id.ids)],
|
|
}
|
|
|
|
@api.onchange('reservation_ids')
|
|
def prepare_invoice_lines(self):
|
|
vals = [(5,0,0)]
|
|
folios = self.folio_ids
|
|
invoice_lines = {}
|
|
for folio in folios:
|
|
for service in folio.service_ids.filtered(
|
|
lambda x: x.is_board_service == False and \
|
|
x.qty_to_invoice != 0 and \
|
|
(x.ser_room_line.id in self.reservation_ids.ids or \
|
|
not x.ser_room_line.id)):
|
|
invoice_lines[service.id] = {
|
|
'description': service.name,
|
|
'product_id': service.product_id.id,
|
|
'qty': service.qty_to_invoice,
|
|
'discount': service.discount,
|
|
'price_unit': service.price_unit,
|
|
'service_id': service.id,
|
|
}
|
|
for reservation in folio.room_lines.filtered(
|
|
lambda x: x.id in self.reservation_ids.ids and
|
|
x.invoice_status == 'to invoice'):
|
|
board_service = reservation.board_service_room_id
|
|
for day in reservation.reservation_line_ids.filtered(
|
|
lambda x: not x.invoice_line_ids).sorted('date'):
|
|
extra_price = 0
|
|
if board_service:
|
|
services = reservation.service_ids.filtered(
|
|
lambda x: x.is_board_service == True)
|
|
for service in services:
|
|
service_date = day.date
|
|
if service.product_id.consumed_on == 'after':
|
|
service_date = (fields.Date.from_string(day.date) + \
|
|
timedelta(days=1)).strftime(DEFAULT_SERVER_DATE_FORMAT)
|
|
extra_price += service.price_unit * \
|
|
service.service_line_ids.filtered(
|
|
lambda x: x.date == service_date).day_qty
|
|
#group_key: if group by reservation, We no need group by room_type
|
|
group_key = (reservation.id, reservation.room_type_id.id,
|
|
day.price + extra_price, day.discount,
|
|
day.cancel_discount)
|
|
if day.cancel_discount == 100:
|
|
continue
|
|
discount_factor = 1.0
|
|
for discount in [day.discount, day.cancel_discount]:
|
|
discount_factor = (
|
|
discount_factor * ((100.0 - discount) / 100.0))
|
|
final_discount = 100.0 - (discount_factor * 100.0)
|
|
description = folio.name + ' ' + reservation.room_type_id.name + ' (' + \
|
|
reservation.board_service_room_id.hotel_board_service_id.name + ')' \
|
|
if board_service else folio.name + ' ' + reservation.room_type_id.name
|
|
if group_key not in invoice_lines:
|
|
invoice_lines[group_key] = {
|
|
'description': description,
|
|
'reservation_id': reservation.id,
|
|
'room_type_id': reservation.room_type_id,
|
|
'product_id': self.env['product.product'].browse(
|
|
reservation.room_type_id.product_id.id),
|
|
'discount': final_discount,
|
|
'price_unit': day.price + extra_price,
|
|
'reservation_line_ids': [(4, day.id)]
|
|
}
|
|
else:
|
|
invoice_lines[group_key][('reservation_line_ids')].append((4,day.id))
|
|
for group_key in invoice_lines:
|
|
vals.append((0, False, invoice_lines[group_key]))
|
|
self.line_ids = vals
|
|
self.line_ids.onchange_reservation_line_ids()
|
|
|
|
@api.onchange('folio_ids')
|
|
def onchange_folio_ids(self):
|
|
vals = []
|
|
folios = self.folio_ids
|
|
invoice_lines = {}
|
|
reservations = self.env['hotel.reservation']
|
|
services = self.env['hotel.service']
|
|
old_folio_ids = self.reservation_ids.mapped('folio_id.id')
|
|
for folio in folios.filtered(lambda r: r.id not in old_folio_ids):
|
|
folio_reservations = folio.room_lines
|
|
if folio_reservations:
|
|
reservations |= folio_reservations
|
|
self.reservation_ids |= reservations
|
|
self.prepare_invoice_lines()
|
|
|
|
@api.model
|
|
def _prepare_invoice(self):
|
|
"""
|
|
Prepare the dict of values to create the new invoice for a folio. This method may be
|
|
overridden to implement custom invoice generation (making sure to call super() to establish
|
|
a clean extension chain).
|
|
"""
|
|
|
|
journal_id = self.env['account.invoice'].default_get(['journal_id'])['journal_id']
|
|
if not journal_id:
|
|
raise UserError(_('Please define an accounting sales journal for this company.'))
|
|
origin = ' '.join(self.folio_ids.mapped('name'))
|
|
pricelist = self.folio_ids[0].pricelist_id
|
|
currency = self.folio_ids[0].currency_id
|
|
payment_term = self.folio_ids[0].payment_term_id
|
|
fiscal_position = self.folio_ids[0].fiscal_position_id
|
|
company = self.folio_ids[0].company_id
|
|
user = self.folio_ids[0].user_id
|
|
team = self.folio_ids[0].team_id
|
|
# REVIEW: Multi pricelist in folios??
|
|
# for folio in self.folio_ids:
|
|
# if folio.pricelist_id != pricelist:
|
|
# raise UserError(_('All Folios must hace the same pricelist'))
|
|
invoice_vals = {
|
|
'name': self.folio_ids[0].client_order_ref or '',
|
|
'origin': origin,
|
|
'type': 'out_invoice',
|
|
'account_id': self.partner_invoice_id.property_account_receivable_id.id,
|
|
'partner_id': self.partner_invoice_id.id,
|
|
'journal_id': journal_id,
|
|
'currency_id': currency.id,
|
|
'payment_term_id': payment_term.id,
|
|
'fiscal_position_id': fiscal_position.id or self.partner_invoice_id.property_account_position_id.id,
|
|
'company_id': company.id,
|
|
'user_id': user and user.id,
|
|
'team_id': team.id,
|
|
'comment': self.folio_ids[0].note
|
|
}
|
|
return invoice_vals
|
|
|
|
class LineAdvancePaymentInv(models.TransientModel):
|
|
_name = "line.advance.inv"
|
|
_description = "Lines Advance Invoice"
|
|
|
|
room_type_id = fields.Many2one('hotel.room.type')
|
|
product_id = fields.Many2one('product.product', string='Down Payment Product',
|
|
domain=[('type', '=', 'service')])
|
|
qty = fields.Integer('Quantity')
|
|
price_unit = fields.Float('Price Unit')
|
|
price_total = fields.Float('Price Total', compute='_compute_price_total')
|
|
price_tax = fields.Float('Price Tax', compute='_compute_price_total')
|
|
price_subtotal = fields.Float('Price Subtotal',
|
|
compute='_compute_price_total',
|
|
store=True)
|
|
advance_inv_id = fields.Many2one('folio.advance.payment.inv')
|
|
price_room = fields.Float(compute='_compute_price_room')
|
|
discount = fields.Float(
|
|
string='Discount (%)',
|
|
digits=dp.get_precision('Discount'), default=0.0)
|
|
to_invoice = fields.Boolean('To Invoice')
|
|
description = fields.Text('Description')
|
|
description_dates = fields.Text('Range')
|
|
reservation_id = fields.Many2one('hotel.reservation')
|
|
service_id = fields.Many2one('hotel.service')
|
|
folio_id = fields.Many2one('hotel.folio', compute='_compute_folio_id')
|
|
reservation_line_ids = fields.Many2many(
|
|
'hotel.reservation.line',
|
|
string='Reservation Lines')
|
|
|
|
@api.depends('qty', 'price_unit', 'discount')
|
|
def _compute_price_total(self):
|
|
for record in self:
|
|
origin = record.reservation_id if record.reservation_id.id else record.service_id
|
|
amount_line = record.price_unit * record.qty
|
|
if amount_line != 0:
|
|
product = record.product_id
|
|
price = amount_line * (1 - (record.discount or 0.0) * 0.01)
|
|
taxes = origin.tax_ids.compute_all(price, origin.currency_id, 1, product=product)
|
|
record.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'],
|
|
})
|
|
|
|
def _compute_price_room(self):
|
|
for record in self:
|
|
if record.reservation_id:
|
|
record.price_room = record.reservation_line_ids[0].price
|
|
|
|
def _compute_folio_id(self):
|
|
for record in self:
|
|
origin = record.reservation_id if record.reservation_id.id else record.service_id
|
|
record.folio_id = origin.folio_id
|
|
|
|
@api.onchange('reservation_line_ids')
|
|
def onchange_reservation_line_ids(self):
|
|
for record in self:
|
|
if record.reservation_id:
|
|
if not record.reservation_line_ids:
|
|
raise UserError(_('If you want drop the line, use the trash icon'))
|
|
record.qty = len(record.reservation_line_ids)
|
|
record.description_dates = record.reservation_line_ids[0].date + ' - ' + \
|
|
((fields.Date.from_string(record.reservation_line_ids[-1].date)) + \
|
|
timedelta(days=1)).strftime(DEFAULT_SERVER_DATE_FORMAT)
|
|
|
|
@api.multi
|
|
def invoice_line_create(self, invoice_id, qty):
|
|
""" Create an invoice line.
|
|
:param invoice_id: integer
|
|
:param qty: float quantity to invoice
|
|
:returns recordset of account.invoice.line created
|
|
"""
|
|
self.ensure_one()
|
|
invoice_lines = self.env['account.invoice.line']
|
|
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
|
origin = self.reservation_id if self.reservation_id.id else self.service_id
|
|
product = self.product_id
|
|
account = product.property_account_income_id or product.categ_id.property_account_income_categ_id
|
|
if not account:
|
|
raise UserError(_('Please define income account for this product: "%s" (id:%d) - or for its category: "%s".') %
|
|
(product.name, product.id, product.categ_id.name))
|
|
|
|
fpos = self.folio_id.fiscal_position_id or self.folio_id.partner_id.property_account_position_id
|
|
if fpos:
|
|
account = fpos.map_account(account)
|
|
vals = {
|
|
'sequence': origin.sequence,
|
|
'origin': origin.name,
|
|
'account_id': account.id,
|
|
'price_unit': self.price_unit,
|
|
'quantity': self.qty,
|
|
'discount': self.discount,
|
|
'uom_id': product.uom_id.id,
|
|
'product_id': product.id or False,
|
|
'invoice_line_tax_ids': [(6, 0, origin.tax_ids.ids)],
|
|
'account_analytic_id': self.folio_id.analytic_account_id.id,
|
|
'analytic_tag_ids': [(6, 0, origin.analytic_tag_ids.ids)]
|
|
}
|
|
if self.reservation_id:
|
|
vals.update({
|
|
'name': self.description + ' (' + self.description_dates + ')',
|
|
'invoice_id': invoice_id,
|
|
'reservation_ids': [(6, 0, [origin.id])],
|
|
'reservation_line_ids': [(6, 0, self.reservation_line_ids.ids)]
|
|
})
|
|
elif self.service_id:
|
|
vals.update({
|
|
'name': self.description,
|
|
'invoice_id': invoice_id,
|
|
'service_ids': [(6, 0, [origin.id])]
|
|
})
|
|
invoice_lines |= self.env['account.invoice.line'].create(vals)
|
|
return invoice_lines
|