diff --git a/hotel/models/hotel_folio.py b/hotel/models/hotel_folio.py
index 23cf212a1..1f968b977 100644
--- a/hotel/models/hotel_folio.py
+++ b/hotel/models/hotel_folio.py
@@ -33,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
@@ -68,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)
@@ -148,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'),
@@ -160,10 +207,8 @@ class HotelFolio(models.Model):
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)]},
+ 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')
@@ -212,12 +257,6 @@ class HotelFolio(models.Model):
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.multi
def compute_amount(self):
@@ -380,8 +419,9 @@ 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({
@@ -390,13 +430,18 @@ class HotelFolio(models.Model):
'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(
@@ -424,127 +469,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
-
- @api.multi
- def _prepare_invoice(self):
- """
- Prepare the dict of values to create the new invoice for a sales order. This method may be
- overridden to implement custom invoice generation (making sure to call super() to establish
- a clean extension chain).
- """
- self.ensure_one()
- 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.'))
- import wdb; wdb.set_trace()
- invoice_vals = {
- 'name': self.client_order_ref or '',
- 'origin': self.name,
- 'type': 'out_invoice',
- 'account_id': self.partner_invoice_id.property_account_receivable_id.id,
- 'partner_id': self.partner_invoice_id.id,
- 'partner_shipping_id': self.partner_id.id,
- 'journal_id': journal_id,
- 'currency_id': self.pricelist_id.currency_id.id,
- 'comment': self.note,
- 'payment_term_id': self.payment_term_id.id,
- 'fiscal_position_id': self.fiscal_position_id.id or self.partner_invoice_id.property_account_position_id.id,
- 'company_id': self.company_id.id,
- 'user_id': self.user_id and self.user_id.id,
- 'team_id': self.team_id.id
- }
- return invoice_vals
-
- @api.multi
- def action_invoice_create(self, grouped=False):
- """
- Create the invoice associated to the Folio.
- :param grouped: if True, invoices are grouped by Folio id. If False, invoices are grouped by
- (partner_invoice_id, currency)
- :returns: list of created invoices
- """
- inv_obj = self.env['account.invoice']
- precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
- invoices = {}
- references = {}
- invoices_origin = {}
- invoices_name = {}
-
- for folio in self:
- group_key = folio.id if grouped else (folio.partner_invoice_id.id, folio.currency_id.id)
- for line in folio.room_lines.sorted(key=lambda l: l.qty_to_invoice < 0):
- if float_is_zero(line.qty_to_invoice, precision_digits=precision):
- continue
- if group_key not in invoices:
- inv_data = folio._prepare_invoice()
- invoice = inv_obj.create(inv_data)
- references[invoice] = folio
- invoices[group_key] = invoice
- invoices_origin[group_key] = [invoice.origin]
- invoices_name[group_key] = [invoice.name]
- elif group_key in invoices:
- if folio.name not in invoices_origin[group_key]:
- invoices_origin[group_key].append(folio.name)
- if folio.client_order_ref and folio.client_order_ref not in invoices_name[group_key]:
- invoices_name[group_key].append(folio.client_order_ref)
-
- if line.qty_to_invoice > 0:
- line.invoice_line_create(invoices[group_key].id, line.nights)
-
- if references.get(invoices.get(group_key)):
- if folio not in references[invoices[group_key]]:
- references[invoices[group_key]] |= folio
-
- for group_key in invoices:
- invoices[group_key].write({'name': ', '.join(invoices_name[group_key]),
- 'origin': ', '.join(invoices_origin[group_key])})
-
- if not invoices:
- raise UserError(_('There is no invoiceable line.'))
-
- for invoice in invoices.values():
- 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_untaxed < 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()
- invoice.message_post_with_view('mail.message_origin_link',
- values={'self': invoice, 'origin': references[invoice]},
- subtype_id=self.env.ref('mail.mt_note').id)
- return [inv.id for inv in invoices.values()]
-
'''
WORKFLOW STATE
'''
diff --git a/hotel/models/hotel_reservation.py b/hotel/models/hotel_reservation.py
index 0ad40862e..2dd7a37c9 100644
--- a/hotel/models/hotel_reservation.py
+++ b/hotel/models/hotel_reservation.py
@@ -77,6 +77,36 @@ class HotelReservation(models.Model):
return folio.room_lines[0].departure_hour
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 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.
+ """
+ pass
+ #~ precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
+ #~ for line in self:
+ #~ if line.state not in ('confirm', 'done'):
+ #~ line.invoice_status = 'no'
+ #~ elif not float_is_zero(line.qty_to_invoice, precision_digits=precision):
+ #~ line.invoice_status = 'to invoice'
+ #~ elif line.state == 'sale' and line.product_id.invoice_policy == 'order' and\
+ #~ float_compare(line.qty_delivered, line.product_uom_qty, precision_digits=precision) == 1:
+ #~ line.invoice_status = 'upselling'
+ #~ elif float_compare(line.qty_invoiced, line.product_uom_qty, precision_digits=precision) >= 0:
+ #~ line.invoice_status = 'invoiced'
+ #~ else:
+ #~ line.invoice_status = 'no'
@api.model
@@ -170,7 +200,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,
@@ -237,13 +267,13 @@ class HotelReservation(models.Model):
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([
+ ('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_ids = fields.Many2many('account.tax',
string='Taxes',
domain=['|', ('active', '=', False), ('active', '=', True)])
qty_to_invoice = fields.Float(
@@ -252,7 +282,7 @@ class HotelReservation(models.Model):
qty_invoiced = fields.Float(
compute='_get_invoice_qty', string='Invoiced', store=True, readonly=True,
digits=dp.get_precision('Product Unit of Measure'))
- invoice_lines = fields.Many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_line_id', string='Invoice Lines', copy=False)
+ invoice_line_ids = fields.Many2many('account.invoice.line', 'reservation_line_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',
@@ -765,7 +795,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.
@@ -775,7 +805,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'],
@@ -809,7 +839,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
@@ -1131,22 +1161,24 @@ class HotelReservation(models.Model):
"""
INVOICING PROCESS
"""
+
@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 in ['confirm', 'done']:
- if line.room_type_id.product_id.invoice_policy == 'order':
- line.qty_to_invoice = line.nights - line.qty_invoiced
- else:
- line.qty_to_invoice = line.qty_delivered - line.qty_invoiced
- else:
- line.qty_to_invoice = 0
+ pass
+ #~ for line in self:
+ #~ if line.folio_id.state in ['confirm', 'done']:
+ #~ if line.room_type_id.product_id.invoice_policy == 'order':
+ #~ line.qty_to_invoice = line.nights - line.qty_invoiced
+ #~ else:
+ #~ line.qty_to_invoice = line.qty_delivered - line.qty_invoiced
+ #~ else:
+ #~ line.qty_to_invoice = 0
- @api.depends('invoice_lines.invoice_id.state', 'invoice_lines.quantity')
+ @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
@@ -1154,64 +1186,13 @@ class HotelReservation(models.Model):
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_lines:
- 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_uom)
- elif invoice_line.invoice_id.type == 'out_refund':
- qty_invoiced -= invoice_line.uom_id._compute_quantity(invoice_line.quantity, line.product_uom)
- line.qty_invoiced = qty_invoiced
-
- @api.multi
- def _prepare_invoice_line(self, qty):
- """
- Prepare the dict of values to create the new invoice line for a reservation.
-
- :param qty: float quantity to invoice
- """
- self.ensure_one()
- res = {}
- product = self.env['product.product'].browse(self.room_type_id.product_id.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)
-
- res = {
- 'name': self.name,
- 'sequence': self.sequence,
- 'origin': self.folio_id.name,
- 'account_id': account.id,
- 'price_unit': self.price_unit,
- 'quantity': qty,
- 'discount': self.discount,
- 'uom_id': self.product_uom.id,
- 'product_id': product.id or False,
- 'invoice_line_tax_ids': [(6, 0, self.tax_id.ids)],
- 'account_analytic_id': self.folio_id.analytic_account_id.id,
- 'analytic_tag_ids': [(6, 0, self.analytic_tag_ids.ids)],
- }
- return res
-
- @api.multi
- def invoice_line_create(self, invoice_id, qty):
- """ Create an invoice line. The quantity to invoice can be positive (invoice) or negative (refund).
- :param invoice_id: integer
- :param qty: float quantity to invoice
- :returns recordset of account.invoice.line created
- """
- invoice_lines = self.env['account.invoice.line']
- precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
- for line in self:
- if not float_is_zero(qty, precision_digits=precision):
- vals = line._prepare_invoice_line(qty=qty)
- vals.update({'invoice_id': invoice_id, 'reservation_ids': [(6, 0, [line.id])]})
- invoice_lines |= self.env['account.invoice.line'].create(vals)
- return invoice_lines
-
+ pass
+ #~ 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_uom)
+ #~ elif invoice_line.invoice_id.type == 'out_refund':
+ #~ qty_invoiced -= invoice_line.uom_id._compute_quantity(invoice_line.quantity, line.product_uom)
+ #~ line.qty_invoiced = qty_invoiced
diff --git a/hotel/models/hotel_service.py b/hotel/models/hotel_service.py
index eac017044..e7a21aade 100644
--- a/hotel/models/hotel_service.py
+++ b/hotel/models/hotel_service.py
@@ -44,7 +44,73 @@ class HotelService(models.Model):
(ids)], limit=1)
return False
- name = fields.Char('Service description')
+ @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.
+ """
+ pass
+ #~ for line in self:
+ #~ if line.folio_id.state in ['confirm', 'done']:
+ #~ if line.room_type_id.product_id.invoice_policy == 'order':
+ #~ line.qty_to_invoice = line.nights - line.qty_invoiced
+ #~ else:
+ #~ line.qty_to_invoice = line.qty_delivered - 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
+ """
+ pass
+ #~ 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_uom)
+ #~ elif invoice_line.invoice_id.type == 'out_refund':
+ #~ qty_invoiced -= invoice_line.uom_id._compute_quantity(invoice_line.quantity, line.product_uom)
+ #~ 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.
+ """
+ pass
+ #~ precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
+ #~ for line in self:
+ #~ if line.state not in ('sale', 'done'):
+ #~ line.invoice_status = 'no'
+ #~ elif not float_is_zero(line.qty_to_invoice, precision_digits=precision):
+ #~ line.invoice_status = 'to invoice'
+ #~ elif line.state == 'sale' and line.product_id.invoice_policy == 'order' and\
+ #~ float_compare(line.qty_delivered, line.product_uom_qty, precision_digits=precision) == 1:
+ #~ line.invoice_status = 'upselling'
+ #~ elif float_compare(line.qty_invoiced, line.product_uom_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')
ser_room_line = fields.Many2one('hotel.reservation', 'Room',
@@ -57,6 +123,12 @@ 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([
+ ('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')
channel_type = fields.Selection([
('door', 'Door'),
('mail', 'Mail'),
@@ -67,6 +139,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,
@@ -130,7 +210,7 @@ class HotelService(models.Model):
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)
@@ -171,7 +251,7 @@ class HotelService(models.Model):
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 +275,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()
diff --git a/hotel/models/inherited_account_invoice.py b/hotel/models/inherited_account_invoice.py
index e42f483f3..1e67465fe 100644
--- a/hotel/models/inherited_account_invoice.py
+++ b/hotel/models/inherited_account_invoice.py
@@ -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,17 +44,11 @@ class AccountInvoice(models.Model):
@api.multi
def _compute_dif_customer_payment(self):
for inv in self:
- return False
- #~ sales = inv.mapped('invoice_line_ids.sale_line_ids.order_id')
- #~ folios = self.env['hotel.folio'].search([('order_id.id','in',sales.ids)])
- #~ 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
+ 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)]
@api.multi
def action_invoice_open(self):
diff --git a/hotel/models/inherited_account_invoice_line.py b/hotel/models/inherited_account_invoice_line.py
index cc3ca1328..1c251cc37 100644
--- a/hotel/models/inherited_account_invoice_line.py
+++ b/hotel/models/inherited_account_invoice_line.py
@@ -11,3 +11,8 @@ class AccountInvoiceLine(models.Model):
'reservation_line_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)
diff --git a/hotel/views/hotel_folio_views.xml b/hotel/views/hotel_folio_views.xml
index 51a6df265..742454e19 100644
--- a/hotel/views/hotel_folio_views.xml
+++ b/hotel/views/hotel_folio_views.xml
@@ -108,17 +108,17 @@
-->
-
+
@@ -152,315 +152,26 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ nolabel="1" context="{'from_folio':True,'room_lines':room_lines,'folio_id': id,'tree_view_ref':'hotel.hotel_reservation_view_bottom_tree', 'form_view_ref':'hotel.hotel_reservation_view_form'}"/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ context="{'from_room':False,'folio_id': id,'tree_view_ref':'hotel.hotel_service_view_tree', 'form_view_ref':'hotel.hotel_service_view_form'}"
+ nolabel="1" />
+
+
+
+
+
+
diff --git a/hotel/views/hotel_reservation_views.xml b/hotel/views/hotel_reservation_views.xml
index e8990fd1e..d6f5dd5bd 100644
--- a/hotel/views/hotel_reservation_views.xml
+++ b/hotel/views/hotel_reservation_views.xml
@@ -6,6 +6,7 @@
hotel.reservationformtree,form,graph,pivot
+ {'from_room': True}
@@ -20,6 +21,7 @@
+
@@ -97,16 +99,6 @@
Books
-