Merge pull request #87 from hootel/pr_wizard_invoice

Pr wizard invoice
This commit is contained in:
Darío Lodeiros
2019-01-27 16:15:10 +01:00
committed by GitHub
14 changed files with 1040 additions and 616 deletions

View File

@@ -31,3 +31,4 @@ from . import hotel_service_line
from . import hotel_board_service
from . import hotel_board_service_room_type_line
from . import hotel_board_service_line
from . import inherited_account_invoice_line

View File

@@ -11,6 +11,8 @@ from dateutil.relativedelta import relativedelta
from odoo.exceptions import except_orm, UserError, ValidationError
from odoo.tools import (
misc,
float_is_zero,
float_compare,
DEFAULT_SERVER_DATETIME_FORMAT,
DEFAULT_SERVER_DATE_FORMAT)
from odoo import models, fields, api, _
@@ -31,9 +33,57 @@ class HotelFolio(models.Model):
# @api.depends('product_id.invoice_policy', 'order_id.state')
def _compute_qty_delivered_updateable(self):
pass
# @api.depends('state', 'order_line.invoice_status')
@api.depends('state', 'room_lines.invoice_status', 'service_ids.invoice_status')
def _get_invoiced(self):
pass
"""
Compute the invoice status of a Folio. Possible statuses:
- no: if the Folio is not in status 'sale' or 'done', we consider that there is nothing to
invoice. This is also the default value if the conditions of no other status is met.
- to invoice: if any Folio line is 'to invoice', the whole Folio is 'to invoice'
- invoiced: if all Folio lines are invoiced, the Folio is invoiced.
- upselling: if all Folio lines are invoiced or upselling, the status is upselling.
The invoice_ids are obtained thanks to the invoice lines of the Folio lines, and we also search
for possible refunds created directly from existing invoices. This is necessary since such a
refund is not directly linked to the Folio.
"""
for folio in self:
invoice_ids = folio.room_lines.mapped('invoice_line_ids').mapped('invoice_id').filtered(lambda r: r.type in ['out_invoice', 'out_refund'])
invoice_ids |= folio.service_ids.mapped('invoice_line_ids').mapped('invoice_id').filtered(lambda r: r.type in ['out_invoice', 'out_refund'])
# Search for invoices which have been 'cancelled' (filter_refund = 'modify' in
# 'account.invoice.refund')
# use like as origin may contains multiple references (e.g. 'SO01, SO02')
refunds = invoice_ids.search([('origin', 'like', folio.name), ('company_id', '=', folio.company_id.id)]).filtered(lambda r: r.type in ['out_invoice', 'out_refund'])
invoice_ids |= refunds.filtered(lambda r: folio.id in r.folio_ids.ids)
# Search for refunds as well
refund_ids = self.env['account.invoice'].browse()
if invoice_ids:
for inv in invoice_ids:
refund_ids += refund_ids.search([('type', '=', 'out_refund'), ('origin', '=', inv.number), ('origin', '!=', False), ('journal_id', '=', inv.journal_id.id)])
# Ignore the status of the deposit product
deposit_product_id = self.env['sale.advance.payment.inv']._default_product_id()
#~ line_invoice_status = [line.invoice_status for line in order.order_line if line.product_id != deposit_product_id]
#~ TODO: REVIEW INVOICE_STATUS
#~ if folio.state not in ('confirm', 'done'):
#~ invoice_status = 'no'
#~ elif any(invoice_status == 'to invoice' for invoice_status in line_invoice_status):
#~ invoice_status = 'to invoice'
#~ elif all(invoice_status == 'invoiced' for invoice_status in line_invoice_status):
#~ invoice_status = 'invoiced'
#~ elif all(invoice_status in ['invoiced', 'upselling'] for invoice_status in line_invoice_status):
#~ invoice_status = 'upselling'
#~ else:
#~ invoice_status = 'no'
folio.update({
'invoice_count': len(set(invoice_ids.ids + refund_ids.ids)),
'invoice_ids': invoice_ids.ids + refund_ids.ids,
#~ 'invoice_status': invoice_status
})
# @api.depends('state', 'product_uom_qty', 'qty_delivered', 'qty_to_invoice', 'qty_invoiced')
def _compute_invoice_status(self):
pass
@@ -44,9 +94,14 @@ class HotelFolio(models.Model):
def _amount_all(self):
pass
@api.model
def _get_default_team(self):
return self.env['crm.team']._get_default_team_id()
#Main Fields--------------------------------------------------------
name = fields.Char('Folio Number', readonly=True, index=True,
default=lambda self: _('New'))
client_order_ref = fields.Char(string='Customer Reference', copy=False)
partner_id = fields.Many2one('res.partner',
track_visibility='onchange')
@@ -61,8 +116,8 @@ class HotelFolio(models.Model):
help="Hotel services detail provide to "
"customer and it will include in "
"main Invoice.")
company_id = fields.Many2one('res.company', 'Company')
company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get('hotel.folio'))
analytic_account_id = fields.Many2one('account.analytic.account', 'Analytic Account', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="The analytic account related to a folio.", copy=False)
currency_id = fields.Many2one('res.currency', related='pricelist_id.currency_id',
string='Currency', readonly=True, required=True)
@@ -87,6 +142,7 @@ class HotelFolio(models.Model):
required=True, readonly=True, index=True,
states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
copy=False, default=fields.Datetime.now)
confirmation_date = fields.Datetime(string='Confirmation Date', readonly=True, index=True, help="Date on which the folio is confirmed.", copy=False)
state = fields.Selection([
('draft', 'Quotation'),
('sent', 'Quotation Sent'),
@@ -111,6 +167,7 @@ class HotelFolio(models.Model):
readonly=True)
return_ids = fields.One2many('payment.return', 'folio_id',
readonly=True)
payment_term_id = fields.Many2one('account.payment.term', string='Payment Terms', oldname='payment_term')
#Amount Fields------------------------------------------------------
pending_amount = fields.Monetary(compute='compute_amount',
@@ -139,8 +196,7 @@ class HotelFolio(models.Model):
compute='_compute_checkin_partner_count')
#Invoice Fields-----------------------------------------------------
hotel_invoice_id = fields.Many2one('account.invoice', 'Invoice')
num_invoices = fields.Integer(compute='_compute_num_invoices')
invoice_count = fields.Integer(compute='_get_invoiced')
invoice_ids = fields.Many2many('account.invoice', string='Invoices',
compute='_get_invoiced', readonly=True, copy=False)
invoice_status = fields.Selection([('upselling', 'Upselling Opportunity'),
@@ -150,12 +206,11 @@ class HotelFolio(models.Model):
string='Invoice Status',
compute='_compute_invoice_status',
store=True, readonly=True, default='no')
#~ partner_invoice_id = fields.Many2one('res.partner',
#~ string='Invoice Address',
#~ readonly=True, required=True,
#~ states={'draft': [('readonly', False)],
#~ 'sent': [('readonly', False)]},
#~ help="Invoice address for current sales order.")
partner_invoice_id = fields.Many2one('res.partner',
string='Invoice Address', required=True,
states={'done': [('readonly', True)]},
help="Invoice address for current sales order.")
fiscal_position_id = fields.Many2one('account.fiscal.position', oldname='fiscal_position', string='Fiscal Position')
#WorkFlow Mail Fields-----------------------------------------------
has_confirmed_reservations_to_send = fields.Boolean(
@@ -173,12 +228,12 @@ class HotelFolio(models.Model):
'Prepaid Warning Days',
help='Margin in days to create a notice if a payment \
advance has not been recorded')
rooms_char = fields.Char('Rooms', compute='_computed_rooms_char')
segmentation_ids = fields.Many2many('res.partner.category',
string='Segmentation')
client_order_ref = fields.Char(string='Customer Reference', copy=False)
note = fields.Text('Terms and conditions')
sequence = fields.Integer(string='Sequence', default=10)
team_id = fields.Many2one('crm.team', 'Sales Channel', change_default=True, default=_get_default_team, oldname='section_id')
@api.depends('room_lines.price_total','service_ids.price_total')
def _amount_all(self):
@@ -197,20 +252,32 @@ class HotelFolio(models.Model):
'amount_total': amount_untaxed + amount_tax,
})
def _computed_rooms_char(self):
for record in self:
record.rooms_char = ', '.join(record.mapped('room_lines.room_id.name'))
@api.multi
def _compute_num_invoices(self):
pass
# for fol in self:
# fol.num_invoices = len(self.mapped('invoice_ids.id'))
# @api.depends('order_line.price_total', 'payment_ids', 'return_ids')
@api.depends('amount_total', 'payment_ids', 'return_ids')
@api.multi
def compute_amount(self):
_logger.info('compute_amount')
acc_pay_obj = self.env['account.payment']
for record in self:
if record.reservation_type in ('staff', 'out'):
vals = {
'pending_amount': 0,
'invoices_paid': 0,
'refund_amount': 0,
}
record.update(vals)
else:
total_inv_refund = 0
payments = acc_pay_obj.search([
('folio_id', '=', record.id)
])
total_paid = sum(pay.amount for pay in payments)
return_lines = self.env['payment.return.line'].search([('move_line_ids','in',payments.mapped('move_line_ids.id')),('return_id.state','=', 'done')])
total_inv_refund = sum(pay_return.amount for pay_return in return_lines)
vals = {
'pending_amount': record.amount_total - total_paid + total_inv_refund,
'invoices_paid': total_paid,
'refund_amount': total_inv_refund,
}
record.update(vals)
@api.multi
def action_pay(self):
@@ -356,7 +423,7 @@ class HotelFolio(models.Model):
if any(f not in vals for f in lfields):
partner = self.env['res.partner'].browse(vals.get('partner_id'))
addr = partner.address_get(['delivery', 'invoice'])
#~ vals['partner_invoice_id'] = vals.setdefault('partner_invoice_id', addr['invoice'])
vals['partner_invoice_id'] = vals.setdefault('partner_invoice_id', addr['invoice'])
vals['pricelist_id'] = vals.setdefault(
'pricelist_id',
partner.property_product_pricelist and partner.property_product_pricelist.id)
@@ -369,23 +436,29 @@ class HotelFolio(models.Model):
"""
Update the following fields when the partner is changed:
- Pricelist
- Payment terms
- Invoice address
- user_id
- Delivery address
"""
if not self.partner_id:
#~ self.update({
#~ 'partner_invoice_id': False,
#~ 'payment_term_id': False,
#~ 'fiscal_position_id': False,
#~ })
self.update({
'partner_invoice_id': False,
'payment_term_id': False,
'fiscal_position_id': False,
})
return
addr = self.partner_id.address_get(['invoice'])
pricelist = self.partner_id.property_product_pricelist and \
self.partner_id.property_product_pricelist.id or \
self.env['ir.default'].sudo().get('res.config.settings', 'default_pricelist_id')
values = {'user_id': self.partner_id.user_id.id or self.env.uid,
'pricelist_id': pricelist
}
values = {
'pricelist_id': pricelist,
'payment_term_id': self.partner_id.property_payment_term_id and self.partner_id.property_payment_term_id.id or False,
'partner_invoice_id': addr['invoice'],
'user_id': self.partner_id.user_id.id or self.env.uid
}
if self.env['ir.config_parameter'].sudo().get_param('sale.use_sale_note') and \
self.env.user.company_id.sale_note:
values['note'] = self.with_context(
@@ -413,31 +486,6 @@ class HotelFolio(models.Model):
else:
return 'normal'
@api.multi
def action_invoice_create(self, grouped=False, states=None):
'''
@param self: object pointer
'''
pass
# if states is None:
# states = ['confirmed', 'done']
# order_ids = [folio.order_id.id for folio in self]
# sale_obj = self.env['sale.order'].browse(order_ids)
# invoice_id = (sale_obj.action_invoice_create(grouped=False,
# states=['confirmed',
# 'done']))
# for line in self:
# values = {'invoiced': True,
# 'state': 'progress' if grouped else 'progress',
# 'hotel_invoice_id': invoice_id
# }
# line.write(values)
# return invoice_id
@api.multi
def advance_invoice(self):
pass
'''
WORKFLOW STATE
'''
@@ -483,7 +531,21 @@ class HotelFolio(models.Model):
@api.multi
def action_confirm(self):
_logger.info('action_confirm')
for folio in self.filtered(lambda folio: folio.partner_id not in folio.message_partner_ids):
folio.message_subscribe([folio.partner_id.id])
self.write({
'state': 'confirm',
'confirmation_date': fields.Datetime.now()
})
#~ if self.env.context.get('send_email'):
#~ self.force_quotation_send()
# create an analytic account if at least an expense product
#~ if any([expense_policy != 'no' for expense_policy in self.order_line.mapped('product_id.expense_policy')]):
#~ if not self.analytic_account_id:
#~ self._create_analytic_account()
return True
"""

View File

@@ -1,17 +1,19 @@
# Copyright 2017-2018 Alexandre Díaz
# Copyright 2017 Dario Lodeiros
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
import time
from datetime import timedelta
from lxml import etree
from odoo.exceptions import UserError, ValidationError
from odoo.tools import (
misc,
float_is_zero,
float_compare,
DEFAULT_SERVER_DATE_FORMAT,
DEFAULT_SERVER_DATETIME_FORMAT)
from odoo import models, fields, api, _
from odoo.addons import decimal_precision as dp
import logging
_logger = logging.getLogger(__name__)
@@ -76,6 +78,28 @@ class HotelReservation(models.Model):
else:
return default_departure_hour
@api.depends('state', 'qty_to_invoice', 'qty_invoiced')
def _compute_invoice_status(self):
"""
Compute the invoice status of a Reservation. Possible statuses:
- no: if the Folio is not in status 'sale' or 'done', we consider that there is nothing to
invoice. This is also hte default value if the conditions of no other status is met.
- to invoice: we refer to the quantity to invoice of the line. Refer to method
`_get_to_invoice_qty()` for more information on how this quantity is calculated.
- invoiced: the quantity invoiced is larger or equal to the quantity ordered.
"""
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
for line in self:
if line.state in ('draft'):
line.invoice_status = 'no'
elif not float_is_zero(line.qty_to_invoice, precision_digits=precision):
line.invoice_status = 'to invoice'
elif float_compare(line.qty_invoiced, len(line.reservation_line_ids), precision_digits=precision) >= 0:
line.invoice_status = 'invoiced'
else:
line.invoice_status = 'no'
@api.model
def name_search(self, name='', args=None, operator='ilike', limit=100):
if args is None:
@@ -115,6 +139,7 @@ class HotelReservation(models.Model):
).days
name = fields.Text('Reservation Description', required=True)
sequence = fields.Integer(string='Sequence', default=10)
room_id = fields.Many2one('hotel.room', string='Room')
@@ -134,6 +159,7 @@ class HotelReservation(models.Model):
track_visibility='onchange')
reservation_type = fields.Selection(related='folio_id.reservation_type',
default=lambda *a: 'normal')
invoice_count = fields.Integer(related='folio_id.invoice_count')
board_service_room_id = fields.Many2one('hotel.board.service.room.type',
string='Board Service')
cancelled_reason = fields.Selection([
@@ -166,7 +192,7 @@ class HotelReservation(models.Model):
partner_id = fields.Many2one(related='folio_id.partner_id')
closure_reason_id = fields.Many2one(related='folio_id.closure_reason_id')
company_id = fields.Many2one('res.company', 'Company')
company_id = fields.Many2one(related='folio_id.company_id', string='Company', store=True, readonly=True)
reservation_line_ids = fields.One2many('hotel.reservation.line',
'reservation_id',
readonly=True, required=True,
@@ -230,26 +256,24 @@ class HotelReservation(models.Model):
# order_line = fields.One2many('sale.order.line', 'order_id', string='Order Lines', states={'cancel': [('readonly', True)], 'done': [('readonly', True)]}, copy=True, auto_join=True)
# product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product')
# product_uom = fields.Many2one('product.uom', string='Unit of Measure', required=True)
# product_uom_qty = fields.Float(string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True, default=1.0)
currency_id = fields.Many2one('res.currency',
related='pricelist_id.currency_id',
string='Currency', readonly=True, required=True)
# invoice_status = fields.Selection([
# ('upselling', 'Upselling Opportunity'),
# ('invoiced', 'Fully Invoiced'),
# ('to invoice', 'To Invoice'),
# ('no', 'Nothing to Invoice')
# ], string='Invoice Status', compute='_compute_invoice_status', store=True, readonly=True, default='no')
tax_id = fields.Many2many('account.tax',
invoice_status = fields.Selection([
('invoiced', 'Fully Invoiced'),
('to invoice', 'To Invoice'),
('no', 'Nothing to Invoice')
], string='Invoice Status', compute='_compute_invoice_status', store=True, readonly=True, default='no')
tax_ids = fields.Many2many('account.tax',
string='Taxes',
domain=['|', ('active', '=', False), ('active', '=', True)])
# qty_to_invoice = fields.Float(
# string='To Invoice', store=True, readonly=True,
# digits=dp.get_precision('Product Unit of Measure'))
# qty_invoiced = fields.Float(
# compute='_get_invoice_qty', string='Invoiced', store=True, readonly=True,
# digits=dp.get_precision('Product Unit of Measure'))
qty_to_invoice = fields.Float(
compute='_get_to_invoice_qty', string='To Invoice', store=True, readonly=True,
digits=dp.get_precision('Product Unit of Measure'))
qty_invoiced = fields.Float(
compute='_get_invoice_qty', string='Invoiced', store=True, readonly=True,
digits=dp.get_precision('Product Unit of Measure'))
invoice_line_ids = fields.Many2many('account.invoice.line', 'reservation_invoice_rel', 'reservation_id', 'invoice_line_id', string='Invoice Lines', copy=False)
# qty_delivered = fields.Float(string='Delivered', copy=False, digits=dp.get_precision('Product Unit of Measure'), default=0.0)
# qty_delivered_updateable = fields.Boolean(compute='_compute_qty_delivered_updateable', string='Can Edit Delivered', readonly=True, default=True)
price_subtotal = fields.Monetary(string='Subtotal',
@@ -275,7 +299,7 @@ class HotelReservation(models.Model):
# FIXME discount per night
discount = fields.Float(string='Discount (%)', digits=dp.get_precision('Discount'), default=0.0)
# analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags')
analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags')
@api.model
def create(self, vals):
@@ -293,19 +317,12 @@ class HotelReservation(models.Model):
vals.update({'folio_id': folio.id,
'reservation_type': vals.get('reservation_type'),
'channel_type': vals.get('channel_type')})
if 'service_ids' in vals and vals['service_ids'][0][2]:
for service in vals['service_ids']:
service[2]['folio_id'] = folio.id
vals.update({
'last_updated_res': fields.Datetime.now(),
})
if 'board_service_room_id' in vals:
board_services = []
board = self.env['hotel.board.service.room.type'].browse(vals['board_service_room_id'])
for line in board.board_service_line_ids:
board_services.append((0, False, {
'product_id': line.product_id.id,
'is_board_service': True,
'folio_id': vals.get('folio_id'),
}))
vals.update({'service_ids': board_services})
if self.compute_price_out_vals(vals):
days_diff = (
fields.Date.from_string(vals['checkout']) - fields.Date.from_string(vals['checkin'])
@@ -349,18 +366,15 @@ class HotelReservation(models.Model):
board_services = []
board = self.env['hotel.board.service.room.type'].browse(vals['board_service_room_id'])
for line in board.board_service_line_ids:
board_services.append((0, False, {
res = {
'product_id': line.product_id.id,
'is_board_service': True,
'folio_id': record.folio_id.id or vals.get('folio_id')
}))
'folio_id': vals.get('folio_id'),
}
res.update(self.env['hotel.service']._prepare_add_missing_fields(res))
board_services.append((0, False, vals))
# NEED REVIEW: Why I need add manually the old IDs if board service is (0,0,(-)) ¿?¿?¿
record.update({'service_ids': [(6, 0, record.service_ids.ids)] + board_services})
update_services = record.service_ids.filtered(
lambda r: r.is_board_service == True
)
for service in update_services:
service.onchange_product_calc_qty()
if record.compute_price_out_vals(vals):
record.update(record.prepare_reservation_lines(
checkin,
@@ -419,12 +433,12 @@ class HotelReservation(models.Model):
""" Deduce missing required fields from the onchange """
res = {}
onchange_fields = ['room_id', 'reservation_type',
'currency_id', 'name', 'board_service_room_id']
'currency_id', 'name', 'board_service_room_id','service_ids']
if values.get('room_type_id'):
line = self.new(values)
if any(f not in values for f in onchange_fields):
line.onchange_room_id()
line.onchange_compute_reservation_description()
line.onchange_room_type_id()
line.onchange_board_service()
if 'pricelist_id' not in values:
line.onchange_partner_id()
@@ -592,7 +606,10 @@ class HotelReservation(models.Model):
update_old_prices=False))
@api.onchange('checkin', 'checkout', 'room_type_id')
def onchange_compute_reservation_description(self):
def onchange_room_type_id(self):
"""
When change de room_type_id, we calc the line description and tax_ids
"""
if self.room_type_id and self.checkin and self.checkout:
checkin_dt = fields.Date.from_string(self.checkin)
checkout_dt = fields.Date.from_string(self.checkout)
@@ -600,12 +617,13 @@ class HotelReservation(models.Model):
checkout_str = checkout_dt.strftime('%d/%m/%Y')
self.name = self.room_type_id.name + ': ' + checkin_str + ' - '\
+ checkout_str
self._compute_tax_ids()
@api.onchange('checkin', 'checkout')
def onchange_update_service_per_day(self):
services = self.service_ids.filtered(lambda r: r.per_day == True)
for service in services:
service.onchange_product_calc_qty()
service.onchange_product_id()
@api.multi
@api.onchange('checkin', 'checkout', 'room_id')
@@ -637,20 +655,23 @@ class HotelReservation(models.Model):
for line in self.board_service_room_id.board_service_line_ids:
product = line.product_id
if product.per_day:
vals = {
res = {
'product_id': product.id,
'is_board_service': True,
'folio_id': self.folio_id.id,
}
vals.update(self.env['hotel.service'].prepare_service_lines(
line = self.env['hotel.service'].new(res)
res.update(self.env['hotel.service']._prepare_add_missing_fields(res))
res.update(self.env['hotel.service'].prepare_service_lines(
dfrom=self.checkin,
days=self.nights,
per_person=product.per_person,
persons=self.adults,
old_line_days=False))
board_services.append((0, False, vals))
board_services.append((0, False, res))
other_services = self.service_ids.filtered(lambda r: r.is_board_service == False)
self.update({'service_ids': [(6, 0, other_services.ids)] + board_services})
self.update({'service_ids': board_services})
self.service_ids |= other_services
for service in self.service_ids.filtered(lambda r: r.is_board_service == True):
service._compute_tax_ids()
service.price_unit = service._compute_price_unit()
@@ -762,7 +783,7 @@ class HotelReservation(models.Model):
return True
return False
@api.depends('reservation_line_ids', 'reservation_line_ids.discount', 'tax_id')
@api.depends('reservation_line_ids', 'reservation_line_ids.discount', 'tax_ids')
def _compute_amount_reservation(self):
"""
Compute the amounts of the reservation.
@@ -772,7 +793,7 @@ class HotelReservation(models.Model):
if amount_room > 0:
product = record.room_type_id.product_id
price = amount_room * (1 - (record.discount or 0.0) * 0.01)
taxes = record.tax_id.compute_all(price, record.currency_id, 1, product=product)
taxes = record.tax_ids.compute_all(price, record.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'],
@@ -806,7 +827,7 @@ class HotelReservation(models.Model):
pricelist=pricelist_id,
uom=product.uom_id.id)
line_price = self.env['account.tax']._fix_tax_included_price_company(
product.price, product.taxes_id, self.tax_id, self.company_id)
product.price, product.taxes_id, self.tax_ids, self.company_id)
if old_line:
cmds.append((1, old_line.id, {
'price': line_price
@@ -1012,7 +1033,7 @@ class HotelReservation(models.Model):
'ignore_avail_restrictions': True}).create(vals)
if not reservation_copy:
raise ValidationError(_("Unexpected error copying record. \
Can't split reservation!"))
Can't split reservation!"))
record.write({
'checkout': new_start_date_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
'price_total': tprice[0],
@@ -1124,3 +1145,56 @@ class HotelReservation(models.Model):
@api.multi
def send_cancel_mail(self):
return self.folio_id.send_cancel_mail()
"""
INVOICING PROCESS
"""
@api.multi
def open_invoices_reservation(self):
invoices = self.folio_id.mapped('invoice_ids')
action = self.env.ref('account.action_invoice_tree1').read()[0]
if len(invoices) > 1:
action['domain'] = [('id', 'in', invoices.ids)]
elif len(invoices) == 1:
action['views'] = [(self.env.ref('account.invoice_form').id, 'form')]
action['res_id'] = invoices.ids[0]
else:
action = self.env.ref('hotel.action_view_folio_advance_payment_inv').read()[0]
action['context'] = {'default_reservation_id': self.id,
'default_folio_id': self.folio_id.id}
return action
@api.multi
def _compute_tax_ids(self):
for record in self:
# If company_id is set, always filter taxes by the company
folio = record.folio_id or self.env.context.get('default_folio_id')
product = self.env['product.product'].browse(record.room_type_id.product_id.id)
record.tax_ids = product.taxes_id.filtered(lambda r: not record.company_id or r.company_id == folio.company_id)
@api.depends('qty_invoiced', 'nights', 'folio_id.state')
def _get_to_invoice_qty(self):
"""
Compute the quantity to invoice. If the invoice policy is order, the quantity to invoice is
calculated from the ordered quantity. Otherwise, the quantity delivered is used.
"""
for line in self:
if line.folio_id.state not in ['draft']:
line.qty_to_invoice = len(line.reservation_line_ids) - line.qty_invoiced
else:
line.qty_to_invoice = 0
@api.depends('invoice_line_ids.invoice_id.state', 'invoice_line_ids.quantity')
def _get_invoice_qty(self):
"""
Compute the quantity invoiced. If case of a refund, the quantity invoiced is decreased. We
must check day per day and sum or decreased on 1 unit per invoice_line
"""
for line in self:
qty_invoiced = 0.0
for day in line.reservation_line_ids:
invoice_lines = day.invoice_line_ids.filtered(lambda r: r.invoice_id.state != 'cancel')
qty_invoiced += len(invoice_lines.filtered(lambda r: r.invoice_id.type == 'out_invoice')) - \
len(invoice_lines.filtered(lambda r: r.invoice_id.type == 'out_refund'))
line.qty_invoiced = qty_invoiced

View File

@@ -4,11 +4,21 @@
from odoo import models, fields, api, _
from odoo.addons import decimal_precision as dp
from odoo.exceptions import ValidationError
from datetime import date
class HotelReservationLine(models.Model):
_name = "hotel.reservation.line"
_order = "date"
@api.multi
def name_get(self):
result = []
for res in self:
date = fields.Date.from_string(res.date)
name = u'%s/%s' % (date.day, date.month)
result.append((res.id, name))
return result
reservation_id = fields.Many2one('hotel.reservation', string='Reservation',
ondelete='cascade', required=True,
copy=False)
@@ -17,6 +27,11 @@ class HotelReservationLine(models.Model):
discount = fields.Float(
string='Discount (%)',
digits=dp.get_precision('Discount'), default=0.0)
invoice_line_ids = fields.Many2many(
'account.invoice.line',
'reservation_line_invoice_rel',
'reservation_line_id', 'invoice_line_id',
string='Invoice Lines', readonly=True, copy=False)
@api.constrains('date')
def constrains_duplicated_date(self):

View File

@@ -2,7 +2,10 @@
# Copyright 2017 Dario Lodeiros
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields, api, _
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
from odoo.tools import (
float_is_zero,
float_compare,
DEFAULT_SERVER_DATE_FORMAT)
from datetime import timedelta
from odoo.exceptions import ValidationError
from odoo.addons import decimal_precision as dp
@@ -42,11 +45,78 @@ class HotelService(models.Model):
ids = [item[1] for item in self.env.context['room_lines']]
return self.env['hotel.reservation'].browse([
(ids)], limit=1)
elif self.env.context.get('default_ser_room_line'):
return self.env.context.get('default_ser_room_line')
return False
name = fields.Char('Service description')
@api.model
def _default_folio_id(self):
if 'folio_id' in self._context:
return self._context['folio_id']
return False
@api.depends('qty_invoiced', 'product_qty', 'folio_id.state')
def _get_to_invoice_qty(self):
"""
Compute the quantity to invoice. If the invoice policy is order, the quantity to invoice is
calculated from the ordered quantity. Otherwise, the quantity delivered is used.
"""
for line in self:
if line.folio_id.state not in ['draft']:
line.qty_to_invoice = line.product_qty - line.qty_invoiced
else:
line.qty_to_invoice = 0
@api.depends('invoice_line_ids.invoice_id.state', 'invoice_line_ids.quantity')
def _get_invoice_qty(self):
"""
Compute the quantity invoiced. If case of a refund, the quantity invoiced is decreased. Note
that this is the case only if the refund is generated from the SO and that is intentional: if
a refund made would automatically decrease the invoiced quantity, then there is a risk of reinvoicing
it automatically, which may not be wanted at all. That's why the refund has to be created from the SO
"""
for line in self:
qty_invoiced = 0.0
for invoice_line in line.invoice_line_ids:
if invoice_line.invoice_id.state != 'cancel':
if invoice_line.invoice_id.type == 'out_invoice':
qty_invoiced += invoice_line.uom_id._compute_quantity(invoice_line.quantity, line.product_id.uom_id)
elif invoice_line.invoice_id.type == 'out_refund':
qty_invoiced -= invoice_line.uom_id._compute_quantity(invoice_line.quantity, line.product_id.uom_id)
line.qty_invoiced = qty_invoiced
@api.depends('product_qty', 'qty_to_invoice', 'qty_invoiced')
def _compute_invoice_status(self):
"""
Compute the invoice status of a SO line. Possible statuses:
- no: if the SO is not in status 'sale' or 'done', we consider that there is nothing to
invoice. This is also hte default value if the conditions of no other status is met.
- to invoice: we refer to the quantity to invoice of the line. Refer to method
`_get_to_invoice_qty()` for more information on how this quantity is calculated.
- upselling: this is possible only for a product invoiced on ordered quantities for which
we delivered more than expected. The could arise if, for example, a project took more
time than expected but we decided not to invoice the extra cost to the client. This
occurs onyl in state 'sale', so that when a SO is set to done, the upselling opportunity
is removed from the list.
- invoiced: the quantity invoiced is larger or equal to the quantity ordered.
"""
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
for line in self:
if line.folio_id.state in ('draft'):
line.invoice_status = 'no'
elif not float_is_zero(line.qty_to_invoice, precision_digits=precision):
line.invoice_status = 'to invoice'
elif float_compare(line.qty_invoiced, line.product_qty, precision_digits=precision) >= 0:
line.invoice_status = 'invoiced'
else:
line.invoice_status = 'no'
name = fields.Char('Service description', required=True)
sequence = fields.Integer(string='Sequence', default=10)
product_id = fields.Many2one('product.product', 'Service', required=True)
folio_id = fields.Many2one('hotel.folio', 'Folio', ondelete='cascade')
folio_id = fields.Many2one('hotel.folio', 'Folio',
ondelete='cascade',
default=_default_folio_id)
ser_room_line = fields.Many2one('hotel.reservation', 'Room',
default=_default_ser_room_line)
per_day = fields.Boolean(related='product_id.per_day')
@@ -57,6 +127,11 @@ class HotelService(models.Model):
# Non-stored related field to allow portal user to see the image of the product he has ordered
product_image = fields.Binary('Product Image', related="product_id.image", store=False)
company_id = fields.Many2one(related='folio_id.company_id', string='Company', store=True, readonly=True)
invoice_status = fields.Selection([
('invoiced', 'Fully Invoiced'),
('to invoice', 'To Invoice'),
('no', 'Nothing to Invoice')
], string='Invoice Status', compute='_compute_invoice_status', store=True, readonly=True, default='no')
channel_type = fields.Selection([
('door', 'Door'),
('mail', 'Mail'),
@@ -67,6 +142,14 @@ class HotelService(models.Model):
tax_ids = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)])
discount = fields.Float(string='Discount (%)', digits=dp.get_precision('Discount'), default=0.0)
currency_id = fields.Many2one(related='folio_id.currency_id', store=True, string='Currency', readonly=True)
invoice_line_ids = fields.Many2many('account.invoice.line', 'service_line_invoice_rel', 'service_id', 'invoice_line_id', string='Invoice Lines', copy=False)
analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags')
qty_to_invoice = fields.Float(
compute='_get_to_invoice_qty', string='To Invoice', store=True, readonly=True,
digits=dp.get_precision('Product Unit of Measure'))
qty_invoiced = fields.Float(
compute='_get_invoice_qty', string='Invoiced', store=True, readonly=True,
digits=dp.get_precision('Product Unit of Measure'))
price_subtotal = fields.Monetary(string='Subtotal',
readonly=True,
store=True,
@@ -126,11 +209,11 @@ class HotelService(models.Model):
def _prepare_add_missing_fields(self, values):
""" Deduce missing required fields from the onchange """
res = {}
onchange_fields = ['price_unit','tax_ids']
onchange_fields = ['price_unit','tax_ids','name']
if values.get('product_id'):
line = self.new(values)
if any(f not in values for f in onchange_fields):
line.onchange_product_calc_qty()
line.onchange_product_id()
for field in onchange_fields:
if field not in values:
res[field] = line._fields[field].convert_to_write(line[field], line)
@@ -154,24 +237,28 @@ class HotelService(models.Model):
def _compute_tax_ids(self):
for record in self:
# If company_id is set, always filter taxes by the company
folio = record.folio_id or self.env.context.get('default_folio_id')
record.tax_ids = record.product_id.taxes_id.filtered(lambda r: not record.company_id or r.company_id == folio.company_id)
folio = record.folio_id or self.env['hotel.folio'].browse(self.env.context.get('default_folio_id'))
reservation = record.ser_room_line or self.env.context.get('ser_room_line')
origin = folio if folio else reservation
record.tax_ids = record.product_id.taxes_id.filtered(lambda r: not record.company_id or r.company_id == origin.company_id)
@api.multi
def _get_display_price(self, product):
folio = self.folio_id or self.env.context.get('default_folio_id')
if folio.pricelist_id.discount_policy == 'with_discount':
return product.with_context(pricelist=folio.pricelist_id.id).price
product_context = dict(self.env.context, partner_id=folio.partner_id.id, date=folio.date_order, uom=self.product_id.uom_id.id)
final_price, rule_id = folio.pricelist_id.with_context(product_context).get_product_price_rule(self.product_id, self.product_qty or 1.0, folio.partner_id)
base_price, currency_id = self.with_context(product_context)._get_real_price_currency(product, rule_id, self.product_qty, product_id.uom_id, folio.pricelist_id.id)
if currency_id != folio.pricelist_id.currency_id.id:
base_price = self.env['res.currency'].browse(currency_id).with_context(product_context).compute(base_price, folio.pricelist_id.currency_id)
reservation = self.ser_room_line or self.env.context.get('ser_room_line')
origin = folio if folio else reservation
if origin.pricelist_id.discount_policy == 'with_discount':
return product.with_context(pricelist=origin.pricelist_id.id).price
product_context = dict(self.env.context, partner_id=origin.partner_id.id, date=folio.date_order if folio else fields.Date.today(), uom=self.product_id.uom_id.id)
final_price, rule_id = origin.pricelist_id.with_context(product_context).get_product_price_rule(self.product_id, self.product_qty or 1.0, origin.partner_id)
base_price, currency_id = self.with_context(product_context)._get_real_price_currency(product, rule_id, self.product_qty, product_id.uom_id, origin.pricelist_id.id)
if currency_id != origin.pricelist_id.currency_id.id:
base_price = self.env['res.currency'].browse(currency_id).with_context(product_context).compute(base_price, origin.pricelist_id.currency_id)
# negative discounts (= surcharge) are included in the display price
return max(base_price, final_price)
@api.onchange('product_id')
def onchange_product_calc_qty(self):
def onchange_product_id(self):
"""
Compute the default quantity according to the
configuration of the selected product, in per_day product configuration,
@@ -195,6 +282,30 @@ class HotelService(models.Model):
for day in record.service_line_ids:
day.no_free_resources()
"""
Description and warnings
"""
product = self.product_id.with_context(
lang=self.folio_id.partner_id.lang,
partner=self.folio_id.partner_id.id
)
title = False
message = False
warning = {}
if product.sale_line_warn != 'no-message':
title = _("Warning for %s") % product.name
message = product.sale_line_warn_msg
warning['title'] = title
warning['message'] = message
result = {'warning': warning}
if product.sale_line_warn == 'block':
self.product_id = False
return result
name = product.name_get()[0][1]
if product.description_sale:
name += '\n' + product.description_sale
vals['name'] = name
"""
Compute tax and price unit
"""
self._compute_tax_ids()
@@ -206,9 +317,10 @@ class HotelService(models.Model):
self.ensure_one()
folio = self.folio_id or self.env.context.get('default_folio_id')
reservation = self.ser_room_line or self.env.context.get('ser_room_line')
if folio or reservation:
partner = folio.partner_id if folio else reservation.partner_id
pricelist = folio.pricelist_id if folio else reservation.pricelist_id
origin = folio if folio else reservation
if origin:
partner = origin.partner_id
pricelist = origin.pricelist_id
if reservation and self.is_board_service:
board_room_type = reservation.board_service_room_id
if board_room_type.price_type == 'fixed':
@@ -224,12 +336,12 @@ class HotelService(models.Model):
lang=partner.lang,
partner=partner.id,
quantity=self.product_qty,
date=folio.date_order or fields.Date.today(),
date=folio.date_order if folio else fields.Date.today(),
pricelist=pricelist.id,
uom=self.product_id.uom_id.id,
fiscal_position=False
)
return self.env['account.tax']._fix_tax_included_price_company(self._get_display_price(product), product.taxes_id, self.tax_ids, folio.company_id)
return self.env['account.tax']._fix_tax_included_price_company(self._get_display_price(product), product.taxes_id, self.tax_ids, origin.company_id)
@api.model
def prepare_service_lines(self, **kwargs):
@@ -263,7 +375,7 @@ class HotelService(models.Model):
Compute the amounts of the service line.
"""
for record in self:
folio = record.folio_id or self.env.context.get('default_folio_id')
folio = record.folio_id or self.env['hotel.folio'].browse(self.env.context.get('default_folio_id'))
reservation = record.ser_room_line or self.env.context.get('ser_room_line')
currency = folio.currency_id if folio else reservation.currency_id
product = record.product_id

View File

@@ -33,7 +33,6 @@ class AccountInvoice(models.Model):
'domain': [('id', 'in', payment_ids)],
}
dif_customer_payment = fields.Boolean(compute='_compute_dif_customer_payment')
from_folio = fields.Boolean(compute='_compute_dif_customer_payment')
sale_ids = fields.Many2many(
'sale.order', 'sale_order_invoice_rel', 'invoice_id',
@@ -45,16 +44,11 @@ class AccountInvoice(models.Model):
@api.multi
def _compute_dif_customer_payment(self):
for inv in self:
sales = inv.mapped('invoice_line_ids.sale_line_ids.order_id')
folios = self.env['hotel.folio'].search([('order_id.id','in',sales.ids)])
folios = inv.mapped('invoice_line_ids.reservation_ids.folio_id')
folios |= inv.mapped('invoice_line_ids.service_ids.folio_id')
if folios:
inv.from_folio = True
inv.folio_ids = [(6, 0, folios.ids)]
payments_obj = self.env['account.payment']
payments = payments_obj.search([('folio_id','in',folios.ids)])
for pay in payments:
if pay.partner_id != inv.partner_id:
inv.dif_customer_payment = True
@api.multi
def action_invoice_open(self):

View File

@@ -0,0 +1,24 @@
# Copyright 2017 Alexandre Díaz
# Copyright 2017 Dario Lodeiros
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
from odoo import api, fields, models, _
class AccountInvoiceLine(models.Model):
_inherit = 'account.invoice.line'
reservation_ids = fields.Many2many(
'hotel.reservation',
'reservation_invoice_rel',
'invoice_line_id', 'reservation_id',
string='Reservations', readonly=True, copy=False)
service_ids = fields.Many2many(
'hotel.service',
'service_line_invoice_rel',
'invoice_line_id', 'service_id',
string='Services', readonly=True, copy=False)
reservation_line_ids = fields.Many2many(
'hotel.reservation.line',
'reservation_line_invoice_rel',
'invoice_line_id', 'reservation_line_id',
string='Reservation Lines', readonly=True, copy=False)