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

@@ -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>