14.0 pms account flow (#36)

* [WIP] Basic tests definition

* [DEL] Default diff invoicing

* [WIP] Reservation refact invoice fields

* [FIX] test price without taxes

* [FIX] Security csv merge

* [WIP]pms: Wizard adv inv views

* [ADD] Wizard Filter Invoice Days

* [WIP] Payment WorkFlow
This commit is contained in:
Darío Lodeiros
2021-01-20 11:41:31 +01:00
committed by GitHub
parent 7285dc3dc0
commit 19fffec6ba
42 changed files with 3025 additions and 580 deletions

View File

@@ -35,6 +35,8 @@
# "templates/pms_email_template.xml",
"views/general.xml",
"data/menus.xml",
"wizards/wizard_payment_folio.xml",
"wizards/folio_make_invoice_advance_views.xml",
"views/pms_amenity_views.xml",
"views/pms_amenity_type_views.xml",
"views/pms_board_service_views.xml",
@@ -50,6 +52,7 @@
"views/pms_room_closure_reason_views.xml",
"views/account_payment_views.xml",
"views/account_move_views.xml",
"views/account_bank_statement_views.xml",
"views/res_users_views.xml",
"views/pms_room_type_class_views.xml",
"views/pms_room_type_availability_plan_views.xml",
@@ -64,10 +67,12 @@
"views/product_template_views.xml",
"views/webclient_templates.xml",
"views/ir_sequence_views.xml",
"views/account_journal_views.xml",
"wizards/wizard_reservation.xml",
"wizards/wizard_massive_changes.xml",
"wizards/wizard_advanced_filters.xml",
"wizards/wizard_folio.xml",
"wizards/wizard_invoice_filter_days.xml",
],
"demo": [
"demo/pms_master_data.xml",

View File

@@ -119,24 +119,28 @@
<field name="room_type_id" ref="pms_room_type_0" />
<field name="floor_id" ref="pms_floor_1" />
<field name="capacity">2</field>
<field name="pms_property_id" ref="pms.main_pms_property" />
</record>
<record id="pms_room_1" model="pms.room">
<field name="name">Single-101</field>
<field name="room_type_id" ref="pms_room_type_1" />
<field name="floor_id" ref="pms_floor_1" />
<field name="capacity">1</field>
<field name="pms_property_id" ref="pms.main_pms_property" />
</record>
<record id="pms_room_2" model="pms.room">
<field name="name">Single-102</field>
<field name="room_type_id" ref="pms_room_type_1" />
<field name="floor_id" ref="pms_floor_1" />
<field name="capacity">1</field>
<field name="pms_property_id" ref="pms.main_pms_property" />
</record>
<record id="pms_room_3" model="pms.room">
<field name="name">Single-103</field>
<field name="room_type_id" ref="pms_room_type_1" />
<field name="floor_id" ref="pms_floor_1" />
<field name="capacity">1</field>
<field name="pms_property_id" ref="pms.main_pms_property" />
</record>
<record id="pms_room_4" model="pms.room">
<field name="name">Double-201</field>
@@ -144,24 +148,28 @@
<field name="floor_id" ref="pms_floor_2" />
<field name="capacity">2</field>
<field name="extra_beds_allowed">1</field>
<field name="pms_property_id" ref="pms.main_pms_property" />
</record>
<record id="pms_room_5" model="pms.room">
<field name="name">Double-202</field>
<field name="room_type_id" ref="pms_room_type_2" />
<field name="floor_id" ref="pms_floor_2" />
<field name="capacity">2</field>
<field name="pms_property_id" ref="pms.main_pms_property" />
</record>
<record id="pms_room_6" model="pms.room">
<field name="name">Triple-203</field>
<field name="room_type_id" ref="pms_room_type_3" />
<field name="floor_id" ref="pms_floor_2" />
<field name="capacity">3</field>
<field name="pms_property_id" ref="pms.main_pms_property" />
</record>
<record id="pms_room_7" model="pms.room">
<field name="name">Open Talk Away Room</field>
<field name="room_type_id" ref="pms_room_type_4" />
<field name="floor_id" ref="pms_floor_0" />
<field name="capacity">1</field>
<field name="pms_property_id" ref="pms.main_pms_property" />
</record>
<!-- product.product for pms services -->
<record id="pms_service_0" model="product.product">

View File

@@ -41,3 +41,7 @@ from . import pms_board_service_room_type_line
from . import pms_board_service_line
from . import account_move_line
from . import pms_cancelation_rule
from . import folio_sale_line
from . import account_bank_statement_line
from . import account_bank_statement
from . import account_journal

View File

@@ -0,0 +1,7 @@
from odoo import fields, models
class AccountBankStatement(models.Model):
_inherit = "account.bank.statement"
property_id = fields.Many2one("pms.property", string="Property", copy=False)

View File

@@ -0,0 +1,29 @@
from odoo import api, fields, models
class AccountBankStatementLine(models.Model):
_inherit = "account.bank.statement.line"
statement_folio_ids = fields.Many2many(
"pms.folio", string="Folios", ondelete="cascade"
)
reservation_ids = fields.Many2many(
"pms.reservation", string="Reservations", ondelete="cascade"
)
service_ids = fields.Many2many("pms.service", string="Services", ondelete="cascade")
@api.model
def _prepare_move_line_default_vals(self, counterpart_account_id=None):
line_vals_list = super(
AccountBankStatementLine, self
)._prepare_move_line_default_vals(counterpart_account_id)
if self.statement_folio_ids:
for line in line_vals_list:
line.update(
{
"folio_ids": [(6, 0, self.statement_folio_ids.ids)],
"reservation_ids": [(6, 0, self.reservation_ids.ids)],
"service_ids": [(6, 0, self.service_ids.ids)],
}
)
return line_vals_list

View File

@@ -0,0 +1,21 @@
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class AccountJournal(models.Model):
_inherit = "account.journal"
pms_property_ids = fields.Many2many("pms.property", string="Property", copy=False)
@api.constrains("pms_property_ids", "company_id")
def _check_property_company_integrity(self):
for rec in self:
if rec.company_id and rec.pms_property_ids:
property_companies = rec.pms_property_ids.mapped("company_id")
if len(property_companies) > 1 or rec.company_id != property_companies:
raise UserError(
_(
"The company of the properties must match "
"the company on account journal"
)
)

View File

@@ -1,4 +1,3 @@
# Copyright 2017 Alexandre Díaz
# Copyright 2017 Dario Lodeiros
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
import json
@@ -15,7 +14,7 @@ class AccountMove(models.Model):
comodel_name="pms.folio", compute="_compute_folio_origin"
)
pms_property_id = fields.Many2one("pms.property")
from_folio = fields.Boolean(compute="_compute_folio_origin")
from_reservation = fields.Boolean(compute="_compute_from_reservation")
outstanding_folios_debits_widget = fields.Text(
compute="_compute_get_outstanding_folios_JSON"
)
@@ -27,14 +26,17 @@ class AccountMove(models.Model):
def _compute_folio_origin(self):
for inv in self:
inv.from_folio = False
inv.folio_ids = False
folios = inv.mapped("invoice_line_ids.reservation_ids.folio_id")
folios |= inv.mapped("invoice_line_ids.service_ids.folio_id")
folios = inv.mapped("invoice_line_ids.folio_ids")
if folios:
inv.from_folio = True
inv.folio_ids = [(6, 0, folios.ids)]
def _compute_from_reservation(self):
for inv in self:
inv.from_reservation = False
if len(inv.invoice_line_ids.mapped("reservation_line_ids")) > 0:
inv.from_reservation = True
# Action methods
def action_folio_payments(self):
@@ -55,6 +57,7 @@ class AccountMove(models.Model):
}
# Business methods
def _compute_get_outstanding_folios_JSON(self):
self.ensure_one()
self.outstanding_folios_debits_widget = json.dumps(False)

View File

@@ -1,7 +1,7 @@
# Copyright 2017 Alexandre Díaz
# Copyright 2017 Dario Lodeiros
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
from odoo import fields, models
from odoo import api, fields, models
class AccountMoveLine(models.Model):
@@ -35,3 +35,82 @@ class AccountMoveLine(models.Model):
readonly=True,
copy=False,
)
folio_line_ids = fields.Many2many(
"folio.sale.line",
"folio_sale_line_invoice_rel",
"invoice_line_id",
"sale_line_id",
string="Folio Lines",
copy=False,
)
folio_ids = fields.Many2many(
"pms.folio",
"payment_folio_rel",
"move_id",
"folio_id",
string="Folios",
ondelete="cascade",
compute="_compute_folio_ids",
readonly=False,
store=True,
)
name = fields.Char(
compute="_compute_name",
readonly=False,
store=True,
)
@api.depends("service_ids", "reservation_ids")
def _compute_folio_ids(self):
for record in self:
if record.service_ids:
record.folio_ids = record.mapped("service_ids.folio_id")
elif record.reservation_ids:
record.folio_ids = record.mapped("reservation_ids.folio_id")
elif not record.folio_ids:
record.folio_ids = False
def invoice_filter_days(self):
action = self.env["ir.actions.act_window"]._for_xml_id(
"pms.pms_invoice_filter_days_action"
)
# Force the values of the move line in the context to avoid issues
ctx = dict(self.env.context)
ctx.pop("active_id", None)
ctx["active_ids"] = self.ids
ctx["active_model"] = "account.move.line"
action["context"] = ctx
return action
def _copy_data_extend_business_fields(self, values):
super(AccountMoveLine, self)._copy_data_extend_business_fields(values)
values["folio_line_ids"] = [(6, None, self.folio_line_ids.ids)]
values["reservation_line_ids"] = [(6, None, self.reservation_line_ids.ids)]
values["service_ids"] = [(6, None, self.service_ids.ids)]
values["reservation_ids"] = [(6, None, self.reservation_ids.ids)]
@api.depends("reservation_line_ids")
def _compute_name(self):
if hasattr(super(), "_compute_name"):
super()._compute_field()
for record in self:
if record.reservation_line_ids:
record.name = record._get_compute_name()
def _get_compute_name(self):
self.ensure_one()
if self.reservation_line_ids:
month = False
name = False
lines = self.reservation_line_ids.sorted("date")
for date in lines.mapped("date"):
if date.month != month:
name = name + "\n" if name else ""
name += date.strftime("%B-%Y") + ": "
name += date.strftime("%d")
month = date.month
else:
name += ", " + date.strftime("%d")
return name
else:
return False

View File

@@ -1,7 +1,6 @@
# Copyright 2017 Dario Lodeiros
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.exceptions import except_orm
from odoo import _, fields, models
class AccountPayment(models.Model):
@@ -9,78 +8,6 @@ class AccountPayment(models.Model):
# Fields declaration
folio_id = fields.Many2one("pms.folio", string="Folio Reference")
amount_total_folio = fields.Float(
compute="_compute_folio_amount",
store=True,
string="Total amount in folio",
)
save_amount = fields.Monetary(string="onchange_amount")
save_date = fields.Date()
save_journal_id = fields.Integer()
# Compute and Search methods
@api.depends("state")
def _compute_folio_amount(self):
# FIXME: Finalize method
res = []
fol = ()
for payment in self:
if payment.folio_id:
fol = payment.env["pms.folio"].search(
[("id", "=", payment.folio_id.id)]
)
else:
return
if not any(fol):
return
if len(fol) > 1:
raise except_orm(
_("Warning"),
_(
"This pay is related with \
more than one Reservation."
),
)
else:
fol.compute_amount()
return res
# Constraints and onchanges
# @api.onchange("amount", "payment_date", "journal_id")
# def onchange_amount(self):
# if self._origin:
# self.save_amount = self._origin.amount
# self.save_journal_id = self._origin.journal_id.id
# self.save_date = self._origin.payment_date
# Action methods
# def return_payment_folio(self):
# journal = self.journal_id
# partner = self.partner_id
# amount = self.amount
# reference = self.communication
# account_move_lines = self.move_line_ids.filtered(
# lambda x: (x.account_id.internal_type == "receivable")
# )
# return_line_vals = {
# "move_line_ids": [(6, False, [x.id for x in account_move_lines])],
# "partner_id": partner.id,
# "amount": amount,
# "reference": reference,
# }
# return_vals = {
# "journal_id": journal.id,
# "line_ids": [(0, 0, return_line_vals)],
# }
# return_pay = self.env["payment.return"].create(return_vals)
# if self.save_amount:
# self.amount = self.save_amount
# if self.save_date:
# self.payment_date = self.save_date
# if self.save_journal_id:
# self.journal_id = self.env["account.journal"].browse(self.save_journal_id)
# return_pay.action_confirm()
# Business methods

View File

