mirror of
https://github.com/OCA/pms.git
synced 2025-01-29 00:17:45 +02:00
[WIP] Invoice WorkFlow
This commit is contained in:
@@ -31,3 +31,4 @@ from . import hotel_service_line
|
|||||||
from . import hotel_board_service
|
from . import hotel_board_service
|
||||||
from . import hotel_board_service_room_type_line
|
from . import hotel_board_service_room_type_line
|
||||||
from . import hotel_board_service_line
|
from . import hotel_board_service_line
|
||||||
|
from . import inherited_account_invoice_line
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ from dateutil.relativedelta import relativedelta
|
|||||||
from odoo.exceptions import except_orm, UserError, ValidationError
|
from odoo.exceptions import except_orm, UserError, ValidationError
|
||||||
from odoo.tools import (
|
from odoo.tools import (
|
||||||
misc,
|
misc,
|
||||||
|
float_is_zero,
|
||||||
|
float_compare,
|
||||||
DEFAULT_SERVER_DATETIME_FORMAT,
|
DEFAULT_SERVER_DATETIME_FORMAT,
|
||||||
DEFAULT_SERVER_DATE_FORMAT)
|
DEFAULT_SERVER_DATE_FORMAT)
|
||||||
from odoo import models, fields, api, _
|
from odoo import models, fields, api, _
|
||||||
@@ -44,9 +46,14 @@ class HotelFolio(models.Model):
|
|||||||
def _amount_all(self):
|
def _amount_all(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_default_team(self):
|
||||||
|
return self.env['crm.team']._get_default_team_id()
|
||||||
|
|
||||||
#Main Fields--------------------------------------------------------
|
#Main Fields--------------------------------------------------------
|
||||||
name = fields.Char('Folio Number', readonly=True, index=True,
|
name = fields.Char('Folio Number', readonly=True, index=True,
|
||||||
default=lambda self: _('New'))
|
default=lambda self: _('New'))
|
||||||
|
client_order_ref = fields.Char(string='Customer Reference', copy=False)
|
||||||
partner_id = fields.Many2one('res.partner',
|
partner_id = fields.Many2one('res.partner',
|
||||||
track_visibility='onchange')
|
track_visibility='onchange')
|
||||||
|
|
||||||
@@ -87,6 +94,7 @@ class HotelFolio(models.Model):
|
|||||||
required=True, readonly=True, index=True,
|
required=True, readonly=True, index=True,
|
||||||
states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
|
states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
|
||||||
copy=False, default=fields.Datetime.now)
|
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([
|
state = fields.Selection([
|
||||||
('draft', 'Quotation'),
|
('draft', 'Quotation'),
|
||||||
('sent', 'Quotation Sent'),
|
('sent', 'Quotation Sent'),
|
||||||
@@ -111,6 +119,7 @@ class HotelFolio(models.Model):
|
|||||||
readonly=True)
|
readonly=True)
|
||||||
return_ids = fields.One2many('payment.return', 'folio_id',
|
return_ids = fields.One2many('payment.return', 'folio_id',
|
||||||
readonly=True)
|
readonly=True)
|
||||||
|
payment_term_id = fields.Many2one('account.payment.term', string='Payment Terms', oldname='payment_term')
|
||||||
|
|
||||||
#Amount Fields------------------------------------------------------
|
#Amount Fields------------------------------------------------------
|
||||||
pending_amount = fields.Monetary(compute='compute_amount',
|
pending_amount = fields.Monetary(compute='compute_amount',
|
||||||
@@ -150,12 +159,13 @@ class HotelFolio(models.Model):
|
|||||||
string='Invoice Status',
|
string='Invoice Status',
|
||||||
compute='_compute_invoice_status',
|
compute='_compute_invoice_status',
|
||||||
store=True, readonly=True, default='no')
|
store=True, readonly=True, default='no')
|
||||||
#~ partner_invoice_id = fields.Many2one('res.partner',
|
partner_invoice_id = fields.Many2one('res.partner',
|
||||||
#~ string='Invoice Address',
|
string='Invoice Address',
|
||||||
#~ readonly=True, required=True,
|
readonly=True, required=True,
|
||||||
#~ states={'draft': [('readonly', False)],
|
states={'draft': [('readonly', False)],
|
||||||
#~ 'sent': [('readonly', False)]},
|
'sent': [('readonly', False)]},
|
||||||
#~ help="Invoice address for current sales order.")
|
help="Invoice address for current sales order.")
|
||||||
|
fiscal_position_id = fields.Many2one('account.fiscal.position', oldname='fiscal_position', string='Fiscal Position')
|
||||||
|
|
||||||
#WorkFlow Mail Fields-----------------------------------------------
|
#WorkFlow Mail Fields-----------------------------------------------
|
||||||
has_confirmed_reservations_to_send = fields.Boolean(
|
has_confirmed_reservations_to_send = fields.Boolean(
|
||||||
@@ -179,6 +189,7 @@ class HotelFolio(models.Model):
|
|||||||
client_order_ref = fields.Char(string='Customer Reference', copy=False)
|
client_order_ref = fields.Char(string='Customer Reference', copy=False)
|
||||||
note = fields.Text('Terms and conditions')
|
note = fields.Text('Terms and conditions')
|
||||||
sequence = fields.Integer(string='Sequence', default=10)
|
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')
|
@api.depends('room_lines.price_total','service_ids.price_total')
|
||||||
def _amount_all(self):
|
def _amount_all(self):
|
||||||
@@ -356,7 +367,7 @@ class HotelFolio(models.Model):
|
|||||||
if any(f not in vals for f in lfields):
|
if any(f not in vals for f in lfields):
|
||||||
partner = self.env['res.partner'].browse(vals.get('partner_id'))
|
partner = self.env['res.partner'].browse(vals.get('partner_id'))
|
||||||
addr = partner.address_get(['delivery', 'invoice'])
|
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(
|
vals['pricelist_id'] = vals.setdefault(
|
||||||
'pricelist_id',
|
'pricelist_id',
|
||||||
partner.property_product_pricelist and partner.property_product_pricelist.id)
|
partner.property_product_pricelist and partner.property_product_pricelist.id)
|
||||||
@@ -373,11 +384,11 @@ class HotelFolio(models.Model):
|
|||||||
- user_id
|
- user_id
|
||||||
"""
|
"""
|
||||||
if not self.partner_id:
|
if not self.partner_id:
|
||||||
#~ self.update({
|
self.update({
|
||||||
#~ 'partner_invoice_id': False,
|
'partner_invoice_id': False,
|
||||||
#~ 'payment_term_id': False,
|
'payment_term_id': False,
|
||||||
#~ 'fiscal_position_id': False,
|
'fiscal_position_id': False,
|
||||||
#~ })
|
})
|
||||||
return
|
return
|
||||||
addr = self.partner_id.address_get(['invoice'])
|
addr = self.partner_id.address_get(['invoice'])
|
||||||
pricelist = self.partner_id.property_product_pricelist and \
|
pricelist = self.partner_id.property_product_pricelist and \
|
||||||
@@ -438,6 +449,102 @@ class HotelFolio(models.Model):
|
|||||||
def advance_invoice(self):
|
def advance_invoice(self):
|
||||||
pass
|
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
|
WORKFLOW STATE
|
||||||
'''
|
'''
|
||||||
@@ -483,7 +590,21 @@ class HotelFolio(models.Model):
|
|||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def action_confirm(self):
|
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
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ from lxml import etree
|
|||||||
from odoo.exceptions import UserError, ValidationError
|
from odoo.exceptions import UserError, ValidationError
|
||||||
from odoo.tools import (
|
from odoo.tools import (
|
||||||
misc,
|
misc,
|
||||||
|
float_is_zero,
|
||||||
|
float_compare,
|
||||||
DEFAULT_SERVER_DATE_FORMAT,
|
DEFAULT_SERVER_DATE_FORMAT,
|
||||||
DEFAULT_SERVER_DATETIME_FORMAT)
|
DEFAULT_SERVER_DATETIME_FORMAT)
|
||||||
from odoo import models, fields, api, _
|
from odoo import models, fields, api, _
|
||||||
@@ -75,6 +77,7 @@ class HotelReservation(models.Model):
|
|||||||
return folio.room_lines[0].departure_hour
|
return folio.room_lines[0].departure_hour
|
||||||
else:
|
else:
|
||||||
return default_departure_hour
|
return default_departure_hour
|
||||||
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def name_search(self, name='', args=None, operator='ilike', limit=100):
|
def name_search(self, name='', args=None, operator='ilike', limit=100):
|
||||||
@@ -115,6 +118,7 @@ class HotelReservation(models.Model):
|
|||||||
).days
|
).days
|
||||||
|
|
||||||
name = fields.Text('Reservation Description', required=True)
|
name = fields.Text('Reservation Description', required=True)
|
||||||
|
sequence = fields.Integer(string='Sequence', default=10)
|
||||||
|
|
||||||
room_id = fields.Many2one('hotel.room', string='Room')
|
room_id = fields.Many2one('hotel.room', string='Room')
|
||||||
|
|
||||||
@@ -230,8 +234,6 @@ 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)
|
# 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_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 = 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',
|
currency_id = fields.Many2one('res.currency',
|
||||||
related='pricelist_id.currency_id',
|
related='pricelist_id.currency_id',
|
||||||
string='Currency', readonly=True, required=True)
|
string='Currency', readonly=True, required=True)
|
||||||
@@ -244,12 +246,13 @@ class HotelReservation(models.Model):
|
|||||||
tax_id = fields.Many2many('account.tax',
|
tax_id = fields.Many2many('account.tax',
|
||||||
string='Taxes',
|
string='Taxes',
|
||||||
domain=['|', ('active', '=', False), ('active', '=', True)])
|
domain=['|', ('active', '=', False), ('active', '=', True)])
|
||||||
# qty_to_invoice = fields.Float(
|
qty_to_invoice = fields.Float(
|
||||||
# string='To Invoice', store=True, readonly=True,
|
compute='_get_to_invoice_qty', string='To Invoice', store=True, readonly=True,
|
||||||
# digits=dp.get_precision('Product Unit of Measure'))
|
digits=dp.get_precision('Product Unit of Measure'))
|
||||||
# qty_invoiced = fields.Float(
|
qty_invoiced = fields.Float(
|
||||||
# compute='_get_invoice_qty', string='Invoiced', store=True, readonly=True,
|
compute='_get_invoice_qty', string='Invoiced', store=True, readonly=True,
|
||||||
# digits=dp.get_precision('Product Unit of Measure'))
|
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)
|
||||||
# qty_delivered = fields.Float(string='Delivered', copy=False, digits=dp.get_precision('Product Unit of Measure'), default=0.0)
|
# 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)
|
# qty_delivered_updateable = fields.Boolean(compute='_compute_qty_delivered_updateable', string='Can Edit Delivered', readonly=True, default=True)
|
||||||
price_subtotal = fields.Monetary(string='Subtotal',
|
price_subtotal = fields.Monetary(string='Subtotal',
|
||||||
@@ -275,7 +278,7 @@ class HotelReservation(models.Model):
|
|||||||
# FIXME discount per night
|
# FIXME discount per night
|
||||||
discount = fields.Float(string='Discount (%)', digits=dp.get_precision('Discount'), default=0.0)
|
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
|
@api.model
|
||||||
def create(self, vals):
|
def create(self, vals):
|
||||||
@@ -1124,3 +1127,91 @@ class HotelReservation(models.Model):
|
|||||||
@api.multi
|
@api.multi
|
||||||
def send_cancel_mail(self):
|
def send_cancel_mail(self):
|
||||||
return self.folio_id.send_cancel_mail()
|
return self.folio_id.send_cancel_mail()
|
||||||
|
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
@api.depends('invoice_lines.invoice_id.state', 'invoice_lines.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_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
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ class HotelReservationLine(models.Model):
|
|||||||
discount = fields.Float(
|
discount = fields.Float(
|
||||||
string='Discount (%)',
|
string='Discount (%)',
|
||||||
digits=dp.get_precision('Discount'), default=0.0)
|
digits=dp.get_precision('Discount'), default=0.0)
|
||||||
|
invoiced = fields.Boolean('Invoiced')
|
||||||
|
|
||||||
|
|
||||||
@api.constrains('date')
|
@api.constrains('date')
|
||||||
def constrains_duplicated_date(self):
|
def constrains_duplicated_date(self):
|
||||||
|
|||||||
@@ -45,16 +45,17 @@ class AccountInvoice(models.Model):
|
|||||||
@api.multi
|
@api.multi
|
||||||
def _compute_dif_customer_payment(self):
|
def _compute_dif_customer_payment(self):
|
||||||
for inv in self:
|
for inv in self:
|
||||||
sales = inv.mapped('invoice_line_ids.sale_line_ids.order_id')
|
return False
|
||||||
folios = self.env['hotel.folio'].search([('order_id.id','in',sales.ids)])
|
#~ sales = inv.mapped('invoice_line_ids.sale_line_ids.order_id')
|
||||||
if folios:
|
#~ folios = self.env['hotel.folio'].search([('order_id.id','in',sales.ids)])
|
||||||
inv.from_folio = True
|
#~ if folios:
|
||||||
inv.folio_ids = [(6, 0, folios.ids)]
|
#~ inv.from_folio = True
|
||||||
payments_obj = self.env['account.payment']
|
#~ inv.folio_ids = [(6, 0, folios.ids)]
|
||||||
payments = payments_obj.search([('folio_id','in',folios.ids)])
|
#~ payments_obj = self.env['account.payment']
|
||||||
for pay in payments:
|
#~ payments = payments_obj.search([('folio_id','in',folios.ids)])
|
||||||
if pay.partner_id != inv.partner_id:
|
#~ for pay in payments:
|
||||||
inv.dif_customer_payment = True
|
#~ if pay.partner_id != inv.partner_id:
|
||||||
|
#~ inv.dif_customer_payment = True
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def action_invoice_open(self):
|
def action_invoice_open(self):
|
||||||
|
|||||||
13
hotel/models/inherited_account_invoice_line.py
Normal file
13
hotel/models/inherited_account_invoice_line.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# 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_line_invoice_rel',
|
||||||
|
'invoice_line_id', 'reservation_id',
|
||||||
|
string='Reservations', readonly=True, copy=False)
|
||||||
@@ -20,9 +20,9 @@
|
|||||||
attrs="{'invisible': [('has_cancelled_reservations_to_send', '=', False)]}" class="oe_highlight"/> -->
|
attrs="{'invisible': [('has_cancelled_reservations_to_send', '=', False)]}" class="oe_highlight"/> -->
|
||||||
<!-- <button name="send_exit_mail" type="object" string="Send Exit Email"
|
<!-- <button name="send_exit_mail" type="object" string="Send Exit Email"
|
||||||
attrs="{'invisible': [('has_checkout_to_send', '=', False)]}" class="oe_highlight"/> -->
|
attrs="{'invisible': [('has_checkout_to_send', '=', False)]}" class="oe_highlight"/> -->
|
||||||
<!-- <button name="%(hotel.action_view_folio_advance_payment_inv)d"
|
<button name="%(hotel.action_view_folio_advance_payment_inv)d"
|
||||||
string="Create Invoice" type="action" class="btn-primary" states="sale"
|
string="Create Invoice" type="action" class="btn-primary" states="sale"
|
||||||
attrs="{'invisible': [('invoice_status', '!=', 'to invoice')]}"/> -->
|
attrs="{'invisible': [('state', '!=', 'confirm')]}"/>
|
||||||
<!-- <button name="action_cancel_draft" states="cancel,sale" string="Set to Draft"
|
<!-- <button name="action_cancel_draft" states="cancel,sale" string="Set to Draft"
|
||||||
type="object" icon="fa-undo" class="oe_highlight" /> -->
|
type="object" icon="fa-undo" class="oe_highlight" /> -->
|
||||||
<button name="action_cancel" string="Cancel Folio" states="sale"
|
<button name="action_cancel" string="Cancel Folio" states="sale"
|
||||||
@@ -132,10 +132,12 @@
|
|||||||
<field name="email" placeholder="email"/>
|
<field name="email" placeholder="email"/>
|
||||||
<field name="mobile" placeholder="mobile"/>
|
<field name="mobile" placeholder="mobile"/>
|
||||||
<field name="phone" />
|
<field name="phone" />
|
||||||
|
<field name="partner_invoice_id" />
|
||||||
<field name="cancelled_reason" attrs="{'invisible':[('state','not in',('cancel'))]}"/>
|
<field name="cancelled_reason" attrs="{'invisible':[('state','not in',('cancel'))]}"/>
|
||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<field name="pricelist_id" domain="[('type','=','sale')]" />
|
<field name="pricelist_id" domain="[('type','=','sale')]" />
|
||||||
|
<field name="company_id" />
|
||||||
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
|
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
|
||||||
<field name="reservation_type" attrs="{'readonly':[('state','not in',('draft'))]}"/>
|
<field name="reservation_type" attrs="{'readonly':[('state','not in',('draft'))]}"/>
|
||||||
<field name="channel_type" attrs="{'required':[('reservation_type','=','normal')]}"/>
|
<field name="channel_type" attrs="{'required':[('reservation_type','=','normal')]}"/>
|
||||||
|
|||||||
@@ -16,14 +16,7 @@ class FolioAdvancePaymentInv(models.TransientModel):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _get_advance_payment_method(self):
|
def _get_advance_payment_method(self):
|
||||||
if self._count() == 1:
|
return 'all'
|
||||||
sale_obj = self.env['sale.order']
|
|
||||||
folio_obj = self.env['hotel.folio']
|
|
||||||
order = sale_obj.browse(folio_obj.mapped('order_id.id'))
|
|
||||||
if all([line.product_id.invoice_policy == 'order' for line in order.order_line]) \
|
|
||||||
or order.invoice_count:
|
|
||||||
return 'all'
|
|
||||||
return 'delivered'
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _default_product_id(self):
|
def _default_product_id(self):
|
||||||
@@ -40,15 +33,22 @@ class FolioAdvancePaymentInv(models.TransientModel):
|
|||||||
return self._default_product_id().taxes_id
|
return self._default_product_id().taxes_id
|
||||||
|
|
||||||
advance_payment_method = fields.Selection([
|
advance_payment_method = fields.Selection([
|
||||||
('delivered', 'Invoiceable lines'),
|
|
||||||
('all', 'Invoiceable lines (deduct down payments)'),
|
('all', 'Invoiceable lines (deduct down payments)'),
|
||||||
('percentage', 'Down payment (percentage)'),
|
('percentage', 'Down payment (percentage)'),
|
||||||
('fixed', 'Down payment (fixed amount)')
|
('fixed', 'Down payment (fixed amount)')
|
||||||
], string='What do you want to invoice?', default=_get_advance_payment_method,
|
], string='What do you want to invoice?', default=_get_advance_payment_method,
|
||||||
required=True)
|
required=True)
|
||||||
|
count = fields.Integer(default=_count, string='# of Orders')
|
||||||
|
folio_ids = fields.Many2many("hotel.folio", string="Folios",
|
||||||
|
help="Folios grouped")
|
||||||
|
group_folios = fields.Boolean('Group Folios')
|
||||||
|
line_ids = fields.One2many('line.advance.inv',
|
||||||
|
'advance_inv_id',
|
||||||
|
string="Invoice Lines")
|
||||||
|
view_detail = fields.Boolean('View Detail')
|
||||||
|
#Advance Payment
|
||||||
product_id = fields.Many2one('product.product', string='Down Payment Product',
|
product_id = fields.Many2one('product.product', string='Down Payment Product',
|
||||||
domain=[('type', '=', 'service')], default=_default_product_id)
|
domain=[('type', '=', 'service')], default=_default_product_id)
|
||||||
count = fields.Integer(default=_count, string='# of Orders')
|
|
||||||
amount = fields.Float('Down Payment Amount',
|
amount = fields.Float('Down Payment Amount',
|
||||||
digits=dp.get_precision('Account'),
|
digits=dp.get_precision('Account'),
|
||||||
help="The amount to be invoiced in advance, taxes excluded.")
|
help="The amount to be invoiced in advance, taxes excluded.")
|
||||||
@@ -139,12 +139,11 @@ class FolioAdvancePaymentInv(models.TransientModel):
|
|||||||
@api.multi
|
@api.multi
|
||||||
def create_invoices(self):
|
def create_invoices(self):
|
||||||
folios = self.env['hotel.folio'].browse(self._context.get('active_ids', []))
|
folios = self.env['hotel.folio'].browse(self._context.get('active_ids', []))
|
||||||
sale_orders = self.env['sale.order'].browse(folios.mapped('order_id.id'))
|
|
||||||
|
|
||||||
if self.advance_payment_method == 'delivered':
|
if self.advance_payment_method == 'delivered':
|
||||||
sale_orders.action_invoice_create()
|
folios.action_invoice_create()
|
||||||
elif self.advance_payment_method == 'all':
|
elif self.advance_payment_method == 'all':
|
||||||
sale_orders.action_invoice_create(final=True)
|
folios.action_invoice_create()
|
||||||
else:
|
else:
|
||||||
# Create deposit product if necessary
|
# Create deposit product if necessary
|
||||||
if not self.product_id:
|
if not self.product_id:
|
||||||
@@ -196,3 +195,68 @@ class FolioAdvancePaymentInv(models.TransientModel):
|
|||||||
'property_account_income_id': self.deposit_account_id.id,
|
'property_account_income_id': self.deposit_account_id.id,
|
||||||
'taxes_id': [(6, 0, self.deposit_taxes_id.ids)],
|
'taxes_id': [(6, 0, self.deposit_taxes_id.ids)],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@api.onchange('view_detail')
|
||||||
|
def prepare_reservation_invoice_lines(self):
|
||||||
|
vals = []
|
||||||
|
folios = self.env['hotel.folio'].browse(self._context.get('active_ids', []))
|
||||||
|
for folio in folios:
|
||||||
|
folio_name = folio.name
|
||||||
|
for reservation in folio.room_lines:
|
||||||
|
reservation_name = reservation.name
|
||||||
|
unit_price = False
|
||||||
|
discount = False
|
||||||
|
qty = 0
|
||||||
|
for day in reservation.reservation_line_ids.sorted('date'):
|
||||||
|
if day.price == unit_price and day.discount == discount:
|
||||||
|
date_to = day.date
|
||||||
|
qty += 1
|
||||||
|
else:
|
||||||
|
if unit_price:
|
||||||
|
vals.append((0, False, {
|
||||||
|
'date_from': date_from,
|
||||||
|
'date_to': date_to,
|
||||||
|
'room_type_id': reservation.room_type_id,
|
||||||
|
'product_id': self.env['product.product'].browse(
|
||||||
|
reservation.room_type_id.product_id.id
|
||||||
|
),
|
||||||
|
'qty': qty,
|
||||||
|
'discount': discount,
|
||||||
|
'unit_price': unit_price
|
||||||
|
}))
|
||||||
|
qty = 1
|
||||||
|
unit_price = day.price
|
||||||
|
date_from = day.date
|
||||||
|
date_to = day.date
|
||||||
|
vals.append((0, False, {
|
||||||
|
'date_from': date_from,
|
||||||
|
'date_to': date_to,
|
||||||
|
'room_type_id': reservation.room_type_id,
|
||||||
|
'product_id': self.env['product.product'].browse(
|
||||||
|
reservation.room_type_id.product_id.id
|
||||||
|
),
|
||||||
|
'qty': qty,
|
||||||
|
'discount': discount,
|
||||||
|
'unit_price': unit_price
|
||||||
|
}))
|
||||||
|
self.line_ids = vals
|
||||||
|
|
||||||
|
class LineAdvancePaymentInv(models.TransientModel):
|
||||||
|
_name = "line.advance.inv"
|
||||||
|
_description = "Lines Advance Invoice"
|
||||||
|
|
||||||
|
date_from = fields.Date('From')
|
||||||
|
date_to = fields.Date('To')
|
||||||
|
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')
|
||||||
|
unit_price = fields.Float('Price')
|
||||||
|
advance_inv_id = fields.Many2one('folio.advance.payment.inv')
|
||||||
|
discount = fields.Float(
|
||||||
|
string='Discount (%)',
|
||||||
|
digits=dp.get_precision('Discount'), default=0.0)
|
||||||
|
to_invoice = fields.Boolean('To Invoice')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,23 @@
|
|||||||
<field name="deposit_taxes_id" class="oe_inline" widget="many2many_tags"
|
<field name="deposit_taxes_id" class="oe_inline" widget="many2many_tags"
|
||||||
domain="[('type_tax_use','=','sale')]"
|
domain="[('type_tax_use','=','sale')]"
|
||||||
attrs="{'invisible': ['|', ('advance_payment_method', 'not in', ('fixed', 'percentage')), ('product_id', '!=', False)]}"/>
|
attrs="{'invisible': ['|', ('advance_payment_method', 'not in', ('fixed', 'percentage')), ('product_id', '!=', False)]}"/>
|
||||||
|
<field name="view_detail" />
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="line_ids" attrs="{'invisible': [('view_detail', '=', False)]}">
|
||||||
|
<tree string="Lines" editable="bottom">
|
||||||
|
<field name="room_type_id" />
|
||||||
|
<field name="date_from" />
|
||||||
|
<field name="date_to" />
|
||||||
|
<field name="qty" />
|
||||||
|
<field name="discount" />
|
||||||
|
<field name="unit_price" />
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
</group>
|
</group>
|
||||||
<footer>
|
<footer>
|
||||||
|
<button name="prepare_reservation_invoice_lines" string="Prepare Lines" type="object"
|
||||||
|
class="btn-primary"/>
|
||||||
<button name="create_invoices" string="Create and View Invoices" type="object"
|
<button name="create_invoices" string="Create and View Invoices" type="object"
|
||||||
context="{'open_invoices': True}" class="btn-primary"/>
|
context="{'open_invoices': True}" class="btn-primary"/>
|
||||||
<button name="create_invoices" string="Create Invoices" type="object"
|
<button name="create_invoices" string="Create Invoices" type="object"
|
||||||
|
|||||||
Reference in New Issue
Block a user