@@ -0,0 +1,857 @@
# Copyright 2020 Dario Lodeiros
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.osv import expression
from odoo.tools import float_compare, float_is_zero
class FolioSaleLine(models.Model):
_name = "folio.sale.line"
_description = "Folio Sale Line"
_order = "folio_id, sequence, id"
_check_company_auto = True
@api.depends("state", "product_uom_qty", "qty_to_invoice", "qty_invoiced")
def _compute_invoice_status(self):
"""
Compute the invoice status of a SO line:
Its if compute based on reservations/services associated status
"""
precision = self.env["decimal.precision"].precision_get(
"Product Unit of Measure"
)
for line in self:
if line.state == "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_uom_qty,
precision_digits=precision,
)
>= 0
):
line.invoice_status = "invoiced"
else:
line.invoice_status = "no"
@api.depends("reservation_line_ids", "service_id")
def _compute_name(self):
for record in self:
if not record.name_updated:
record.name = record._get_compute_name()
@api.depends("name")
def _compute_name_updated(self):
self.name_updated = False
for record in self.filtered("name"):
if record.name != record._get_compute_name():
record.name_updated = True
def _get_compute_name(self):
self.ensure_one()
if self.reservation_line_ids:
month = False
name = False
lines = self.reservation_line_ids.sorted("date")
for date in lines.mapped("date"):
if date.month != month:
name = name + "\n" if name else ""
name += date.strftime("%B-%Y") + ": "
name += date.strftime("%d")
month = date.month
else:
name += ", " + date.strftime("%d")
return name
elif self.service_id:
return self.service_id.name
else:
return False
@api.depends("service_id", "service_id.price_unit")
def _compute_price_unit(self):
"""
Compute unit prices of services
On reservations the unit price is compute by group in folio
"""
for record in self:
if record.service_id:
record.price_unit = record.service_id.price_unit
elif not record.price_unit:
record.price_unit = False
@api.depends("product_uom_qty", "discount", "price_unit", "tax_ids")
def _compute_amount(self):
"""
Compute the amounts of the Sale line.
"""
for line in self:
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
taxes = line.tax_ids.compute_all(
price,
line.folio_id.currency_id,
line.product_uom_qty,
product=line.product_id,
)
line.update(
{
"price_tax": sum(
t.get("amount", 0.0) for t in taxes.get("taxes", [])
),
"price_total": taxes["total_included"],
"price_subtotal": taxes["total_excluded"],
}
)
if self.env.context.get(
"import_file", False
) and not self.env.user.user_has_groups("account.group_account_manager"):
line.tax_ids.invalidate_cache(
["invoice_repartition_line_ids"], [line.tax_ids.id]
)
@api.depends("reservation_id.tax_ids", "service_id.tax_ids")
def _compute_tax_ids(self):
for record in self:
record.tax_ids = (
record.service_id.tax_ids
if record.service_id
else record.reservation_id.tax_ids
)
@api.depends("service_id", "service_id.discount")
def _compute_discount(self):
self.discount = 0.0
for record in self.filtered("service_id"):
record.discount = record.service_id.discount
@api.depends("reservation_id.room_type_id", "service_id.product_id")
def _compute_product_id(self):
for record in self:
if record.reservation_id:
record.product_id = record.reservation_id.room_type_id.product_id
elif record.service_id:
record.product_id = record.service_id.product_id
else:
record.product_id = False
# @api.depends('product_id', 'folio_id.state', 'qty_invoiced', 'qty_delivered')
# def _compute_product_updatable(self):
# for line in self:
# if line.state in ['done', 'cancel'] or (
# line.state == 'sale' and (
# line.qty_invoiced > 0 or line.qty_delivered > 0)):
# line.product_updatable = False
# else:
# line.product_updatable = True
# no trigger product_id.invoice_policy to avoid retroactively changing SO
@api.depends("qty_invoiced", "product_uom_qty", "folio_id.state")
def _compute_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_uom_qty - line.qty_invoiced
else:
line.qty_to_invoice = 0
@api.depends(
"invoice_lines.move_id.state",
"invoice_lines.quantity",
"untaxed_amount_to_invoice",
)
def _compute_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 Folio 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 Folio
"""
for line in self:
qty_invoiced = 0.0
for invoice_line in line.invoice_lines:
if invoice_line.move_id.state != "cancel":
if invoice_line.move_id.move_type == "out_invoice":
qty_invoiced += invoice_line.product_uom_id._compute_quantity(
invoice_line.quantity, line.product_uom
)
elif invoice_line.move_id.move_type == "out_refund":
if (
not line.is_downpayment
or line.untaxed_amount_to_invoice == 0
):
qty_invoiced -= (
invoice_line.product_uom_id._compute_quantity(
invoice_line.quantity, line.product_uom
)
)
line.qty_invoiced = qty_invoiced
@api.depends("price_unit", "discount")
def _compute_get_price_reduce(self):
for line in self:
line.price_reduce = line.price_unit * (1.0 - line.discount / 100.0)
@api.depends("price_total", "product_uom_qty")
def _compute_get_price_reduce_tax(self):
for line in self:
line.price_reduce_taxinc = (
line.price_total / line.product_uom_qty if line.product_uom_qty else 0.0
)
@api.depends("price_subtotal", "product_uom_qty")
def _compute_get_price_reduce_notax(self):
for line in self:
line.price_reduce_taxexcl = (
line.price_subtotal / line.product_uom_qty
if line.product_uom_qty
else 0.0
)
# @api.model
# def _prepare_add_missing_fields(self, values):
# """ Deduce missing required fields from the onchange """
# res = {}
# onchange_fields = ['name', 'price_unit', 'product_uom', 'tax_ids']
# if values.get('folio_id') and values.get('product_id') and any(
# f not in values for f in onchange_fields
# ):
# line = self.new(values)
# line.product_id_change()
# for field in onchange_fields:
# if field not in values:
# res[field] = line._fields[field].convert_to_write(
# line[field], line
# )
# return res
# @api.model_create_multi
# def create(self, vals_list):
# for values in vals_list:
# if values.get('display_type', self.default_get(
# ['display_type'])['display_type']
# ):
# values.update(product_id=False, price_unit=0,
# product_uom_qty=0, product_uom=False,
# customer_lead=0)
# values.update(self._prepare_add_missing_fields(values))
# lines = super().create(vals_list)
# for line in lines:
# if line.product_id and line.folio_id.state == 'sale':
# msg = _("Extra line with %s ") % (line.product_id.display_name,)
# line.folio_id.message_post(body=msg)
# # create an analytic account if at least an expense product
# if line.product_id.expense_policy not in [False, 'no'] and \
# not line.folio_id.analytic_account_id:
# line.folio_id._create_analytic_account()
# return lines
# _sql_constraints = [
# ('accountable_required_fields',
# "CHECK(display_type IS NOT NULL OR \
# (product_id IS NOT NULL AND product_uom IS NOT NULL))",
# "Missing required fields on accountable sale order line."),
# ('non_accountable_null_fields',
# "CHECK(display_type IS NULL OR (product_id IS NULL AND \
# price_unit = 0 AND product_uom_qty = 0 AND \
# product_uom IS NULL AND customer_lead = 0))",
# "Forbidden values on non-accountable sale order line"),
# ]
def _update_line_quantity(self, values):
folios = self.mapped("folio_id")
for order in folios:
order_lines = self.filtered(lambda x: x.folio_id == order)
msg = "<b>" + _("The ordered quantity has been updated.") + "</b><ul>"
for line in order_lines:
msg += "<li> %s: <br/>" % line.product_id.display_name
msg += (
_(
"Ordered Quantity: %(old_qty)s -> %(new_qty)s",
old_qty=line.product_uom_qty,
new_qty=values["product_uom_qty"],
)
+ "<br/>"
)
# if line.product_id.type in ('consu', 'product'):
# msg += _("Delivered Quantity: %s", line.qty_delivered) + "<br/>"
msg += _("Invoiced Quantity: %s", line.qty_invoiced) + "<br/>"
msg += "</ul>"
order.message_post(body=msg)
# def write(self, values):
# if 'display_type' in values and self.filtered(
# lambda line: line.display_type != values.get('display_type')):
# raise UserError(_("You cannot change the type of a sale order line.\
# Instead you should delete the current line and create \
# a new line of the proper type."))
# if 'product_uom_qty' in values:
# precision = self.env['decimal.precision'].precision_get(
# 'Product Unit of Measure'
# )
# self.filtered(
# lambda r: r.state == 'sale' and \
# float_compare(
# r.product_uom_qty,
# values['product_uom_qty'],
# precision_digits=precision) != 0)._update_line_quantity(
# values
# )
# # Prevent writing on a locked SO.
# protected_fields = self._get_protected_fields()
# if 'done' in self.mapped('folio_id.state') and any(
# f in values.keys() for f in protected_fields
# ):
# protected_fields_modified = list(set(protected_fields) & set(
# values.keys()
# ))
# fields = self.env['ir.model.fields'].search([
# ('name', 'in', protected_fields_modified),
# ('model', '=', self._name)
# ])
# raise UserError(
# _('It is forbidden to modify the following \
# fields in a locked order:\n%s')
# % '\n'.join(fields.mapped('field_description'))
# )
# result = super(SaleOrderLine, self).write(values)
# return result
folio_id = fields.Many2one(
"pms.folio",
string="Folio Reference",
required=True,
ondelete="cascade",
index=True,
copy=False,
)
reservation_id = fields.Many2one(
"pms.reservation",
string="Reservation Reference",
ondelete="cascade",
index=True,
copy=False,
)
service_id = fields.Many2one(
"pms.service",
string="Service Reference",
ondelete="cascade",
index=True,
copy=False,
)
name = fields.Text(
string="Description", compute="_compute_name", store=True, readonly=False
)
name_updated = fields.Boolean(compute="_compute_name_updated", store=True)
reservation_line_ids = fields.Many2many(
"pms.reservation.line",
string="Nights",
)
sequence = fields.Integer(string="Sequence", default=10)
invoice_lines = fields.Many2many(
"account.move.line",
"folio_sale_line_invoice_rel",
"sale_line_id",
"invoice_line_id",
string="Invoice Lines",
copy=False,
)
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",
)
price_unit = fields.Float(
"Unit Price",
digits="Product Price",
compute="_compute_price_unit",
store=True,
)
price_subtotal = fields.Monetary(
compute="_compute_amount", string="Subtotal", readonly=True, store=True
)
price_tax = fields.Float(
compute="_compute_amount", string="Total Tax", readonly=True, store=True
)
price_total = fields.Monetary(
compute="_compute_amount", string="Total", readonly=True, store=True
)
price_reduce = fields.Float(
compute="_compute_get_price_reduce",
string="Price Reduce",
digits="Product Price",
readonly=True,
store=True,
)
tax_ids = fields.Many2many(
"account.tax",
compute="_compute_tax_ids",
store=True,
string="Taxes",
domain=["|", ("active", "=", False), ("active", "=", True)],
)
price_reduce_taxinc = fields.Monetary(
compute="_compute_get_price_reduce_tax",
string="Price Reduce Tax inc",
readonly=True,
store=True,
)
price_reduce_taxexcl = fields.Monetary(
compute="_compute_get_price_reduce_notax",
string="Price Reduce Tax excl",
readonly=True,
store=True,
)
discount = fields.Float(
string="Discount (%)",
digits="Discount",
compute="_compute_discount",
store=True,
)
product_id = fields.Many2one(
"product.product",
string="Product",
domain="[('sale_ok', '=', True),\
'|', ('company_id', '=', False), \
('company_id', '=', company_id)]",
change_default=True,
ondelete="restrict",
check_company=True,
compute="_compute_product_id",
store=True,
)
# product_updatable = fields.Boolean(
# compute='_compute_product_updatable',
# string='Can Edit Product',
# readonly=True,
# default=True)
product_uom_qty = fields.Float(
string="Quantity",
digits="Product Unit of Measure",
compute="_compute_product_uom_qty",
store=True,
readonly=False,
)
product_uom = fields.Many2one(
"uom.uom",
string="Unit of Measure",
domain="[('category_id', '=', product_uom_category_id)]",
)
product_uom_category_id = fields.Many2one(
related="product_id.uom_id.category_id", readonly=True
)
product_uom_readonly = fields.Boolean(compute="_compute_product_uom_readonly")
product_custom_attribute_value_ids = fields.One2many(
"product.attribute.custom.value",
"sale_order_line_id",
string="Custom Values",
copy=True,
)
qty_to_invoice = fields.Float(
compute="_compute_get_to_invoice_qty",
string="To Invoice Quantity",
store=True,
readonly=True,
digits="Product Unit of Measure",
)
qty_invoiced = fields.Float(
compute="_compute_get_invoice_qty",
string="Invoiced Quantity",
store=True,
readonly=True,
compute_sudo=True,
digits="Product Unit of Measure",
)
untaxed_amount_invoiced = fields.Monetary(
"Untaxed Invoiced Amount",
compute="_compute_untaxed_amount_invoiced",
compute_sudo=True,
store=True,
)
untaxed_amount_to_invoice = fields.Monetary(
"Untaxed Amount To Invoice",
compute="_compute_untaxed_amount_to_invoice",
compute_sudo=True,
store=True,
)
currency_id = fields.Many2one(
related="folio_id.currency_id",
depends=["folio_id.currency_id"],
store=True,
string="Currency",
readonly=True,
)
company_id = fields.Many2one(
related="folio_id.company_id",
string="Company",
store=True,
readonly=True,
index=True,
)
folio_partner_id = fields.Many2one(
related="folio_id.partner_id", store=True, string="Customer", readonly=False
)
analytic_tag_ids = fields.Many2many(
"account.analytic.tag",
string="Analytic Tags",
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
)
analytic_line_ids = fields.One2many(
"account.analytic.line", "so_line", string="Analytic lines"
)
is_downpayment = fields.Boolean(
string="Is a down payment",
help="Down payments are made when creating invoices from a folio."
" They are not copied when duplicating a folio.",
)
state = fields.Selection(
related="folio_id.state",
string="Folio Status",
readonly=True,
copy=False,
store=True,
)
display_type = fields.Selection(
[("line_section", "Section"), ("line_note", "Note")],
default=False,
help="Technical field for UX purpose.",
)
@api.depends("reservation_line_ids", "service_id")
def _compute_product_uom_qty(self):
for line in self:
if line.reservation_line_ids:
line.product_uom_qty = len(line.reservation_line_ids)
elif line.service_id:
line.product_uom_qty = line.service_id.product_qty
elif not line.product_uom_qty:
line.product_uom_qty = False
@api.depends("state")
def _compute_product_uom_readonly(self):
for line in self:
line.product_uom_readonly = line.state in ["sale", "done", "cancel"]
@api.depends(
"invoice_lines",
"invoice_lines.price_total",
"invoice_lines.move_id.state",
"invoice_lines.move_id.move_type",
)
def _compute_untaxed_amount_invoiced(self):
"""Compute the untaxed amount already invoiced from
the sale order line, taking the refund attached
the so line into account. This amount is computed as
SUM(inv_line.price_subtotal) - SUM(ref_line.price_subtotal)
where
`inv_line` is a customer invoice line linked to the SO line
`ref_line` is a customer credit note (refund) line linked to the SO line
"""
for line in self:
amount_invoiced = 0.0
for invoice_line in line.invoice_lines:
if invoice_line.move_id.state == "posted":
invoice_date = (
invoice_line.move_id.invoice_date or fields.Date.today()
)
if invoice_line.move_id.move_type == "out_invoice":
amount_invoiced += invoice_line.currency_id._convert(
invoice_line.price_subtotal,
line.currency_id,
line.company_id,
invoice_date,
)
elif invoice_line.move_id.move_type == "out_refund":
amount_invoiced -= invoice_line.currency_id._convert(
invoice_line.price_subtotal,
line.currency_id,
line.company_id,
invoice_date,
)
line.untaxed_amount_invoiced = amount_invoiced
@api.depends(
"state",
"price_reduce",
"product_id",
"untaxed_amount_invoiced",
"product_uom_qty",
)
def _compute_untaxed_amount_to_invoice(self):
"""Total of remaining amount to invoice on the sale order line (taxes excl.) as
total_sol - amount already invoiced
where Total_sol depends on the invoice policy of the product.
Note: Draft invoice are ignored on purpose, the 'to invoice' amount should
come only from the SO lines.
"""
for line in self:
amount_to_invoice = 0.0
if line.state != "draft":
# Note: do not use price_subtotal field as it returns
# zero when the ordered quantity is zero.
# It causes problem for expense line (e.i.: ordered qty = 0,
# deli qty = 4, price_unit = 20 ; subtotal is zero),
# but when you can invoice the line,
# you see an amount and not zero.
# Since we compute untaxed amount, we can use directly the price
# reduce (to include discount) without using `compute_all()`
# method on taxes.
price_subtotal = 0.0
price_subtotal = line.price_reduce * line.product_uom_qty
if len(line.tax_ids.filtered(lambda tax: tax.price_include)) > 0:
# As included taxes are not excluded from the computed subtotal,
# `compute_all()` method has to be called to retrieve
# the subtotal without them.
# `price_reduce_taxexcl` cannot be used as it is computed from
# `price_subtotal` field. (see upper Note)
price_subtotal = line.tax_ids.compute_all(
price_subtotal,
currency=line.folio_id.currency_id,
quantity=line.product_uom_qty,
product=line.product_id,
partner=line.folio_id.partner_shipping_id,
)["total_excluded"]
if any(
line.invoice_lines.mapped(lambda l: l.discount != line.discount)
):
# In case of re-invoicing with different
# discount we try to calculate manually the
# remaining amount to invoice
amount = 0
for inv_line in line.invoice_lines:
if (
len(
inv_line.tax_ids.filtered(lambda tax: tax.price_include)
)
> 0
):
amount += inv_line.tax_ids.compute_all(
inv_line.currency_id._convert(
inv_line.price_unit,
line.currency_id,
line.company_id,
inv_line.date or fields.Date.today(),
round=False,
)
* inv_line.quantity
)["total_excluded"]
else:
amount += (
inv_line.currency_id._convert(
inv_line.price_unit,
line.currency_id,
line.company_id,
inv_line.date or fields.Date.today(),
round=False,
)
* inv_line.quantity
)
amount_to_invoice = max(price_subtotal - amount, 0)
else:
amount_to_invoice = price_subtotal - line.untaxed_amount_invoiced
line.untaxed_amount_to_invoice = amount_to_invoice
def _get_invoice_line_sequence(self, new=0, old=0):
"""
Method intended to be overridden in third-party
module if we want to prevent the resequencing of invoice lines.
:param int new: the new line sequence
:param int old: the old line sequence
:return: the sequence of the SO line, by default the new one.
"""
return new or old
def _prepare_invoice_line(self, **optional_values):
"""
Prepare the dict of values to create the new invoice line for a folio sale line.
:param qty: float quantity to invoice
:param optional_values: any parameter that
should be added to the returned invoice line
"""
self.ensure_one()
reservation = self.reservation_id
service = self.service_id
reservation_lines = self.reservation_line_ids.filtered(lambda l: not l.invoiced)
res = {
"display_type": self.display_type,
"sequence": self.sequence,
"name": self.name,
"product_id": self.product_id.id,
"product_uom_id": self.product_uom.id,
"quantity": self.qty_to_invoice,
"discount": self.discount,
"price_unit": self.price_unit,
"tax_ids": [(6, 0, self.tax_ids.ids)],
"analytic_account_id": self.folio_id.analytic_account_id.id,
"analytic_tag_ids": [(6, 0, self.analytic_tag_ids.ids)],
"folio_line_ids": [(6, 0, [self.id])],
"reservation_ids": [(6, 0, reservation.ids)],
"service_ids": [(6, 0, service.ids)],
"reservation_line_ids": [(6, 0, reservation_lines.ids)],
}
if optional_values:
res.update(optional_values)
if self.display_type:
res["account_id"] = False
return res
def name_get(self):
result = []
for so_line in self.sudo():
name = "{} - {}".format(
so_line.folio_id.name,
so_line.name and so_line.name.split("\n")[0] or so_line.product_id.name,
)
result.append((so_line.id, name))
return result
@api.model
def _name_search(
self, name, args=None, operator="ilike", limit=100, name_get_uid=None
):
if operator in ("ilike", "like", "=", "=like", "=ilike"):
args = expression.AND(
[
args or [],
["|", ("folio_id.name", operator, name), ("name", operator, name)],
]
)
return super(FolioSaleLine, self)._name_search(
name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid
)
def _check_line_unlink(self):
"""
Check wether a line can be deleted or not.
Lines cannot be deleted if the folio is confirmed; downpayment
lines who have not yet been invoiced bypass that exception.
:rtype: recordset folio.sale.line
:returns: set of lines that cannot be deleted
"""
return self.filtered(
lambda line: line.state not in ("draft")
and (line.invoice_lines or not line.is_downpayment)
)
# def unlink(self):
# if self._check_line_unlink():
# raise UserError(
# _("""You can not remove an sale line once the sales
# folio is confirmed.\n
# You should rather set the quantity to 0.""")
# )
# return super(FolioSaleLine, self).unlink()
def _get_real_price_currency(self, product, rule_id, qty, uom, pricelist_id):
"""Retrieve the price before applying the pricelist
:param obj product: object of current product record
:parem float qty: total quentity of product
:param tuple price_and_rule: tuple(price, suitable_rule)
coming from pricelist computation
:param obj uom: unit of measure of current folio line
:param integer pricelist_id: pricelist id of folio"""
PricelistItem = self.env["product.pricelist.item"]
field_name = "lst_price"
currency_id = None
product_currency = product.currency_id
if rule_id:
pricelist_item = PricelistItem.browse(rule_id)
if pricelist_item.pricelist_id.discount_policy == "without_discount":
while (
pricelist_item.base == "pricelist"
and pricelist_item.base_pricelist_id
and pricelist_item.base_pricelist_id.discount_policy
== "without_discount"
):
price, rule_id = pricelist_item.base_pricelist_id.with_context(
uom=uom.id
).get_product_price_rule(product, qty, self.folio_id.partner_id)
pricelist_item = PricelistItem.browse(rule_id)
if pricelist_item.base == "standard_price":
field_name = "standard_price"
product_currency = product.cost_currency_id
elif (
pricelist_item.base == "pricelist" and pricelist_item.base_pricelist_id
):
field_name = "price"
product = product.with_context(
pricelist=pricelist_item.base_pricelist_id.id
)
product_currency = pricelist_item.base_pricelist_id.currency_id
currency_id = pricelist_item.pricelist_id.currency_id
if not currency_id:
currency_id = product_currency
cur_factor = 1.0
else:
if currency_id.id == product_currency.id:
cur_factor = 1.0
else:
cur_factor = currency_id._get_conversion_rate(
product_currency,
currency_id,
self.company_id or self.env.company,
self.folio_id.date_order or fields.Date.today(),
)
product_uom = self.env.context.get("uom") or product.uom_id.id
if uom and uom.id != product_uom:
# the unit price is in a different uom
uom_factor = uom._compute_price(1.0, product.uom_id)
else:
uom_factor = 1.0
return product[field_name] * uom_factor * cur_factor, currency_id
def _get_protected_fields(self):
return [
"product_id",
"name",
"price_unit",
"product_uom",
"product_uom_qty",
"tax_ids",
"analytic_tag_ids",
]

View File

@@ -106,7 +106,9 @@ class PmsBoardServiceRoomType(models.Model):
# Action methods
def open_board_lines_form(self):
action = self.env.ref("pms.action_pms_board_service_room_type_view").read()[0]
action = (
self.env.ref("pms.action_pms_board_service_room_type_view").sudo().read()[0]
)
action["views"] = [
(self.env.ref("pms.pms_board_service_room_type_form").id, "form")
]

View File

@@ -3,8 +3,11 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from itertools import groupby
from odoo import _, api, fields, models
from odoo.exceptions import AccessError, UserError
from odoo.tools import float_is_zero
_logger = logging.getLogger(__name__)
@@ -25,24 +28,21 @@ class PmsFolio(models.Model):
result.append((folio.id, name))
return result
@api.model
def _default_diff_invoicing(self):
"""
If the guest has an invoicing address set,
this method return diff_invoicing = True, else, return False
"""
if "folio_id" in self.env.context:
folio = self.env["pms.folio"].browse([self.env.context["folio_id"]])
if folio.partner_id.id == folio.partner_invoice_id.id:
return False
return True
@api.model
def _get_default_pms_property(self):
return (
self.env.user.pms_property_id
) # TODO: Change by property env variable (like company)
def _default_note(self):
return (
self.env["ir.config_parameter"]
.sudo()
.get_param("account.use_invoice_terms")
and self.env.company.invoice_terms
or ""
)
# Fields declaration
name = fields.Char(
string="Folio Number", readonly=True, index=True, default=lambda self: _("New")
@@ -78,12 +78,30 @@ class PmsFolio(models.Model):
help="Services detail provide to customer and it will "
"include in main Invoice.",
)
sale_line_ids = fields.One2many(
"folio.sale.line",
"folio_id",
compute="_compute_sale_line_ids",
compute_sudo=True,
store="True",
)
invoice_count = fields.Integer(
string="Invoice Count", compute="_compute_get_invoiced", readonly=True
)
company_id = fields.Many2one(
"res.company",
"Company",
required=True,
default=lambda self: self.env.company,
)
move_line_ids = fields.Many2many(
"account.move.line",
"payment_folio_rel",
"folio_id",
"move_id",
string="Payments",
readonly=True,
)
analytic_account_id = fields.Many2one(
"account.analytic.account",
"Analytic Account",
@@ -138,8 +156,15 @@ class PmsFolio(models.Model):
ondelete="restrict",
domain=[("channel_type", "=", "direct")],
)
payment_ids = fields.One2many("account.payment", "folio_id", readonly=True)
# return_ids = fields.One2many("payment.return", "folio_id", readonly=True)
transaction_ids = fields.Many2many(
"payment.transaction",
"folio_transaction_rel",
"folio_id",
"transaction_id",
string="Transactions",
copy=False,
readonly=True,
)
payment_term_id = fields.Many2one(
"account.payment.term",
string="Payment Terms",
@@ -172,9 +197,22 @@ class PmsFolio(models.Model):
"account.move",
string="Invoices",
compute="_compute_get_invoiced",
search="_search_invoice_ids",
readonly=True,
copy=False,
compute_sudo=True,
)
payment_state = fields.Selection(
selection=[
("not_paid", "Not Paid"),
("paid", "Paid"),
("partial", "Partially Paid"),
],
string="Payment Status",
store=True,
readonly=True,
copy=False,
tracking=True,
compute="_compute_amount",
)
partner_invoice_id = fields.Many2one(
"res.partner",
@@ -289,7 +327,7 @@ class PmsFolio(models.Model):
("no", "Nothing to Invoice"),
],
string="Invoice Status",
compute="_compute_get_invoiced",
compute="_compute_get_invoice_status",
store=True,
readonly=True,
default="no",
@@ -304,6 +342,12 @@ class PmsFolio(models.Model):
advance has not been recorded",
)
sequence = fields.Integer(string="Sequence", default=10)
note = fields.Text("Terms and conditions", default=_default_note)
reference = fields.Char(
string="Payment Ref.",
copy=False,
help="The payment communication of this sale order.",
)
# Compute and Search methods
@api.depends("reservation_ids", "reservation_ids.state")
@@ -313,6 +357,100 @@ class PmsFolio(models.Model):
folio.reservation_ids.filtered(lambda a: a.state != "cancelled")
)
@api.depends(
"reservation_ids",
"service_ids",
"service_ids.reservation_id",
"reservation_ids.reservation_line_ids",
"reservation_ids.reservation_line_ids.price",
"reservation_ids.reservation_line_ids.discount",
"reservation_ids.reservation_line_ids.cancel_discount",
)
def _compute_sale_line_ids(self):
for folio in self:
sale_lines = [(5, 0, 0)]
reservations = folio.reservation_ids
services_without_room = folio.service_ids.filtered(
lambda s: not s.reservation_id
)
# TODO: Not delete old sale line ids
for reservation in reservations:
sale_lines.append(
(
0,
False,
{
"display_type": "line_section",
"name": reservation.name,
},
)
)
group_lines = {}
for line in reservation.reservation_line_ids:
# On resevations the price, and discounts fields are used
# by group, we need pass this in the create line
group_key = (
reservation.id,
line.price,
line.discount,
line.cancel_discount,
)
if line.cancel_discount == 100:
continue
discount_factor = 1.0
for discount in [line.discount, line.cancel_discount]:
discount_factor = discount_factor * ((100.0 - discount) / 100.0)
final_discount = 100.0 - (discount_factor * 100.0)
if group_key not in group_lines:
group_lines[group_key] = {
"reservation_id": reservation.id,
"discount": final_discount,
"price_unit": line.price,
"reservation_line_ids": [(4, line.id)],
}
else:
group_lines[group_key][("reservation_line_ids")].append(
(4, line.id)
)
for item in group_lines.items():
sale_lines.append((0, False, item[1]))
for service in reservation.service_ids:
# On service the price, and discounts fields are
# compute in the sale.order.line
sale_lines.append(
(
0,
False,
{
"name": service.name,
"service_id": service.id,
},
)
)
if services_without_room:
sale_lines.append(
(
0,
False,
{
"display_type": "line_section",
"name": _("Others"),
},
)
)
for service in services_without_room:
sale_lines.append(
(
0,
False,
{
"name": service.name,
"service_id": service.id,
},
)
)
folio.sale_line_ids = sale_lines
@api.depends("partner_id", "agency_id")
def _compute_pricelist_id(self):
for folio in self:
@@ -365,100 +503,98 @@ class PmsFolio(models.Model):
else:
folio.commission = 0
@api.depends(
"state", "reservation_ids.invoice_status", "service_ids.invoice_status"
)
@api.depends("sale_line_ids.invoice_lines")
def _compute_get_invoiced(self):
# The invoice_ids are obtained thanks to the invoice lines of the SO
# 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 SO.
for order in self:
invoices = order.sale_line_ids.invoice_lines.move_id.filtered(
lambda r: r.move_type in ("out_invoice", "out_refund")
)
order.move_ids = invoices
order.invoice_count = len(invoices)
def _search_invoice_ids(self, operator, value):
if operator == "in" and value:
self.env.cr.execute(
"""
SELECT array_agg(so.id)
FROM pms_folio so
JOIN folio_sale_line sol ON sol.folio_id = so.id
JOIN folio_sale_line_invoice_rel soli_rel ON \
soli_rel.sale_line_ids = sol.id
JOIN account_move_line aml ON aml.id = soli_rel.invoice_line_id
JOIN account_move am ON am.id = aml.move_id
WHERE
am.move_type in ('out_invoice', 'out_refund') AND
am.id = ANY(%s)
""",
(list(value),),
)
so_ids = self.env.cr.fetchone()[0] or []
return [("id", "in", so_ids)]
return [
"&",
(
"sale_line_ids.invoice_lines.move_id.move_type",
"in",
("out_invoice", "out_refund"),
),
("sale_line_ids.invoice_lines.move_id", operator, value),
]
@api.depends("state", "sale_line_ids.invoice_status")
def _compute_get_invoice_status(self):
"""
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.
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.
- no: if the Folio is in status 'draft', 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 SO line is 'to invoice', the whole SO is 'to invoice'
- invoiced: if all SO lines are invoiced, the SO is invoiced.
- upselling: if all SO lines are invoiced or upselling, the status is upselling.
"""
self.move_ids = False
for folio in self.filtered("pricelist_id"):
move_ids = (
folio.reservation_ids.mapped("move_line_ids")
.mapped("move_id")
.filtered(lambda r: r.type in ["out_invoice", "out_refund"])
)
invoice_ids = (
folio.service_ids.mapped("move_line_ids")
.mapped("move_id")
.filtered(lambda r: r.type in ["out_invoice", "out_refund"])
)
# TODO: Search for invoices which have been 'cancelled'
# (filter_refund = 'modify' in 'account.move.refund')
# use like as origin may contains multiple references
# (e.g. 'SO01, SO02')
refunds = invoice_ids.search(
unconfirmed_orders = self.filtered(lambda so: so.state in ["draft"])
unconfirmed_orders.invoice_status = "no"
confirmed_orders = self - unconfirmed_orders
if not confirmed_orders:
return
line_invoice_status_all = [
(d["folio_id"][0], d["invoice_status"])
for d in self.env["folio.sale.line"].read_group(
[
("invoice_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.move"].browse()
if invoice_ids:
for inv in invoice_ids:
refund_ids += refund_ids.search(
[
("type", "=", "out_refund"),
("invoice_origin", "=", inv.number),
("invoice_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()
service_invoice_status = [
service.invoice_status
for service in folio.service_ids
if service.product_id != deposit_product_id
]
reservation_invoice_status = [
reservation.invoice_status for reservation in folio.reservation_ids
]
if folio.state not in ("confirm", "done"):
invoice_status = "no"
elif any(
invoice_status == "to invoice"
for invoice_status in service_invoice_status
) or any(
invoice_status == "to invoice"
for invoice_status in reservation_invoice_status
):
invoice_status = "to invoice"
elif all(
invoice_status == "invoiced"
for invoice_status in service_invoice_status
) or any(
invoice_status == "invoiced"
for invoice_status in reservation_invoice_status
):
invoice_status = "invoiced"
else:
invoice_status = "no"
folio.update(
{
"move_ids": move_ids.ids + refund_ids.ids,
"invoice_status": invoice_status,
}
("folio_id", "in", confirmed_orders.ids),
("is_downpayment", "=", False),
("display_type", "=", False),
],
["folio_id", "invoice_status"],
["folio_id", "invoice_status"],
lazy=False,
)
]
for order in confirmed_orders:
line_invoice_status = [
d[1] for d in line_invoice_status_all if d[0] == order.id
]
if order.state in ("draft"):
order.invoice_status = "no"
elif any(
invoice_status == "to invoice" for invoice_status in line_invoice_status
):
order.invoice_status = "to invoice"
elif line_invoice_status and all(
invoice_status == "invoiced" for invoice_status in line_invoice_status
):
order.invoice_status = "invoiced"
elif line_invoice_status and all(
invoice_status in ("invoiced", "upselling")
for invoice_status in line_invoice_status
):
order.invoice_status = "upselling"
else:
order.invoice_status = "no"
@api.depends("reservation_ids.price_total", "service_ids.price_total")
def _compute_amount_all(self):
@@ -514,39 +650,67 @@ class PmsFolio(models.Model):
)
# TODO: Add return_ids to depends
@api.depends("amount_total", "payment_ids", "reservation_type", "state")
@api.depends(
"amount_total",
"reservation_type",
"state",
"move_line_ids",
"move_line_ids.parent_state",
"sale_line_ids.invoice_lines",
"sale_line_ids.invoice_lines.move_id.payment_state",
)
def _compute_amount(self):
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
# )
journals = record.pms_property_id._get_payment_methods()
paid_out = 0
for journal in journals:
paid_out += sum(
self.env["account.move.line"]
.search(
[
("folio_ids", "in", record.id),
(
"account_id",
"in",
tuple(
journal.default_account_id.ids
+ journal.payment_debit_account_id.ids
+ journal.payment_credit_account_id.ids
),
),
(
"display_type",
"not in",
("line_section", "line_note"),
),
("move_id.state", "!=", "cancel"),
]
)
.mapped("balance")
)
total = record.amount_total
# REVIEW: Must We ignored services in cancelled folios
# pending amount?
if record.state == "cancelled":
total = total - sum(record.service_ids.mapped("price_total"))
# Compute 'payment_state'.
if total <= paid_out:
payment_state = "paid"
elif paid_out < total:
payment_state = "partial"
else:
payment_state = "not_paid"
vals = {
"pending_amount": total - total_paid + total_inv_refund,
"invoices_paid": total_paid,
# "refund_amount": total_inv_refund,
"pending_amount": total - paid_out,
"invoices_paid": paid_out,
"payment_state": payment_state,
}
record.update(vals)
@@ -554,30 +718,13 @@ class PmsFolio(models.Model):
def action_pay(self):
self.ensure_one()
partner = self.partner_id.id
amount = self.pending_amount
view_id = self.env.ref("pms.account_payment_view_form_folio").id
return {
"name": _("Register Payment"),
"view_type": "form",
"view_mode": "form",
"res_model": "account.payment",
"type": "ir.actions.act_window",
"view_id": view_id,
"context": {
"default_folio_id": self.id,
"default_amount": amount,
"default_payment_type": "inbound",
"default_partner_type": "customer",
"default_partner_id": partner,
"default_communication": self.name,
},
"target": "new",
}
action = self.env.ref("pms.action_payment_folio").sudo().read()[0]
action["res_id"] = self.id
return action
def open_moves_folio(self):
invoices = self.mapped("move_ids")
action = self.env.ref("account.action_move_out_invoice_type").read()[0]
action = self.env.ref("account.action_move_out_invoice_type").sudo().read()[0]
if len(invoices) > 1:
action["domain"] = [("id", "in", invoices.ids)]
elif len(invoices) == 1:
@@ -685,11 +832,327 @@ class PmsFolio(models.Model):
# 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')]):
# self.sale_line_ids.mapped('product_id.expense_policy')]):
# if not self.analytic_account_id:
# self._create_analytic_account()
return True
# CHECKIN/OUT PROCESS
def _compute_checkin_partner_count(self):
for record in self:
if record.reservation_type == "normal" and record.reservation_ids:
filtered_reservs = record.reservation_ids.filtered(
lambda x: x.state != "cancelled"
)
mapped_checkin_partner = filtered_reservs.mapped(
"checkin_partner_ids.id"
)
record.checkin_partner_count = len(mapped_checkin_partner)
mapped_checkin_partner_count = filtered_reservs.mapped(
lambda x: (x.adults + x.children) - len(x.checkin_partner_ids)
)
record.checkin_partner_pending_count = sum(mapped_checkin_partner_count)
def _prepare_invoice(self):
"""
Prepare the dict of values to create the new invoice for a folio.
This method may be overridden to implement custom invoice generation
(making sure to call super() to establish a clean extension chain).
"""
self.ensure_one()
journal = (
self.env["account.move"]
.with_context(default_move_type="out_invoice")
._get_default_journal()
)
if not journal:
raise UserError(
_("Please define an accounting sales journal for the company %s (%s).")
% (self.company_id.name, self.company_id.id)
)
invoice_vals = {
"ref": self.client_order_ref or "",
"move_type": "out_invoice",
"narration": self.note,
"currency_id": self.pricelist_id.currency_id.id,
# 'campaign_id': self.campaign_id.id,
# 'medium_id': self.medium_id.id,
# 'source_id': self.source_id.id,
"invoice_user_id": self.user_id and self.user_id.id,
"partner_id": self.partner_invoice_id.id,
"partner_bank_id": self.company_id.partner_id.bank_ids[:1].id,
"journal_id": journal.id, # company comes from the journal
"invoice_origin": self.name,
"invoice_payment_term_id": self.payment_term_id.id,
"payment_reference": self.reference,
"transaction_ids": [(6, 0, self.transaction_ids.ids)],
"folio_ids": [(6, 0, [self.id])],
"invoice_line_ids": [],
"company_id": self.company_id.id,
}
return invoice_vals
def action_view_invoice(self):
invoices = self.mapped("move_ids")
action = self.env["ir.actions.actions"]._for_xml_id(
"account.action_move_out_invoice_type"
)
if len(invoices) > 1:
action["domain"] = [("id", "in", invoices.ids)]
elif len(invoices) == 1:
form_view = [(self.env.ref("account.view_move_form").id, "form")]
if "views" in action:
action["views"] = form_view + [
(state, view) for state, view in action["views"] if view != "form"
]
else:
action["views"] = form_view
action["res_id"] = invoices.id
else:
action = {"type": "ir.actions.act_window_close"}
context = {
"default_move_type": "out_invoice",
}
if len(self) == 1:
context.update(
{
"default_partner_id": self.partner_id.id,
"default_invoice_payment_term_id": self.payment_term_id.id
or self.partner_id.property_payment_term_id.id
or self.env["account.move"]
.default_get(["invoice_payment_term_id"])
.get("invoice_payment_term_id"),
"default_invoice_origin": self.mapped("name"),
"default_user_id": self.user_id.id,
}
)
action["context"] = context
return action
def _get_invoice_grouping_keys(self):
return ["company_id", "partner_id", "currency_id"]
@api.model
def _nothing_to_invoice_error(self):
msg = _(
"""There is nothing to invoice!\n
Reason(s) of this behavior could be:
- You should deliver your products before invoicing them: Click on the "truck"
icon (top-right of your screen) and follow instructions.
- You should modify the invoicing policy of your product: Open the product,
go to the "Sales tab" and modify invoicing policy from "delivered quantities"
to "ordered quantities".
"""
)
return UserError(msg)
def _create_invoices(
self,
grouped=False,
final=False,
date=None,
lines_to_invoice=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)
:param final: if True, refunds will be generated if necessary
:returns: list of created invoices
:lines_to_invoice: invoice specific lines, if False, invoice all
"""
if not self.env["account.move"].check_access_rights("create", False):
try:
self.check_access_rights("write")
self.check_access_rule("write")
except AccessError:
return self.env["account.move"]
# 1) Create invoices.
if not lines_to_invoice:
lines_to_invoice = self.sale_line_ids
invoice_vals_list = self.get_invoice_vals_list(final, lines_to_invoice)
if not invoice_vals_list:
raise self._nothing_to_invoice_error()
# 2) Manage 'grouped' parameter: group by (partner_id, currency_id).
if not grouped:
new_invoice_vals_list = []
invoice_grouping_keys = self._get_invoice_grouping_keys()
for _grouping_keys, invoices in groupby(
invoice_vals_list,
key=lambda x: [
x.get(grouping_key) for grouping_key in invoice_grouping_keys
],
):
origins = set()
payment_refs = set()
refs = set()
ref_invoice_vals = None
for invoice_vals in invoices:
if not ref_invoice_vals:
ref_invoice_vals = invoice_vals
else:
ref_invoice_vals["invoice_line_ids"] += invoice_vals[
"invoice_line_ids"
]
origins.add(invoice_vals["invoice_origin"])
payment_refs.add(invoice_vals["payment_reference"])
refs.add(invoice_vals["ref"])
ref_invoice_vals.update(
{
"ref": ", ".join(refs)[:2000],
"invoice_origin": ", ".join(origins),
"payment_reference": len(payment_refs) == 1
and payment_refs.pop()
or False,
}
)
new_invoice_vals_list.append(ref_invoice_vals)
invoice_vals_list = new_invoice_vals_list
# 3) Create invoices.
# As part of the invoice creation, we make sure the
# sequence of multiple SO do not interfere
# in a single invoice. Example:
# Folio 1:
# - Section A (sequence: 10)
# - Product A (sequence: 11)
# Folio 2:
# - Section B (sequence: 10)
# - Product B (sequence: 11)
#
# If Folio 1 & 2 are grouped in the same invoice,
# the result will be:
# - Section A (sequence: 10)
# - Section B (sequence: 10)
# - Product A (sequence: 11)
# - Product B (sequence: 11)
#
# Resequencing should be safe, however we resequence only
# if there are less invoices than orders, meaning a grouping
# might have been done. This could also mean that only a part
# of the selected SO are invoiceable, but resequencing
# in this case shouldn't be an issue.
if len(invoice_vals_list) < len(self):
FolioSaleLine = self.env["folio.sale.line"]
for invoice in invoice_vals_list:
sequence = 1
for line in invoice["invoice_line_ids"]:
line[2]["sequence"] = FolioSaleLine._get_invoice_line_sequence(
new=sequence, old=line[2]["sequence"]
)
sequence += 1
# Manage the creation of invoices in sudo because
# a salesperson must be able to generate an invoice from a
# sale order without "billing" access rights.
# However, he should not be able to create an invoice from scratch.
moves = (
self.env["account.move"]
.sudo()
.with_context(default_move_type="out_invoice")
.create(invoice_vals_list)
)
# 4) Some moves might actually be refunds: convert
# them if the total amount is negative
# We do this after the moves have been created
# since we need taxes, etc. to know if the total
# is actually negative or not
if final:
moves.sudo().filtered(
lambda m: m.amount_total < 0
).action_switch_invoice_into_refund_credit_note()
for move in moves:
move.message_post_with_view(
"mail.message_origin_link",
values={
"self": move,
"origin": move.line_ids.mapped("folio_line_ids.folio_id"),
},
subtype_id=self.env.ref("mail.mt_note").id,
)
return moves
def get_invoice_vals_list(self, final=False, lines_to_invoice=False):
precision = self.env["decimal.precision"].precision_get(
"Product Unit of Measure"
)
invoice_vals_list = []
invoice_item_sequence = 0
for order in self:
order = order.with_company(order.company_id)
current_section_vals = None
down_payments = order.env["folio.sale.line"]
# Invoice values.
invoice_vals = order._prepare_invoice()
# Invoice line values (keep only necessary sections).
invoice_lines_vals = []
for line in order.sale_line_ids.filtered(
lambda l: l.id in lines_to_invoice.ids
):
if line.display_type == "line_section":
current_section_vals = line._prepare_invoice_line(
sequence=invoice_item_sequence + 1
)
continue
if line.display_type != "line_note" and float_is_zero(
line.qty_to_invoice, precision_digits=precision
):
continue
if (
line.qty_to_invoice > 0
or (line.qty_to_invoice < 0 and final)
or line.display_type == "line_note"
):
if line.is_downpayment:
down_payments += line
continue
if current_section_vals:
invoice_item_sequence += 1
invoice_lines_vals.append(current_section_vals)
current_section_vals = None
invoice_item_sequence += 1
prepared_line = line._prepare_invoice_line(
sequence=invoice_item_sequence
)
invoice_lines_vals.append(prepared_line)
# If down payments are present in SO, group them under common section
if down_payments:
invoice_item_sequence += 1
down_payments_section = order._prepare_down_payment_section_line(
sequence=invoice_item_sequence
)
invoice_lines_vals.append(down_payments_section)
for down_payment in down_payments:
invoice_item_sequence += 1
invoice_down_payment_vals = down_payment._prepare_invoice_line(
sequence=invoice_item_sequence
)
invoice_lines_vals.append(invoice_down_payment_vals)
if not any(
new_line["display_type"] is False for new_line in invoice_lines_vals
):
raise self._nothing_to_invoice_error()
invoice_vals["invoice_line_ids"] = [
(0, 0, invoice_line_id) for invoice_line_id in invoice_lines_vals
]
invoice_vals_list.append(invoice_vals)
return invoice_vals_list
def _get_tax_amount_by_group(self):
self.ensure_one()
res = {}
@@ -730,3 +1193,25 @@ class PmsFolio(models.Model):
for record in self:
if record.agency_id and record.channel_type_id:
raise models.ValidationError(_("There must be only one sale channel"))
@api.model
def _prepare_down_payment_section_line(self, **optional_values):
"""
Prepare the dict of values to create a new down
payment section for a sales order line.
:param optional_values: any parameter that should
be added to the returned down payment section
"""
down_payments_section_line = {
"display_type": "line_section",
"name": _("Down Payments"),
"product_id": False,
"product_uom_id": False,
"quantity": 0,
"discount": 0,
"price_unit": 0,
"account_id": False,
}
if optional_values:
down_payments_section_line.update(optional_values)
return down_payments_section_line

View File

@@ -108,3 +108,22 @@ class PmsProperty(models.Model):
date = date.astimezone(pytz.utc)
date = date.replace(tzinfo=None)
return date
def _get_payment_methods(self):
self.ensure_one()
payment_methods = self.env["account.journal"].search(
[
"&",
("type", "in", ["cash", "bank"]),
"|",
("pms_property_ids", "in", self.id),
"|",
"&",
("pms_property_ids", "=", False),
("company_id", "=", self.company_id.id),
"&",
("pms_property_ids", "=", False),
("company_id", "=", False),
]
)
return payment_methods

View File

@@ -84,20 +84,6 @@ class PmsReservation(models.Model):
segmentation_ids = folio.segmentation_ids
return segmentation_ids
@api.model
def _default_diff_invoicing(self):
"""
If the guest has an invoicing address set,
this method return diff_invoicing = True, else, return False
"""
if "reservation_id" in self.env.context:
reservation = self.env["pms.reservation"].browse(
[self.env.context["reservation_id"]]
)
if reservation.partner_id.id == reservation.partner_invoice_id.id:
return False
return True
# Fields declaration
name = fields.Text(
"Reservation Description",
@@ -113,6 +99,7 @@ class PmsReservation(models.Model):
string="Room",
ondelete="restrict",
domain="[('id', 'in', allowed_room_ids)]",
copy=False,
)
allowed_room_ids = fields.Many2many(
"pms.room",
@@ -124,6 +111,7 @@ class PmsReservation(models.Model):
string="Folio",
tracking=True,
ondelete="restrict",
copy=False,
)
board_service_room_id = fields.Many2one(
"pms.board.service.room.type",
@@ -137,6 +125,7 @@ class PmsReservation(models.Model):
compute="_compute_room_type_id",
store=True,
readonly=False,
copy=False,
)
partner_id = fields.Many2one(
"res.partner",
@@ -178,6 +167,7 @@ class PmsReservation(models.Model):
compute="_compute_reservation_line_ids",
store=True,
readonly=False,
copy=False,
)
service_ids = fields.One2many(
"pms.service",
@@ -212,6 +202,7 @@ class PmsReservation(models.Model):
compute="_compute_checkin_partner_ids",
store=True,
readonly=False,
copy=False,
)
count_pending_arrival = fields.Integer(
"Pending Arrival",
@@ -249,13 +240,16 @@ class PmsReservation(models.Model):
)
currency_id = fields.Many2one(
"res.currency",
related="pricelist_id.currency_id",
string="Currency",
depends=["pricelist_id"],
store=True,
readonly=True,
)
tax_ids = fields.Many2many(
"account.tax",
string="Taxes",
compute="_compute_tax_ids",
readonly="False",
store=True,
ondelete="restrict",
domain=["|", ("active", "=", False), ("active", "=", True)],
)
@@ -267,9 +261,10 @@ class PmsReservation(models.Model):
string="Invoice Lines",
copy=False,
)
analytic_tag_ids = fields.Many2many("account.analytic.tag", string="Analytic Tags")
localizator = fields.Char(
string="Localizator", compute="_compute_localizator", store=True
string="Localizator",
compute="_compute_localizator",
store=True,
)
adults = fields.Integer(
"Adults",
@@ -282,7 +277,6 @@ class PmsReservation(models.Model):
children_occupying = fields.Integer(
string="Children occupying",
)
children = fields.Integer(
"Children",
readonly=False,
@@ -315,23 +309,26 @@ class PmsReservation(models.Model):
compute="_compute_splitted",
store=True,
)
rooms = fields.Char(
string="Room/s",
compute="_compute_rooms",
store=True,
tracking=True,
)
credit_card_details = fields.Text(related="folio_id.credit_card_details")
cancelled_reason = fields.Selection(
[("late", "Late"), ("intime", "In time"), ("noshow", "No Show")],
string="Cause of cancelled",
tracking=True,
copy=False,
)
out_service_description = fields.Text("Cause of out of service")
checkin = fields.Date("Check In", required=True, default=_get_default_checkin)
checkout = fields.Date("Check Out", required=True, default=_get_default_checkout)
checkin = fields.Date(
"Check In", required=True, default=_get_default_checkin, copy=False
)
checkout = fields.Date(
"Check Out", required=True, default=_get_default_checkout, copy=False
)
arrival_hour = fields.Char(
"Arrival Hour",
default=_get_default_arrival_hour,
@@ -350,11 +347,6 @@ class PmsReservation(models.Model):
"Exact Departure",
compute="_compute_checkout_datetime",
)
# TODO: As checkin_partner_count is a computed field, it can't not
# be used in a domain filer Non-stored field
# pms.reservation.checkin_partner_count cannot be searched
# searching on a computed field can also be enabled by setting the
# search parameter. The value is a method name returning a Domains
checkin_partner_count = fields.Integer(
"Checkin counter", compute="_compute_checkin_partner_count"
)
@@ -363,8 +355,16 @@ class PmsReservation(models.Model):
compute="_compute_checkin_partner_count",
search="_search_checkin_partner_pending",
)
overbooking = fields.Boolean("Is Overbooking", default=False)
reselling = fields.Boolean("Is Reselling", default=False)
overbooking = fields.Boolean(
"Is Overbooking",
default=False,
copy=False,
)
reselling = fields.Boolean(
"Is Reselling",
default=False,
copy=False,
)
nights = fields.Integer("Nights", compute="_compute_nights", store=True)
origin = fields.Char("Origin", compute="_compute_origin", store=True)
detail_origin = fields.Char(
@@ -380,11 +380,13 @@ class PmsReservation(models.Model):
string="Internal Partner Notes", related="partner_id.comment"
)
folio_internal_comment = fields.Text(
string="Internal Folio Notes", related="folio_id.internal_comment"
string="Internal Folio Notes",
related="folio_id.internal_comment",
)
preconfirm = fields.Boolean("Auto confirm to Save", default=True)
invoice_status = fields.Selection(
[
("upselling", "Upselling Opportunity"),
("invoiced", "Fully Invoiced"),
("to invoice", "To Invoice"),
("no", "Nothing to Invoice"),
@@ -396,19 +398,39 @@ class PmsReservation(models.Model):
default="no",
)
qty_to_invoice = fields.Float(
compute="_compute_get_to_invoice_qty",
string="To Invoice",
compute="_compute_qty_to_invoice",
string="To Invoice Quantity",
store=True,
readonly=True,
digits=("Product Unit of Measure"),
)
qty_invoiced = fields.Float(
compute="_compute_get_invoice_qty",
string="Invoiced",
compute="_compute_qty_invoiced",
string="Invoiced Quantity",
store=True,
readonly=True,
digits=("Product Unit of Measure"),
)
untaxed_amount_invoiced = fields.Monetary(
"Untaxed Invoiced Amount",
compute="_compute_untaxed_amount_invoiced",
compute_sudo=True,
store=True,
)
untaxed_amount_to_invoice = fields.Monetary(
"Untaxed Amount To Invoice",
compute="_compute_untaxed_amount_to_invoice",
compute_sudo=True,
store=True,
)
analytic_tag_ids = fields.Many2many(
"account.analytic.tag",
string="Analytic Tags",
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
)
analytic_line_ids = fields.One2many(
"account.analytic.line", "so_line", string="Analytic lines"
)
price_subtotal = fields.Monetary(
string="Subtotal",
readonly=True,
@@ -443,6 +465,7 @@ class PmsReservation(models.Model):
string="Discount (€)",
digits=("Discount"),
compute="_compute_discount",
readonly=False,
store=True,
)
@@ -532,17 +555,18 @@ class PmsReservation(models.Model):
)
reservation.allowed_room_ids = rooms_available
@api.depends("reservation_type", "agency_id")
@api.depends("reservation_type", "agency_id", "folio_id")
def _compute_partner_id(self):
for reservation in self:
if reservation.reservation_type == "out":
reservation.partner_id = reservation.pms_property_id.partner_id.id
if reservation.folio_id:
reservation.partner_id = reservation.folio_id.partner_id
else:
reservation.partner_id = False
if not reservation.partner_id and reservation.agency_id:
reservation.partner_id = reservation.agency_id
elif not reservation.partner_id:
if reservation.folio_id:
reservation.partner_id = reservation.folio_id.partner_id
elif reservation.agency_id:
reservation.partner_id = reservation.agency_id
else:
reservation.partner_id = False
@api.depends("partner_id")
def _compute_partner_invoice_id(self):
@@ -851,11 +875,10 @@ class PmsReservation(models.Model):
line.invoice_status = "no"
@api.depends("qty_invoiced", "nights", "folio_id.state")
def _compute_get_to_invoice_qty(self):
def _compute_qty_to_invoice(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.
Compute the quantity to invoice. The quantity to invoice is
calculated from the nights quantity.
"""
for line in self:
if line.folio_id.state not in ["draft"]:
@@ -863,25 +886,138 @@ class PmsReservation(models.Model):
else:
line.qty_to_invoice = 0
@api.depends("move_line_ids.move_id.state", "move_line_ids.quantity")
def _compute_get_invoice_qty(self):
@api.depends(
"move_line_ids.move_id.state",
"move_line_ids.quantity",
"untaxed_amount_to_invoice",
)
def _compute_qty_invoiced(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:
for record in self:
qty_invoiced = 0.0
for day in line.reservation_line_ids:
invoice_lines = day.move_line_ids.filtered(
for line in record.reservation_line_ids:
invoice_lines = line.move_line_ids.filtered(
lambda r: r.move_id.state != "cancel"
)
qty_invoiced += len(
invoice_lines.filtered(lambda r: r.move_id.type == "out_invoice")
invoice_lines.filtered(
lambda r: r.move_id.move_type == "out_invoice"
)
) - len(
invoice_lines.filtered(lambda r: r.move_id.type == "out_refund")
invoice_lines.filtered(
lambda r: r.move_id.move_type == "out_refund"
)
)
line.qty_invoiced = qty_invoiced
record.qty_invoiced = qty_invoiced
@api.depends(
"move_line_ids",
"move_line_ids.price_total",
"move_line_ids.move_id.state",
"move_line_ids.move_id.move_type",
)
def _compute_untaxed_amount_invoiced(self):
"""Compute the untaxed amount already invoiced from the reservation,
taking the refund attached
the reservation into account. This amount is computed as
SUM(inv_line.price_subtotal) - SUM(ref_line.price_subtotal)
where
`inv_line` is a customer invoice line linked to the reservation
`ref_line` is a customer credit note (refund)
line linked to the reservation
"""
for line in self:
amount_invoiced = 0.0
for invoice_line in line.move_line_ids:
if invoice_line.move_id.state == "posted":
invoice_date = (
invoice_line.move_id.invoice_date or fields.Date.today()
)
if invoice_line.move_id.move_type == "out_invoice":
amount_invoiced += invoice_line.currency_id._convert(
invoice_line.price_subtotal,
line.currency_id,
line.company_id,
invoice_date,
)
elif invoice_line.move_id.move_type == "out_refund":
amount_invoiced -= invoice_line.currency_id._convert(
invoice_line.price_subtotal,
line.currency_id,
line.company_id,
invoice_date,
)
line.untaxed_amount_invoiced = amount_invoiced
@api.depends(
"state", "discount", "price_total", "room_type_id", "untaxed_amount_invoiced"
)
def _compute_untaxed_amount_to_invoice(self):
"""Total of remaining amount to invoice on the reservation (taxes excl.) as
total_sol - amount already invoiced
Note: Draft invoice are ignored on purpose, the 'to invoice' amount should
come only from the reservation.
"""
for line in self:
amount_to_invoice = 0.0
if line.state not in ["draft"]:
price_subtotal = 0.0
price_subtotal = line.price_subtotal
if len(line.tax_ids.filtered(lambda tax: tax.price_include)) > 0:
# As included taxes are not excluded from the computed subtotal,
# `compute_all()` method
# has to be called to retrieve the subtotal without them.
price_subtotal = line.tax_ids.compute_all(
price_subtotal,
currency=line.currency_id,
quantity=line.nights,
product=line.room_type_id.product_id,
)["total_excluded"]
if any(
line.move_line_ids.mapped(lambda i: i.discount != line.discount)
):
# In case of re-invoicing with different discount we
# try to calculate manually the
# remaining amount to invoice
amount = 0
for move in line.move_line_ids:
if (
len(move.tax_ids.filtered(lambda tax: tax.price_include))
> 0
):
amount += move.tax_ids.compute_all(
move.currency_id._convert(
move.price_unit,
line.currency_id,
line.company_id,
move.date or fields.Date.today(),
round=False,
)
* move.quantity
)["total_excluded"]
else:
amount += (
move.currency_id._convert(
move.price_unit,
line.currency_id,
line.company_id,
move.date or fields.Date.today(),
round=False,
)
* move.quantity
)
amount_to_invoice = max(price_subtotal - amount, 0)
else:
amount_to_invoice = price_subtotal - line.untaxed_amount_invoiced
line.untaxed_amount_to_invoice = amount_to_invoice
@api.depends("reservation_line_ids")
def _compute_nights(self):
@@ -1033,33 +1169,10 @@ class PmsReservation(models.Model):
if record.agency_id and not record.agency_id.is_agency:
raise ValidationError(_("booking agency with wrong configuration: "))
# @api.constrains("reservation_type", "partner_id")
# def _check_partner_reservation(self):
# for reservation in self:
# if (
# reservation.reservation_type == "out"
# and reservation.partner_id.id != \
# reservation.pms_property_id.partner_id.id
# ):
# raise models.ValidationError(
# _("The partner on out reservations must be a property partner")
# )
# @api.constrains("closure_reason_id", "reservation_type")
# def _check_clousure_reservation(self):
# for reservation in self:
# if reservation.closure_reason_id and \
# reservation.reservation_type != "out":
# raise models.ValidationError(
# _("Only the out reservations can has a clousure reason")
# )
# self._compute_tax_ids() TODO: refact
# Action methods
def open_folio(self):
action = self.env.ref("pms.open_pms_folio1_form_tree_all").read()[0]
action = self.env.ref("pms.open_pms_folio1_form_tree_all").sudo().read()[0]
if self.folio_id:
action["views"] = [(self.env.ref("pms.pms_folio_view_form").id, "form")]
action["res_id"] = self.folio_id.id
@@ -1068,7 +1181,7 @@ class PmsReservation(models.Model):
return action
def open_reservation_form(self):
action = self.env.ref("pms.open_pms_reservation_form_tree_all").read()[0]
action = self.env.ref("pms.open_pms_reservation_form_tree_all").sudo().read()[0]
action["views"] = [(self.env.ref("pms.pms_reservation_view_form").id, "form")]
action["res_id"] = self.id
return action
@@ -1146,6 +1259,19 @@ class PmsReservation(models.Model):
result.append((res.id, name))
return result
def copy_data(self, default=None):
rooms_available = self.env["pms.room.type.availability.plan"].rooms_available(
self.checkin,
self.checkout,
room_type_id=self.room_type_id.id,
pricelist=self.pricelist_id.id,
)
if self.preferred_room_id.id in rooms_available.ids:
default["preferred_room_id"] = self.preferred_room_id.id
if self.room_type_id.id in rooms_available.mapped("room_type_id.id"):
default["room_type_id"] = self.room_type_id.id
return super(PmsReservation, self).copy_data(default)
@api.model
def create(self, vals):
if "folio_id" in vals:
@@ -1405,19 +1531,15 @@ class PmsReservation(models.Model):
if reservation.checkout_datetime <= fields.Datetime.now():
reservations.state = "no_checkout"
def unify(self):
# TODO
return True
@api.depends("room_type_id")
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 = record.with_company(record.company_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
lambda t: t.company_id == record.env.company
)
@api.depends("reservation_line_ids", "reservation_line_ids.room_id")

View File

@@ -65,6 +65,11 @@ class PmsReservationLine(models.Model):
store=True,
readonly=False,
)
invoiced = fields.Boolean(
string="Invoiced",
compute="_compute_invoiced",
store=True,
)
cancel_discount = fields.Float(
string="Cancel Discount (%)",
digits=("Discount"),
@@ -116,7 +121,7 @@ class PmsReservationLine(models.Model):
room_type_id=reservation.room_type_id.id
if not free_room_select
else False,
current_lines=line._origin.reservation_id.reservation_line_ids.ids,
current_lines=line.reservation_id.reservation_line_ids.ids,
pricelist=line.reservation_id.pricelist_id.id,
)
# if there is availability for the entire stay
@@ -282,8 +287,6 @@ class PmsReservationLine(models.Model):
line.reservation_id.company_id,
)
# TODO: Out of service 0 amount
else:
line.price = line._origin.price
@api.depends("reservation_id.state", "reservation_id.overbooking")
def _compute_occupies_availability(self):
@@ -315,6 +318,18 @@ class PmsReservationLine(models.Model):
return True
return False
@api.depends("move_line_ids", "move_line_ids.move_id.state")
def _compute_invoiced(self):
for line in self:
qty_invoiced = 0
for invoice_line in line.move_line_ids:
if invoice_line.move_id.state != "cancel":
if invoice_line.move_id.move_type == "out_invoice":
qty_invoiced += 1
elif invoice_line.move_id.move_type == "out_refund":
qty_invoiced -= 1
line.invoiced = False if qty_invoiced < 1 else True
# TODO: Refact method and allowed cancelled single days
@api.depends("reservation_id.cancelled_reason")
def _compute_cancel_discount(self):

View File

@@ -30,8 +30,8 @@ class PmsRoom(models.Model):
name = fields.Char("Room Name", required=True)
pms_property_id = fields.Many2one(
"pms.property",
store=True,
readonly=True,
required=True,
ondelete="restrict",
)
room_type_id = fields.Many2one(
"pms.room.type", "Property Room Type", required=True, ondelete="restrict"

View File

@@ -424,14 +424,20 @@ class PmsService(models.Model):
qty_invoiced = 0.0
for invoice_line in line.move_line_ids:
if invoice_line.move_id.state != "cancel":
if invoice_line.move_id.type == "out_invoice":
qty_invoiced += invoice_line.uom_id._compute_quantity(
invoice_line.quantity, line.product_id.uom_id
)
elif invoice_line.move_id.type == "out_refund":
qty_invoiced -= invoice_line.uom_id._compute_quantity(
if invoice_line.move_id.move_type == "out_invoice":
qty_invoiced += invoice_line.product_uom_id._compute_quantity(
invoice_line.quantity, line.product_id.uom_id
)
elif invoice_line.move_id.move_type == "out_refund":
if (
not line.is_downpayment
or line.untaxed_amount_to_invoice == 0
):
qty_invoiced -= (
invoice_line.product_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")
@@ -496,7 +502,7 @@ class PmsService(models.Model):
# Action methods
def open_service_ids(self):
action = self.env.ref("pms.action_pms_services_form").read()[0]
action = self.env.ref("pms.action_pms_services_form").sudo().read()[0]
action["views"] = [(self.env.ref("pms.pms_service_view_form").id, "form")]
action["res_id"] = self.id
action["target"] = "new"

View File

@@ -1,7 +1,11 @@
# Copyright 2017 Alexandre Díaz, Pablo Quesada, Darío Lodeiros
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from odoo import fields, models
_logger = logging.getLogger(__name__)
class ProductPricelist(models.Model):
"""Before creating a 'daily' pricelist, you need to consider the following:

View File

@@ -269,7 +269,7 @@
</div>
</div>
<div>
<span t-if="doc.payment_ids">
<!-- <span t-if="doc.payment_ids">
<table style="width:80%;">
<thead>
<tr>
@@ -288,7 +288,7 @@
</tr>
</tbody>
</table>
</span>
</span> -->
<!-- <span t-if="doc.return_ids">
<table style="width:80%;">
<thead>

View File

@@ -53,3 +53,8 @@ user_access_pms_advanced_filters_wizard,user_access_pms_advanced_filters_wizard,
user_access_pms_folio_wizard,user_access_pms_folio_wizard,model_pms_folio_wizard,pms.group_pms_user,1,1,1,1
user_access_pms_folio_availability_wizard,user_access_pms_folio_availability_wizard,model_pms_folio_availability_wizard,pms.group_pms_user,1,1,1,1
user_access_pms_num_rooms_selection,user_access_pms_num_rooms_selection,model_pms_num_rooms_selection,pms.group_pms_user,1,1,1,1
user_access_pms_folio_sale_line,user_access_pms_folio_sale_line,model_folio_sale_line,pms.group_pms_user,1,0,0,0
user_access_folio_make_invoice_advance,user_access_folio_make_invoice_advance,model_folio_advance_payment_inv,pms.group_pms_user,1,1,1,1
user_access_pms_invoice_filter_days,user_access_pms_invoice_filter_days,model_pms_invoice_filter_days,pms.group_pms_user,1,1,1,1
user_access_pms_invoice_filter_days_items,user_access_pms_invoice_filter_days_items,model_pms_invoice_filter_days_items,pms.group_pms_user,1,1,1,1
user_access_wizard_payment_folio,user_access_wizard_payment_folio,model_wizard_payment_folio,pms.group_pms_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
53 user_access_pms_folio_wizard user_access_pms_folio_wizard model_pms_folio_wizard pms.group_pms_user 1 1 1 1
54 user_access_pms_folio_availability_wizard user_access_pms_folio_availability_wizard model_pms_folio_availability_wizard pms.group_pms_user 1 1 1 1
55 user_access_pms_num_rooms_selection user_access_pms_num_rooms_selection model_pms_num_rooms_selection pms.group_pms_user 1 1 1 1
56 user_access_pms_folio_sale_line user_access_pms_folio_sale_line model_folio_sale_line pms.group_pms_user 1 0 0 0
57 user_access_folio_make_invoice_advance user_access_folio_make_invoice_advance model_folio_advance_payment_inv pms.group_pms_user 1 1 1 1
58 user_access_pms_invoice_filter_days user_access_pms_invoice_filter_days model_pms_invoice_filter_days pms.group_pms_user 1 1 1 1
59 user_access_pms_invoice_filter_days_items user_access_pms_invoice_filter_days_items model_pms_invoice_filter_days_items pms.group_pms_user 1 1 1 1
60 user_access_wizard_payment_folio user_access_wizard_payment_folio model_wizard_payment_folio pms.group_pms_user 1 1 1 1

View File

@@ -118,7 +118,6 @@ class TestPmsCheckinPartner(TestHotel):
"checkin": "2012-01-15",
}
)
# ACT & ASSERT
with self.assertRaises(ValidationError), self.cr.savepoint():
self.checkin1.action_on_board()

View File

@@ -0,0 +1,43 @@
from odoo.tests.common import SavepointCase
class TestPmsFolioInvoice(SavepointCase):
def setUp(self):
super(TestPmsFolioInvoice, self).setUp()
def test_invoice_folio(self):
"""Test create and invoice from the Folio, and check qty invoice/to invoice,
and the related amounts"""
def test_invoice_by_days_folio(self):
"""Test create and invoice from the Folio, and check qty invoice/to invoice,
and the related amounts in a specific segment of days (reservation lines)"""
def test_invoice_by_services_folio(self):
"""Test create and invoice from the Folio, and check qty invoice/to invoice,
and the related amounts in a specific segment of services (qtys)"""
def test_invoice_board_service(self):
"""Test create and invoice from the Folio, and check qty invoice/to invoice,
and the related amounts with board service linked"""
def test_invoice_line_group_by_room_type_sections(self):
"""Test create and invoice from the Folio, and check qty invoice/to invoice,
and the grouped invoice lines by room type, by one
line by unit prices/qty with nights"""
def test_autoinvoice_folio(self):
""" Test create and invoice the cron by partner preconfig automation """
def test_downpayment(self):
"""Test invoice qith a way of downpaument and check dowpayment's
folio line is created and also check a total amount of invoice is
equal to a respective folio's total amount"""
def test_invoice_with_discount(self):
"""Test create with a discount and check discount applied
on both Folio lines and an inovoice lines"""
def test_reinvoice(self):
"""Test the compute reinvoice folio take into account
nights and services qty invoiced"""

View File

@@ -0,0 +1,10 @@
from odoo.tests.common import SavepointCase
class TestPmsFolioPrice(SavepointCase):
def setUp(self):
super(TestPmsFolioPrice, self).setUp()
def test_price_folio(self):
"""Test create reservation and services, and check price
tax and discounts"""

View File

@@ -0,0 +1,10 @@
from freezegun import freeze_time
from odoo.tests.common import SavepointCase
freeze_time("2000-02-02")
class TestPmsInvoiceRefund(SavepointCase):
def setUp(self):
super(TestPmsInvoiceRefund, self).setUp()

View File

@@ -0,0 +1,10 @@
from freezegun import freeze_time
from odoo.tests.common import SavepointCase
freeze_time("2000-02-02")
class TestPmsPayment(SavepointCase):
def setUp(self):
super(TestPmsPayment, self).setUp()

View File

@@ -126,7 +126,7 @@ class TestPmsPricelistRules(common.TransactionCase):
# ASSERT
self.assertEqual(
expected_price, reservation.price_total, "The price is not as expected"
expected_price, reservation.price_subtotal, "The price is not as expected"
)
@freeze_time("2000-01-16 00:00:00")
@@ -179,7 +179,7 @@ class TestPmsPricelistRules(common.TransactionCase):
expected_price = self.item1.fixed_price * n_days
# ASSERT
self.assertEqual(
expected_price, reservation.price_total, "The price is not as expected"
expected_price, reservation.price_subtotal, "The price is not as expected"
)
@freeze_time("2000-01-05 00:00:00")
@@ -233,7 +233,7 @@ class TestPmsPricelistRules(common.TransactionCase):
expected_price = self.item2.fixed_price * n_days
# ASSERT
self.assertEqual(
expected_price, reservation.price_total, "The price is not as expected"
expected_price, reservation.price_subtotal, "The price is not as expected"
)
@freeze_time("2000-01-20 00:00:00")
@@ -301,7 +301,7 @@ class TestPmsPricelistRules(common.TransactionCase):
expected_price = self.item1.fixed_price
# ASSERT
self.assertEqual(
expected_price, reservation.price_total, "The price is not as expected"
expected_price, reservation.price_subtotal, "The price is not as expected"
)
@freeze_time("2000-01-25 00:00:00")
@@ -369,7 +369,7 @@ class TestPmsPricelistRules(common.TransactionCase):
expected_price = self.item3.fixed_price
# ASSERT
self.assertEqual(
expected_price, reservation.price_total, "The price is not as expected"
expected_price, reservation.price_subtotal, "The price is not as expected"
)
@freeze_time("2000-02-01 00:00:00")
@@ -437,7 +437,7 @@ class TestPmsPricelistRules(common.TransactionCase):
expected_price = self.item3.fixed_price * n_days
# ASSERT
self.assertEqual(
expected_price, reservation.price_total, "The price is not as expected"
expected_price, reservation.price_subtotal, "The price is not as expected"
)
@freeze_time("2000-02-01 00:00:00")
@@ -492,7 +492,7 @@ class TestPmsPricelistRules(common.TransactionCase):
expected_price = self.item1.fixed_price * n_days
# ASSERT
self.assertEqual(
expected_price, reservation.price_total, "The price is not as expected"
expected_price, reservation.price_subtotal, "The price is not as expected"
)
@freeze_time("2000-02-10 00:00:00")
@@ -547,7 +547,7 @@ class TestPmsPricelistRules(common.TransactionCase):
# ASSERT
self.assertEqual(
expected_price, reservation.price_total, "The price is not as expected"
expected_price, reservation.price_subtotal, "The price is not as expected"
)
@freeze_time("2000-01-01 00:00:00")
@@ -571,7 +571,7 @@ class TestPmsPricelistRules(common.TransactionCase):
# ASSERT
self.assertEqual(
expected_price, reservation.price_total, "The price is not as expected"
expected_price, reservation.price_subtotal, "The price is not as expected"
)
@freeze_time("2000-01-01 00:00:00")
@@ -603,10 +603,9 @@ class TestPmsPricelistRules(common.TransactionCase):
)
n_days = (reservation.checkout - reservation.checkin).days
expected_price = self.item1.fixed_price * n_days
# ASSERT
self.assertEqual(
expected_price, reservation.price_total, "The price is not as expected"
expected_price, reservation.price_subtotal, "The price is not as expected"
)
@freeze_time("2000-01-01 00:00:00")
@@ -641,7 +640,7 @@ class TestPmsPricelistRules(common.TransactionCase):
# ASSERT
self.assertEqual(
expected_price, reservation.price_total, "The price is not as expected"
expected_price, reservation.price_subtotal, "The price is not as expected"
)
@freeze_time("2000-01-01 00:00:00")
@@ -689,7 +688,7 @@ class TestPmsPricelistRules(common.TransactionCase):
expected_price = self.item2.fixed_price * n_days
# ASSERT
self.assertEqual(
expected_price, reservation.price_total, "The price is not as expected"
expected_price, reservation.price_subtotal, "The price is not as expected"
)
@freeze_time("2000-01-01 00:00:00")
@@ -738,7 +737,7 @@ class TestPmsPricelistRules(common.TransactionCase):
# ASSERT
self.assertEqual(
expected_price, reservation.price_total, "The price is not as expected"
expected_price, reservation.price_subtotal, "The price is not as expected"
)
@freeze_time("2000-01-01 00:00:00")
@@ -790,7 +789,7 @@ class TestPmsPricelistRules(common.TransactionCase):
# ASSERT
self.assertEqual(
expected_price, reservation.price_total, "The price is not as expected"
expected_price, reservation.price_subtotal, "The price is not as expected"
)
@freeze_time("2000-01-01 00:00:00")
@@ -856,5 +855,5 @@ class TestPmsPricelistRules(common.TransactionCase):
# ASSERT
self.assertEqual(
expected_price, reservation.price_total, "The price is not as expected"
expected_price, reservation.price_subtotal, "The price is not as expected"
)

View File

@@ -0,0 +1,12 @@
from odoo.tests.common import SavepointCase
class TestPmsFolioInvoice(SavepointCase):
def setUp(self):
super(TestPmsFolioInvoice, self).setUp()
def test_price_reservation(self):
"""Test create a reservation, and check price and discounts"""
def test_general_discount_reservation(self):
"""Test a discount in reservation head, and check lines"""

View File

@@ -0,0 +1,10 @@
from freezegun import freeze_time
from odoo.tests.common import SavepointCase
freeze_time("2000-02-02")
class TestPmsInvoiceSimpleInvoice(SavepointCase):
def setUp(self):
super(TestPmsInvoiceSimpleInvoice, self).setUp()

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" ?>
<odoo>
<record id="bank_statement_form" model="ir.ui.view">
<field name="model">account.bank.statement</field>
<field name="inherit_id" ref="account.view_bank_statement_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='company_id']" position="after">
<field name="property_id" />
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" ?>
<odoo>
<record id="account_journal_view_form" model="ir.ui.view">
<field name="model">account.journal</field>
<field name="inherit_id" ref="account.view_account_journal_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='company_id']" position="after">
<field name="pms_property_ids" widget="many2many_tags" />
</xpath>
</field>
</record>
</odoo>

View File

@@ -6,21 +6,37 @@
<field name="arch" type="xml">
<xpath expr="//field[@name='invoice_date']" position="after">
<field name="folio_ids" widget="many2many_tags" />
<field name="from_folio" invisible="1" />
<field name="from_reservation" invisible="1" />
<field name="pms_property_id" invisible="1" />
</xpath>
<xpath
expr="//field[@name='invoice_outstanding_credits_debits_widget']"
position="before"
expr="//field[@name='invoice_line_ids']/tree/field[@name='name']"
position="after"
>
<field
name="outstanding_folios_debits_widget"
colspan="2"
nolabel="1"
widget="payment"
attrs="{'invisible': [('state', 'not in', 'open')]}"
<button
name="invoice_filter_days"
type="object"
icon="fa-calendar"
string="Filter-days"
aria-label="Change Period"
class="float-right"
attrs="{
'column_invisible': ['|',('parent.from_reservation', '=', False),('parent.state', '!=', 'draft')],
'invisible': [('reservation_line_ids', '=', [])]
}"
/>
<field name="reservation_line_ids" invisible="1" />
</xpath>
<!-- <xpath
expr="//field[@name='invoice_line_ids']/tree/field[@name='quantity']"
position="attributes"
>
<attribute name="attrs">
{
'readonly': [('reservation_line_ids', '!=', False)],
}
</attribute>
</xpath> -->
</field>
</record>
</odoo>

View File

@@ -6,211 +6,8 @@
<field name="arch" type="xml">
<xpath expr="//field[@name='date']" position="after">
<field name="folio_id" />
<field name="save_amount" invisible="1" />
<field name="save_journal_id" invisible="1" />
<field name="save_date" invisible="1" />
</xpath>
</field>
</record>
<record id="account_payment_view_form_folio" model="ir.ui.view">
<field name="name">account.payment.folio.form</field>
<field name="model">account.payment</field>
<field name="priority">20</field>
<field name="arch" type="xml">
<form string="Register Payment" version="7">
<header>
<button
name="action_draft"
class="oe_highlight"
states="cancelled"
string="Set To Draft"
type="object"
/>
<button
string="Validate"
name="post"
type="object"
class="btn-primary"
attrs="{'invisible': [('state','!=','draft')]}"
/>
<button
string="Modify"
name="modify"
type="object"
class="oe_edit_only btn-primary"
attrs="{'invisible': [('state','=','draft')]}"
/>
<!-- <button
string="Return"
name="return_payment_folio"
type="object"
class="oe_edit_only btn-primary"
attrs="{'invisible': [('state','=','draft')]}"
/> -->
<button
string="Delete"
name="delete"
type="object"
class="oe_read_only btn-primary"
attrs="{'invisible': [('state','=','draft')]}"
/>
<button
string="Cancel"
name="cancel"
class="oe_read_only btn-default"
special="cancel"
/>
<field
name="state"
widget="statusbar"
statusbar_visible="draft,posted,reconciled,cancelled"
/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<!-- <button
class="oe_stat_button"
name="button_journal_entries"
string="Journal Items"
type="object"
groups="account.group_account_user"
attrs="{'invisible':[('move_line_ids','=',[])]}"
icon="fa-bars"
/> -->
<!-- <field name="move_line_ids" invisible="1" /> -->
<!-- <button
class="oe_stat_button"
name="button_invoices"
string="Invoices"
type="object"
attrs="{'invisible':[('has_invoices','=',False)]}"
icon="fa-bars"
/> -->
<!-- <button
class="oe_stat_button"
name="open_payment_matching_screen"
string="Payment Matching"
type="object"
attrs="{'invisible':[('move_reconciled','=',True)]}"
icon="fa-university"
/> -->
<!-- <field name="has_invoices" invisible="1" />
<field name="move_reconciled" invisible="1" /> -->
</div>
<field name="id" invisible="1" />
<div
class="oe_title"
attrs="{'invisible': [('state', '=', 'draft')]}"
>
<h1>
<field name="name" />
</h1>
</div>
<group>
<group>
<field name="company_id" invisible="1" />
<field name="payment_type" invisible="1" />
<field name="save_amount" invisible="1" />
<field name="save_date" invisible="1" />
<field name="save_journal_id" invisible="1" />
<field
name="partner_type"
widget="selection"
invisible="1"
/>
<field
name="partner_id"
attrs="{
'required': [('state', '=', 'draft'), ('payment_type', 'in', ('inbound', 'outbound'))],
'invisible': [('payment_type', 'not in', ('inbound', 'outbound'))]
}"
context="{
'default_is_company': True,
'default_supplier': payment_type == 'outbound',
'default_customer': payment_type == 'inbound'
}"
/>
<label for="amount" />
<div name="amount_div" class="o_row">
<field name="amount" />
<field
name="currency_id"
options="{'no_create': True, 'no_open': True}"
groups="base.group_multi_currency"
attrs="{'readonly': [('state', '!=', 'draft')]}"
/>
</div>
<field name="journal_id" widget="selection" />
<!-- <field
name="destination_journal_id"
widget="selection"
attrs="{
'required': [('payment_type', '=', 'transfer')],
'invisible': [('payment_type', '!=', 'transfer')],
'readonly': [('state', '!=', 'draft')]}"
/> -->
<field name="hide_payment_method" invisible="1" />
<field
name="payment_method_id"
string=" "
widget="radio"
attrs="{'invisible': [('hide_payment_method', '=', True)]}"
/>
<field name="payment_method_code" invisible="1" />
<field name="suitable_journal_ids" invisible="1" />
<field name="available_payment_method_ids" invisible="1" />
</group>
<group>
<!-- <field name="payment_date" /> -->
<!-- <field
name="communication"
attrs="{'invisible': [('state', '!=', 'draft'), ('communication', '=', False)]}"
/> -->
<field name="folio_id" readonly="1" force_save="1" />
</group>
</group>
</sheet>
<footer>
</footer>
<div class="oe_chatter">
<field
name="message_follower_ids"
widget="mail_followers"
groups="base.group_user"
/>
<field name="message_ids" widget="mail_thread" />
</div>
</form>
</field>
</record>
<record id="account_payment_view_tree_folio" model="ir.ui.view">
<field name="name">account.payment.folio.tree</field>
<field name="model">account.payment</field>
<field name="priority">20</field>
<field name="arch" type="xml">
<tree
decoration-info="state == 'draft'"
decoration-muted="state in ['reconciled', 'cancelled']"
edit="false"
>
<!-- <field name="payment_date" /> -->
<field name="name" />
<field name="journal_id" />
<field name="payment_method_id" />
<field name="partner_id" string="Customer" />
<field name="amount" sum="Amount" />
<field name="state" />
<field name="company_id" groups="base.group_multi_company" />
<field name="currency_id" invisible="1" />
<field name="partner_type" invisible="1" />
<button
type="object"
class="oe_stat_button"
icon="fa fa-2x fa-pencil"
name="modify_payment"
/>
</tree>
</field>
</record>
</odoo>

View File

@@ -25,6 +25,13 @@
string="Set to Done"
help="If a Folio is done, you cannot modify it manually anymore. However, you will still be able to invoice. This is used to freeze the Folio."
/>
<button
name="%(pms.action_view_folio_advance_payment_inv)d"
string="Create Invoice"
type="action"
class="btn-primary"
attrs="{'invisible': [('invoice_status', '!=', 'to invoice')]}"
/>
<field
name="state"
select="2"
@@ -35,11 +42,11 @@
<sheet>
<div class="oe_button_box" name="button_box">
<button
type="object"
type="action"
class="oe_stat_button"
id="payment_smart_button"
icon="fa-money"
name="action_pay"
name="%(pms.action_payment_folio)d"
attrs="{'invisible': [('pending_amount','&lt;=',0)]}"
>
<div class="o_form_field o_stat_info">
@@ -82,7 +89,31 @@
widget="percentpie"
/>
</button>
<button
name="action_view_invoice"
type="object"
class="oe_stat_button"
icon="fa-pencil-square-o"
attrs="{'invisible': [('invoice_count', '=', 0)]}"
>
<field
name="invoice_count"
widget="statinfo"
string="Invoices"
/>
</button>
</div>
<widget
name="web_ribbon"
title="Paid"
attrs="{'invisible': [('payment_state', '!=', 'paid')]}"
/>
<widget
name="web_ribbon"
title="Partial"
bg_color="bg-warning"
attrs="{'invisible': [('payment_state', '!=', 'partial')]}"
/>
<h2>
<field name="name" />
</h2>
@@ -186,6 +217,7 @@
</group>
<group invisible="1">
<field name="payment_state" invisible="1" force_save="1" />
<field name="move_ids" invisible="1" />
<field name="invoice_status" invisible="1" />
<field name="currency_id" invisible="1" />
@@ -193,6 +225,116 @@
<field name="invoices_paid" invisible="1" />
</group>
<notebook colspan="4" col="1">
<page string="Sale Lines">
<field
name="sale_line_ids"
widget="section_and_note_one2many"
>
<tree string="Sales Lines" editable="bottom">
<control>
<create
name="add_product_control"
string="Add a product"
/>
<create
name="add_section_control"
string="Add a section"
context="{'default_display_type': 'line_section'}"
/>
<create
name="add_note_control"
string="Add a note"
context="{'default_display_type': 'line_note'}"
/>
</control>
<field name="sequence" widget="handle" />
<!-- We do not display the type because we don't want the user to be bothered with that information if he has no section or note. -->
<field name="display_type" invisible="1" />
<!-- <field name="product_updatable" invisible="1"/> -->
<field
name="product_id"
options="{'no_open': True}"
force_save="1"
/>
<field
name="name"
widget="section_and_note_text"
optional="show"
/>
<field
name="analytic_tag_ids"
optional="hide"
groups="analytic.group_analytic_tags"
widget="many2many_tags"
options="{'color_field': 'color'}"
domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"
/>
<field
name="product_uom_qty"
decoration-info="(not display_type and invoice_status == 'to invoice')"
decoration-bf="(not display_type and invoice_status == 'to invoice')"
context="{
'partner_id': parent.partner_id,
'quantity': product_uom_qty,
'pricelist': parent.pricelist_id,
'uom': product_uom,
'company_id': parent.company_id
}"
/>
<field
name="qty_invoiced"
decoration-info="(not display_type and invoice_status == 'to invoice')"
decoration-bf="(not display_type and invoice_status == 'to invoice')"
string="Invoiced"
attrs="{'column_invisible': [('parent.state', '=', 'draft')]}"
optional="show"
/>
<field name="qty_to_invoice" invisible="1" />
<field name="product_uom_readonly" invisible="1" />
<field name="reservation_line_ids" invisible="1" />
<field
name="product_uom"
force_save="1"
string="UoM"
groups="uom.group_uom"
options='{"no_open": True}'
optional="show"
/>
<field name="price_unit" />
<field
name="tax_ids"
widget="many2many_tags"
options="{'no_create': True}"
domain="[('type_tax_use','=','sale'),('company_id','=',parent.company_id)]"
optional="show"
/>
<field
name="discount"
string="Disc.%"
groups="product.group_discount_per_so_line"
optional="show"
widget="product_discount"
/>
<field
name="price_subtotal"
widget="monetary"
groups="account.group_show_line_subtotals_tax_excluded"
/>
<field
name="price_total"
widget="monetary"
groups="account.group_show_line_subtotals_tax_included"
/>
<field name="state" invisible="0" />
<field name="invoice_status" invisible="0" />
<field name="currency_id" invisible="1" />
<field name="price_tax" invisible="1" />
<field name="company_id" invisible="1" />
</tree>
</field>
</page>
<page string="Reservation Rooms">
<field
name="reservation_ids"
@@ -239,24 +381,6 @@
/>
</group>
</page>
<page
name="payments"
string="Payments"
attrs="{'invisible': [('invoices_paid','&lt;=',0)]}"
>
<field
name="payment_ids"
context="{'tree_view_ref':'pms.account_payment_view_tree_folio', 'form_view_ref':'pms.account_payment_view_form_folio'}"
options="{'no_create': True}"
/>
</page>
<!-- <page
name="returns"
string="Retun Payments"
attrs="{'invisible': [('refund_amount','&lt;=',0)]}"
>
<field name="return_ids" options="{'no_create': True}" />
</page> -->
<page string="Other data" invisible="1">
<group>
<field name="user_id" />

View File

@@ -402,6 +402,10 @@
<field name="date" readonly="1" force_save="1" />
<field name="price" />
<field name="discount" />
<field
name="move_line_ids"
widget="many2many_tags"
/>
<field
name="cancel_discount"
attrs="{'column_invisible': [('parent.state','!=','cancelled')]}"

View File

@@ -33,11 +33,6 @@
<group>
<field name="pms_property_ids" widget="many2many_tags" />
<field name="company_id" />
<field
name="list_price"
widget='monetary'
options="{'currency_field': 'currency_id', 'field_digits': True}"
/>
</group>
</group>
<group colspan="2">
@@ -46,6 +41,15 @@
<field name="room_ids" widget="many2many_tags" />
<field name="total_rooms_count" />
</group>
<group name="accounting_group">
<field
name="list_price"
widget='monetary'
options="{'currency_field': 'currency_id', 'field_digits': True}"
/>
<field name="taxes_id" widget="many2many_tags" />
<field name="categ_id" />
</group>
</group>
<notebook>
<page string="Board Services">

View File

@@ -3,3 +3,6 @@ from . import wizard_massive_changes
from . import wizard_advanced_filters
from . import wizard_folio
from . import wizard_folio_availability
from . import folio_make_invoice_advance
from . import wizard_invoice_filter_days
from . import wizard_payment_folio

View File

@@ -0,0 +1,288 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import time
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class FolioAdvancePaymentInv(models.TransientModel):
_name = "folio.advance.payment.inv"
_description = "Folio Advance Payment Invoice"
@api.model
def _count(self):
return len(self._context.get("active_ids", []))
@api.model
def _default_product_id(self):
product_id = (
self.env["ir.config_parameter"]
.sudo()
.get_param("sale.default_deposit_product_id")
)
return self.env["product.product"].browse(int(product_id)).exists()
@api.model
def _default_deposit_account_id(self):
return self._default_product_id()._get_product_accounts()["income"]
@api.model
def _default_deposit_taxes_id(self):
return self._default_product_id().taxes_id
@api.model
def _default_has_down_payment(self):
if self._context.get("active_model") == "pms.folio" and self._context.get(
"active_id", False
):
folio = self.env["pms.folio"].browse(self._context.get("active_id"))
return folio.sale_line_ids.filtered(lambda line: line.is_downpayment)
return False
@api.model
def _default_currency_id(self):
if self._context.get("active_model") == "pms.folio" and self._context.get(
"active_id", False
):
sale_order = self.env["pms.folio"].browse(self._context.get("active_id"))
return sale_order.currency_id
advance_payment_method = fields.Selection(
[
("delivered", "Regular invoice"),
("percentage", "Down payment (percentage)"),
("fixed", "Down payment (fixed amount)"),
],
string="Create Invoice",
default="delivered",
required=True,
help="A standard invoice is issued with all the order \
lines ready for invoicing, \
according to their invoicing policy \
(based on ordered or delivered quantity).",
)
bill_services = fields.Boolean("Bill Services", default=True)
bill_rooms = fields.Boolean("Bill Rooms", default=True)
deduct_down_payments = fields.Boolean("Deduct down payments", default=True)
has_down_payments = fields.Boolean(
"Has down payments", default=_default_has_down_payment, readonly=True
)
product_id = fields.Many2one(
"product.product",
string="Down Payment Product",
domain=[("type", "=", "service")],
default=_default_product_id,
)
count = fields.Integer(default=_count, string="Order Count")
amount = fields.Float(
"Down Payment Amount",
digits="Account",
help="The percentage of amount to be invoiced in advance, taxes excluded.",
)
currency_id = fields.Many2one(
"res.currency", string="Currency", default=_default_currency_id
)
fixed_amount = fields.Monetary(
"Down Payment Amount (Fixed)",
help="The fixed amount to be invoiced in advance, taxes excluded.",
)
deposit_account_id = fields.Many2one(
"account.account",
string="Income Account",
domain=[("deprecated", "=", False)],
help="Account used for deposits",
default=_default_deposit_account_id,
)
deposit_taxes_id = fields.Many2many(
"account.tax",
string="Customer Taxes",
help="Taxes used for deposits",
default=_default_deposit_taxes_id,
)
@api.onchange("advance_payment_method")
def onchange_advance_payment_method(self):
if self.advance_payment_method == "percentage":
amount = self.default_get(["amount"]).get("amount")
return {"value": {"amount": amount}}
return {}
def _prepare_invoice_values(self, order, name, amount, line):
invoice_vals = {
"ref": order.client_order_ref,
"move_type": "out_invoice",
"invoice_origin": order.name,
"invoice_user_id": order.user_id.id,
"narration": order.note,
"partner_id": order.partner_invoice_id.id,
"currency_id": order.pricelist_id.currency_id.id,
"payment_reference": order.reference,
"invoice_payment_term_id": order.payment_term_id.id,
"partner_bank_id": order.company_id.partner_id.bank_ids[:1].id,
# 'campaign_id': order.campaign_id.id,
# 'medium_id': order.medium_id.id,
# 'source_id': order.source_id.id,
"invoice_line_ids": [
(
0,
0,
{
"name": name,
"price_unit": amount,
"quantity": 1.0,
"product_id": self.product_id.id,
"product_uom_id": line.product_uom.id,
"tax_ids": [(6, 0, line.tax_ids.ids)],
"folio_line_ids": [(6, 0, [line.id])],
"analytic_tag_ids": [(6, 0, line.analytic_tag_ids.ids)],
"analytic_account_id": order.analytic_account_id.id or False,
},
)
],
}
return invoice_vals
def _get_advance_details(self, order):
context = {"lang": order.partner_id.lang}
if self.advance_payment_method == "percentage":
amount = order.amount_untaxed * self.amount / 100
name = _("Down payment of %s%%") % (self.amount)
else:
amount = self.fixed_amount
name = _("Down Payment")
del context
return amount, name
def _create_invoice(self, order, line, amount):
if (self.advance_payment_method == "percentage" and self.amount <= 0.00) or (
self.advance_payment_method == "fixed" and self.fixed_amount <= 0.00
):
raise UserError(_("The value of the down payment amount must be positive."))
amount, name = self._get_advance_details(order)
invoice_vals = self._prepare_invoice_values(order, name, amount, line)
if order.fiscal_position_id:
invoice_vals["fiscal_position_id"] = order.fiscal_position_id.id
invoice = (
self.env["account.move"].sudo().create(invoice_vals).with_user(self.env.uid)
)
invoice.message_post_with_view(
"mail.message_origin_link",
values={"self": invoice, "origin": order},
subtype_id=self.env.ref("mail.mt_note").id,
)
return invoice
def _prepare_line(self, order, analytic_tag_ids, tax_ids, amount):
context = {"lang": order.partner_id.lang}
so_values = {
"name": _("Down Payment: %s") % (time.strftime("%m %Y"),),
"price_unit": amount,
"product_uom_qty": 0.0,
"folio_id": order.id,
"discount": 0.0,
"product_uom": self.product_id.uom_id.id,
"product_id": self.product_id.id,
"analytic_tag_ids": analytic_tag_ids,
"tax_ids": [(6, 0, tax_ids)],
"is_downpayment": True,
"sequence": order.sale_line_ids
and order.sale_line_ids[-1].sequence + 1
or 10,
}
del context
return so_values
def create_invoices(self):
folios = self.env["pms.folio"].browse(self._context.get("active_ids", []))
if self.advance_payment_method == "delivered":
lines_to_invoice = self._get_lines_to_invoice(
folios=folios,
bill_services=self.bill_services,
bill_rooms=self.bill_rooms,
)
folios._create_invoices(
final=self.deduct_down_payments,
lines_to_invoice=lines_to_invoice,
)
else:
# Create deposit product if necessary
if not self.product_id:
vals = self._prepare_deposit_product()
self.product_id = self.env["product.product"].create(vals)
self.env["ir.config_parameter"].sudo().set_param(
"sale.default_deposit_product_id", self.product_id.id
)
sale_line_obj = self.env["folio.sale.line"]
for order in folios:
amount, name = self._get_advance_details(order)
if self.product_id.invoice_policy != "order":
raise UserError(
_(
"""The product used to invoice a down payment should
have an invoice policy set to "Ordered quantities".
Please update your deposit product to be able
to create a deposit invoice."""
)
)
if self.product_id.type != "service":
raise UserError(
_(
"""The product used to invoice a down payment should
be of type 'Service'.
Please use another product or update this product."""
)
)
taxes = self.product_id.taxes_id.filtered(
lambda r: not order.company_id or r.company_id == order.company_id
)
tax_ids = order.fiscal_position_id.map_tax(taxes, self.product_id).ids
analytic_tag_ids = []
for line in order.sale_line_ids:
analytic_tag_ids = [
(4, analytic_tag.id, None)
for analytic_tag in line.analytic_tag_ids
]
line_values = self._prepare_line(
order, analytic_tag_ids, tax_ids, amount
)
line = sale_line_obj.sudo().create(line_values)
self._create_invoice(order, line, amount)
if self._context.get("open_invoices", False):
return folios.action_view_invoice()
return {"type": "ir.actions.act_window_close"}
def _prepare_deposit_product(self):
return {
"name": "Down payment",
"type": "service",
"invoice_policy": "order",
"property_account_income_id": self.deposit_account_id.id,
"taxes_id": [(6, 0, self.deposit_taxes_id.ids)],
"company_id": False,
}
@api.model
def _get_lines_to_invoice(self, folios, bill_services=True, bill_rooms=True):
lines_to_invoice = folios.sale_line_ids
if not self.bill_services:
lines_to_invoice = lines_to_invoice - lines_to_invoice.filtered(
lambda l: l.service_id and not l.service_id.is_board_service
)
if not self.bill_rooms:
lines_to_invoice = lines_to_invoice.filtered(
lambda l: l.reservation_id and l.reservation_line_ids
)
if not lines_to_invoice:
raise UserError(_("Nothing to invoice"))
return lines_to_invoice

View File

@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_folio_advance_payment_inv" model="ir.ui.view">
<field name="name">Invoice Orders</field>
<field name="model">folio.advance.payment.inv</field>
<field name="arch" type="xml">
<form string="Invoice Folio Order">
<p class="oe_grey">
Invoices will be created in draft so that you can review
them before validation.
</p>
<group>
<group>
<field
name="count"
attrs="{'invisible': [('count','=', 1)]}"
readonly="True"
/>
<field
name="advance_payment_method"
class="oe_inline"
widget="radio"
attrs="{'invisible': [('count','&gt;',1)]}"
/>
<field name="has_down_payments" invisible="1" />
<label
for="deduct_down_payments"
string=""
attrs="{'invisible': ['|', ('has_down_payments', '=', False), ('advance_payment_method', '!=', 'delivered')]}"
/>
<div
attrs="{'invisible': ['|', ('has_down_payments', '=', False), ('advance_payment_method', '!=', 'delivered')]}"
id="down_payment_details"
>
<field name="deduct_down_payments" nolabel="1" />
<label for="deduct_down_payments" />
</div>
<field
name="product_id"
context="{'default_invoice_policy': 'order'}"
class="oe_inline"
invisible="1"
/>
<label
for="amount"
attrs="{'invisible': [('advance_payment_method', 'not in', ('fixed','percentage'))]}"
/>
<div
attrs="{'invisible': [('advance_payment_method', 'not in', ('fixed','percentage'))]}"
id="payment_method_details"
>
<field name="currency_id" invisible="1" />
<field
name="fixed_amount"
attrs="{'required': [('advance_payment_method', '=', 'fixed')], 'invisible': [('advance_payment_method', '!=','fixed')]}"
class="oe_inline"
/>
<field
name="amount"
attrs="{'required': [('advance_payment_method', '=', 'percentage')], 'invisible': [('advance_payment_method', '!=', 'percentage')]}"
class="oe_inline"
/>
<span
attrs="{'invisible': [('advance_payment_method', '!=', 'percentage')]}"
class="oe_inline"
>%</span>
</div>
<field
name="deposit_account_id"
options="{'no_create': True}"
class="oe_inline"
attrs="{'invisible': ['|', ('advance_payment_method', 'not in', ('fixed', 'percentage')), ('product_id', '!=', False)]}"
groups="account.group_account_manager"
/>
<field
name="deposit_taxes_id"
class="oe_inline"
widget="many2many_tags"
domain="[('type_tax_use','=','sale')]"
attrs="{'invisible': ['|', ('advance_payment_method', 'not in', ('fixed', 'percentage')), ('product_id', '!=', False)]}"
/>
</group>
<group>
<field name="bill_services" widget="boolean_toggle" />
<field name="bill_rooms" widget="boolean_toggle" />
</group>
</group>
<footer>
<button
name="create_invoices"
id="create_invoice_open"
string="Create and View Invoice"
type="object"
context="{'open_invoices': True}"
class="btn-primary"
/>
<button
name="create_invoices"
id="create_invoice"
string="Create Invoice"
type="object"
/>
<button
string="Cancel"
class="btn-secondary"
special="cancel"
/>
</footer>
</form>
</field>
</record>
<record
id="action_view_folio_advance_payment_inv"
model="ir.actions.act_window"
>
<field name="name">Create invoices</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">folio.advance.payment.inv</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<!-- TODO: check if we need this -->
<field name="binding_model_id" ref="pms.model_pms_folio" />
<field name="binding_view_types">list</field>
</record>
</odoo>

View File

@@ -0,0 +1,114 @@
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class InvoiceFilterDays(models.TransientModel):
_name = "pms.invoice.filter.days"
_description = "Filter Days"
@api.model
def default_reservation_lines(self):
return (
self.env["account.move.line"]
.browse(self.env.context.get("active_ids"))
.reservation_line_ids
)
@api.model
def default_move_lines(self):
return self.env["account.move.line"].browse(self.env.context.get("active_ids"))
@api.model
def default_from_date(self):
return min(
self.env["account.move.line"]
.browse(self.env.context.get("active_ids"))
.reservation_line_ids.mapped("date")
)
@api.model
def default_to_date(self):
return max(
self.env["account.move.line"]
.browse(self.env.context.get("active_ids"))
.reservation_line_ids.mapped("date")
)
move_line_ids = fields.Many2many("account.move.line", default=default_move_lines)
move_ids = fields.Many2many("account.move", compute="_compute_move_ids")
reservation_line_ids = fields.Many2many(
"pms.reservation.line", default=default_reservation_lines
)
from_date = fields.Date("Date From", default=default_from_date)
to_date = fields.Date("Date to", default=default_to_date)
date_ids = fields.One2many(
comodel_name="pms.invoice.filter.days.items",
inverse_name="filter_wizard_id",
compute="_compute_date_ids",
store=True,
readonly=False,
)
def do_filter(self):
self.ensure_one()
invoice_lines = self.move_line_ids
for line in invoice_lines:
reservation_lines = line.reservation_line_ids.filtered(
lambda d: d.date in self.date_ids.filtered("included").mapped("date")
)
if not reservation_lines:
raise UserError(_("You can not remove all lines for invoice"))
else:
# Write on invoice for syncr business/account
line.move_id.write(
{
"invoice_line_ids": [
(
1,
line.id,
{
"reservation_line_ids": [
(6, False, reservation_lines.ids)
],
"quantity": len(reservation_lines),
},
)
]
}
)
@api.depends("from_date", "to_date", "reservation_line_ids")
def _compute_date_ids(self):
self.ensure_one()
date_list = [(5, 0, 0)]
dates = self.reservation_line_ids.filtered(
lambda d: d.date >= self.from_date and d.date <= self.to_date
).mapped("date")
for date in dates:
date_list.append(
(
0,
False,
{
"date": date,
},
)
)
self.date_ids = date_list
@api.depends("move_line_ids")
def _compute_move_ids(self):
self.ensure_one()
self.move_ids = [(6, 0, self.move_line_ids.mapped("move_id.id"))]
class InvoiceFilterDaysItems(models.TransientModel):
_name = "pms.invoice.filter.days.items"
_description = "Item Days"
_rec_name = "date"
date = fields.Date("Date")
included = fields.Boolean("Included", default=True)
filter_wizard_id = fields.Many2one(comodel_name="pms.invoice.filter.days")

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<data>
<record id="pms_invoice_filter_days_form" model="ir.ui.view">
<field name="name">pms.invoice.filter.days.form</field>
<field name="model">pms.invoice.filter.days</field>
<field name="arch" type="xml">
<form>
<div class="o_row">
<field
name="from_date"
widget="daterange"
nolabel="1"
class="oe_inline"
options="{'related_end_date': 'to_date'}"
/>
<i
class="fa fa-long-arrow-right mx-2"
aria-label="Arrow icon"
title="Arrow"
/>
<field
name="to_date"
widget="daterange"
nolabel="1"
class="oe_inline"
options="{'related_start_date': 'from_date'}"
/>
</div>
<group>
<field name="move_ids" invisible="0" />
<field
name="move_line_ids"
string="Invoice Lines"
widget="many2many_tags"
options="{'no_create':True, 'no_open':True}"
domain="[
('move_id', 'in', move_ids),
('reservation_line_ids', '!=', 0),
('exclude_from_invoice_tab', '=', False),
('display_type', '=', False)
]"
/>
<field
name="date_ids"
default_focus="1"
string="Dates to invoice"
>
<tree
editable="bottom"
create="false"
delete="false"
decoration-muted="not included"
decoration-primary="included"
>
<field name="date" readonly="1" force_save="1" />
<field name="included" />
</tree>
</field>
<field name="reservation_line_ids" invisible="1" />
</group>
<footer>
<button
string="Apply"
name="do_filter"
type="object"
class="oe_highlight"
/>
<button
string="Cancel"
class="btn btn-secondary"
special="cancel"
/>
</footer>
</form>
</field>
</record>
<record id="pms_invoice_filter_days_action" model="ir.actions.act_window">
<field name="name">Filter Days</field>
<field name="res_model">pms.invoice.filter.days</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,124 @@
import logging
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
_logger = logging.getLogger(__name__)
class WizardPaymentFolio(models.TransientModel):
_name = "wizard.payment.folio"
_description = "Payments"
@api.model
def default_folio_id(self):
return self.env["pms.folio"].browse(self._context.get("active_id", [])).id
@api.model
def _default_amount(self):
folio = self.env["pms.folio"].browse(self._context.get("active_id", []))
return folio.pending_amount
@api.model
def _default_partner(self):
folio = self.env["pms.folio"].browse(self._context.get("active_id", []))
return folio.partner_id.id
folio_id = fields.Many2one(
"pms.folio",
string="Folio",
required=True,
default=default_folio_id,
)
reservation_ids = fields.Many2many(
"pms.reservation",
string="Reservations",
)
service_ids = fields.Many2many(
"pms.service",
string="Services",
)
payment_method_id = fields.Many2one(
"account.journal",
string="Payment Method",
required=True,
domain="[('id', 'in', allowed_method_ids)]",
)
allowed_method_ids = fields.Many2many(
"account.journal",
"allowed_payment_journal_rel",
"payment_id",
"journal_id",
compute="_compute_allowed_method_ids",
store="True",
)
amount = fields.Float("Amount", digits=("Product Price"), default=_default_amount)
date = fields.Date("Date", default=fields.Date.context_today, required=True)
partner_id = fields.Many2one("res.partner", default=_default_partner)
@api.depends("folio_id")
def _compute_allowed_method_ids(self):
self.ensure_one()
journal_ids = False
if self.folio_id:
journal_ids = self.folio_id.pms_property_id._get_payment_methods().ids
self.allowed_method_ids = journal_ids
def button_payment(self):
BankStatementLine = self.env["account.bank.statement.line"]
line = self._get_statement_line_vals(
journal=self.payment_method_id,
receivable_account=self.payment_method_id.suspense_account_id,
user=self.env.user,
amount=self.amount,
folios=self.folio_id,
partner=self.partner_id,
date=self.date,
)
BankStatementLine.sudo().create(line)
def _get_statement_line_vals(
self,
journal,
receivable_account,
user,
amount,
folios,
reservations=False,
services=False,
partner=False,
date=False,
):
property_folio_id = folios.mapped("pms_property_id.id")
if len(property_folio_id) != 1:
raise ValidationError(_("Only can payment by property"))
statement = (
self.env["account.bank.statement"]
.sudo()
.search(
[
("journal_id", "=", journal.id),
("property_id", "=", property_folio_id[0]),
("state", "=", "open"),
]
)
)
reservation_ids = reservations.ids if reservations else []
service_ids = services.ids if services else []
# TODO: If not open statement, create new, with cash control option
if statement:
return {
"date": date,
"amount": amount,
"partner_id": partner.id if partner else False,
"statement_folio_ids": [(6, 0, folios.ids)],
"reservation_ids": [(6, 0, reservation_ids)],
"service_ids": [(6, 0, service_ids)],
"payment_ref": folios.mapped("name"),
"statement_id": statement.id,
"journal_id": statement.journal_id.id,
"counterpart_account_id": receivable_account.id,
}
else:
return False

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="wizard_payment_folio_view_form" model="ir.ui.view">
<field name="name">wizard.payment.folio.view.form</field>
<field name="model">wizard.payment.folio</field>
<field name="arch" type="xml">
<form string="Payment">
<group>
<group>
<field name="allowed_method_ids" invisible="1" />
<field name="payment_method_id" widget="radio" />
<field name="amount" />
</group>
<group>
<field name="date" />
<field name="partner_id" />
<field name="folio_id" />
</group>
</group>
<footer>
<button
type="object"
class="btn-primary"
id="payment"
name="button_payment"
string="Pay"
/>
<button string="Cancel" class="oe_link" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="action_payment_folio" model="ir.actions.act_window">
<field name="name">Payment Folio</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">wizard.payment.folio</field>
<field name="view_id" ref="wizard_payment_folio_view_form" />
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>