[IMP] contract: Portal

This commit is contained in:
Víctor Martínez
2020-12-16 08:45:07 +01:00
committed by Christopher Rogos
parent c228e52167
commit 1a1d301251
18 changed files with 490 additions and 1 deletions

View File

@@ -1,2 +1,3 @@
from . import controllers
from . import models
from . import wizards

View File

@@ -6,6 +6,7 @@
# Copyright 2016-2017 LasLabs Inc.
# Copyright 2018-2019 ACSONE SA/NV
# Copyright 2020 Tecnativa - Pedro M. Baeza
# Copyright 2020 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
@@ -15,7 +16,7 @@
"license": "AGPL-3",
"author": "Tecnativa, ACSONE SA/NV, Odoo Community Association (OCA)",
"website": "https://github.com/oca/contract",
"depends": ["base", "account", "product"],
"depends": ["base", "account", "product", "portal"],
"external_dependencies": {"python": ["dateutil"]},
"data": [
"security/groups.xml",
@@ -43,6 +44,8 @@
"views/res_partner_view.xml",
"views/res_config_settings.xml",
"views/contract_terminate_reason.xml",
"views/contract_portal_templates.xml",
],
"demo": ["demo/assets.xml"],
"installable": True,
}

View File

@@ -0,0 +1,3 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import main

View File

@@ -0,0 +1,99 @@
# Copyright 2020 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, http
from odoo.exceptions import AccessError, MissingError
from odoo.http import request
from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager
class PortalContract(CustomerPortal):
def _prepare_portal_layout_values(self):
values = super()._prepare_portal_layout_values()
model = "contract.contract"
values["contract_count"] = 0
if request.env[model].check_access_rights("read", raise_exception=False):
values["contract_count"] = request.env[model].search_count([])
return values
def _contract_get_page_view_values(self, contract, access_token, **kwargs):
values = {
"page_name": "Contracts",
"contract": contract,
}
return self._get_page_view_values(
contract, access_token, values, "my_contracts_history", False, **kwargs
)
def _get_filter_domain(self, kw):
return []
@http.route(
["/my/contracts", "/my/contracts/page/<int:page>"],
type="http",
auth="user",
website=True,
)
def portal_my_contracts(
self, page=1, date_begin=None, date_end=None, sortby=None, **kw
):
values = self._prepare_portal_layout_values()
contract_obj = request.env["contract.contract"]
domain = self._get_filter_domain(kw)
searchbar_sortings = {
"date": {"label": _("Date"), "order": "recurring_next_date desc"},
"name": {"label": _("Name"), "order": "name desc"},
"code": {"label": _("Reference"), "order": "code desc"},
}
# default sort by order
if not sortby:
sortby = "date"
order = searchbar_sortings[sortby]["order"]
# count for pager
contract_count = contract_obj.search_count(domain)
# pager
pager = portal_pager(
url="/my/contracts",
url_args={
"date_begin": date_begin,
"date_end": date_end,
"sortby": sortby,
},
total=contract_count,
page=page,
step=self._items_per_page,
)
# content according to pager and archive selected
contracts = contract_obj.search(
domain, order=order, limit=self._items_per_page, offset=pager["offset"]
)
request.session["my_contracts_history"] = contracts.ids[:100]
values.update(
{
"date": date_begin,
"contracts": contracts,
"page_name": "Contracts",
"pager": pager,
"default_url": "/my/contracts",
"searchbar_sortings": searchbar_sortings,
"sortby": sortby,
}
)
return request.render("contract.portal_my_contracts", values)
@http.route(
["/my/contracts/<int:contract_contract_id>"],
type="http",
auth="public",
website=True,
)
def portal_my_contract_detail(self, contract_contract_id, access_token=None, **kw):
try:
contract_sudo = self._document_check_access(
"contract.contract", contract_contract_id, access_token
)
except (AccessError, MissingError):
return request.redirect("/my")
values = self._contract_get_page_view_values(contract_sudo, access_token, **kw)
return request.render("contract.portal_contract_page", values)

View File

@@ -62,6 +62,8 @@
%endif
<p></p>
</div>
<p></p>
<a href="${object.get_base_url()}/my/contracts/${object.id}?access_token=${object.access_token}" target="_blank" style="background-color:#875A7B;padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">View contract</a>
</div>
]]></field>
</record>

12
contract/demo/assets.xml Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- License AGPL-3.0 (http://www.gnu.org/licenses/agpl.html). -->
<odoo>
<template id="contract_frontend_demo" inherit_id="web.assets_frontend">
<xpath expr="//script[last()]" position="after">
<script
type="text/javascript"
src="/contract/static/src/js/contract_portal_tour.js"
/>
</xpath>
</template>
</odoo>

View File

@@ -21,6 +21,7 @@ class ContractContract(models.Model):
"mail.activity.mixin",
"contract.abstract.contract",
"contract.recurrency.mixin",
"portal.mixin",
]
active = fields.Boolean(default=True,)
@@ -113,6 +114,19 @@ class ContractContract(models.Model):
track_visibility="onchange",
)
def _compute_access_url(self):
for record in self:
record.access_url = "/my/contracts/{}".format(record.id)
def action_preview(self):
"""Invoked when 'Preview' button in contract form view is clicked."""
self.ensure_one()
return {
"type": "ir.actions.act_url",
"target": "self",
"url": self.get_portal_url(),
}
def _inverse_partner_id(self):
for rec in self:
if not rec.invoice_partner_id:

View File

@@ -1,2 +1,4 @@
To view discount field in contract line, you need to set *Discount on lines* in
user access rights.
Contracts can be viewed on the portal (list and detail) if the user logged into the portal is a follower of the contract.

View File

@@ -2,3 +2,5 @@ This module enables contracts management with recurring
invoicing functions. Also you can print and send by email contract report.
It works for customer contract and supplier contracts.
Contracts are shown in portal.

View File

@@ -23,3 +23,9 @@
#. Contract templates can be created from the Configuration -> Contracts -> Contract Templates menu.
They allow to define default journal, price list and lines when creating a contract.
To use it, just select the template on the contract and fields will be filled automatically.
* Contracts appear in portal to following users in every contract:
.. image:: ../static/src/screenshots/portal-my.png
.. image:: ../static/src/screenshots/portal-list.png
.. image:: ../static/src/screenshots/portal-detail.png

View File

@@ -8,6 +8,14 @@
name="domain_force"
>['|',('company_id','=',False),('company_id','in',company_ids)]</field>
</record>
<record id="rule_contract_contract_portal" model="ir.rule">
<field name="name">Contract contract portal</field>
<field name="model_id" ref="model_contract_contract" />
<field
name="domain_force"
>[('message_partner_ids', 'in', [user.partner_id.id])]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]" />
</record>
<record id="rule_contract_line_multi_company" model="ir.rule">
<field name="name">Contract line multi-company</field>
<field name="model_id" ref="model_contract_line" />

View File

@@ -3,7 +3,9 @@
"contract_template_user","Recurring user","model_contract_template","account.group_account_invoice",1,0,0,0
"contract_manager","Recurring manager","model_contract_contract","account.group_account_manager",1,1,1,1
"contract_user","Recurring user","model_contract_contract","account.group_account_invoice",1,0,0,0
"contract_portal","Recurring portal","model_contract_contract","base.group_portal",1,0,0,0
"contract_line_manager","Recurring manager","model_contract_line","account.group_account_manager",1,1,1,1
"contract_line_user","Recurring user","model_contract_line","account.group_account_invoice",1,0,0,0
"contract_line_portal","Recurring portal","model_contract_line","base.group_portal",1,0,0,0
"contract_template_line_manager","Recurring manager","model_contract_template_line","account.group_account_manager",1,1,1,1
"contract_template_line_user","Recurring user","model_contract_template_line","account.group_account_invoice",1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
3 contract_template_user Recurring user model_contract_template account.group_account_invoice 1 0 0 0
4 contract_manager Recurring manager model_contract_contract account.group_account_manager 1 1 1 1
5 contract_user Recurring user model_contract_contract account.group_account_invoice 1 0 0 0
6 contract_portal Recurring portal model_contract_contract base.group_portal 1 0 0 0
7 contract_line_manager Recurring manager model_contract_line account.group_account_manager 1 1 1 1
8 contract_line_user Recurring user model_contract_line account.group_account_invoice 1 0 0 0
9 contract_line_portal Recurring portal model_contract_line base.group_portal 1 0 0 0
10 contract_template_line_manager Recurring manager model_contract_template_line account.group_account_manager 1 1 1 1
11 contract_template_line_user Recurring user model_contract_template_line account.group_account_invoice 1 0 0 0

View File

@@ -0,0 +1,23 @@
odoo.define("contract.tour", function(require) {
"use strict";
var tour = require("web_tour.tour");
tour.register(
"contract_portal_tour",
{
test: true,
url: "/my",
},
[
{
content: "Go /my/contracts url",
trigger: 'a[href*="/my/contracts"]',
},
{
content: "Go to Contract item",
trigger: ".tr_contract_link:eq(0)",
},
]
);
});

View File

@@ -1,3 +1,4 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import test_contract
from . import test_portal

View File

@@ -2302,3 +2302,8 @@ class TestContract(TestContractBase):
# Assign same currency as computed one
self.contract2.currency_id = currency_cad.id
self.assertFalse(self.contract2.manual_currency_id)
def test_contract_action_preview(self):
action = self.contract.action_preview()
self.assertIn("/my/contracts/", action["url"])
self.assertIn("access_token=", action["url"])

View File

@@ -0,0 +1,31 @@
# Copyright 2020 Tecnativa - Víctor Martínez
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl)
import odoo.tests
from odoo import http
@odoo.tests.tagged("post_install", "-at_install")
class TestContractPortal(odoo.tests.HttpCase):
def test_tour(self):
partner = self.env["res.partner"].create({"name": "partner test contract"})
contract = self.env["contract.contract"].create(
{"name": "Test Contract", "partner_id": partner.id}
)
user_portal = self.env.ref("base.demo_user0")
contract.message_subscribe(partner_ids=user_portal.partner_id.ids)
self.phantom_js(
"/",
"odoo.__DEBUG__.services['web_tour.tour'].run('contract_portal_tour')",
"odoo.__DEBUG__.services['web_tour.tour'].tours.contract_portal_tour.ready",
login="portal",
)
# Contract access
self.authenticate("portal", "portal")
http.root.session_store.save(self.session)
url_contract = "/my/contracts/{}?access_token={}".format(
contract.id, contract.access_token,
)
self.assertEqual(self.url_open(url=url_contract).status_code, 200)
contract.message_unsubscribe(partner_ids=user_portal.partner_id.ids)
self.assertEqual(self.url_open(url=url_contract).status_code, 200)

View File

@@ -66,6 +66,7 @@
attrs="{'invisible': [('is_terminated','=',False)]}"
groups="contract.can_terminate_contract"
/>
<button type="object" string="Preview" name="action_preview" />
</header>
<sheet string="Contract">
<field name="active" invisible="1" />

View File

@@ -0,0 +1,274 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Tecnativa - Víctor Martínez
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<template
id="portal_my_home_menu_contract"
name="Portal layout : Contract menu entries"
inherit_id="portal.portal_breadcrumbs"
priority="35"
>
<xpath expr="//ol[hasclass('o_portal_submenu')]" position="inside">
<li
t-if="page_name == 'Contracts'"
t-attf-class="breadcrumb-item #{'active ' if not contract else ''}"
>
<a
t-if="contract"
t-attf-href="/my/contracts?{{ keep_query() }}"
>Contracts</a>
<t t-else="">Contracts</t>
</li>
<li t-if="contract" class="breadcrumb-item active">
<t t-esc="contract.name" />
</li>
</xpath>
</template>
<template
id="portal_my_home_contract"
name="Portal My Home : Contract entries"
inherit_id="portal.portal_my_home"
priority="30"
>
<xpath expr="//div[hasclass('o_portal_docs')]" position="inside">
<t t-call="portal.portal_docs_entry" t-if="contract_count">
<t t-set="title">Contracts</t>
<t t-set="url" t-value="'/my/contracts'" />
<t t-set="count" t-value="contract_count" />
</t>
</xpath>
</template>
<template id="portal_my_contracts" name="My Contracts">
<t t-call="portal.portal_layout">
<t t-set="breadcrumbs_searchbar" t-value="True" />
<t t-call="portal.portal_searchbar">
<t t-set="title">Contracts</t>
</t>
<t t-if="contracts" t-call="portal.portal_table">
<thead>
<tr class="active">
<th>Contract #</th>
<th class='d-none d-md-table-cell'>Date</th>
<th class='d-none d-md-table-cell'>Date end</th>
<th class='text-right'>Reference</th>
</tr>
</thead>
<tbody>
<t t-foreach="contracts" t-as="contract">
<tr>
<td>
<a
t-att-href="contract.get_portal_url()"
t-attf-class="tr_contract_link"
t-att-title="contract.name"
>
<t t-esc="contract.name" />
</a>
</td>
<td class="d-none d-md-table-cell">
<span t-field="contract.recurring_next_date" />
</td>
<td class="d-none d-md-table-cell">
<span t-field="contract.date_end" />
</td>
<td class='text-right'>
<span t-field="contract.code" />
</td>
</tr>
</t>
</tbody>
</t>
</t>
</template>
<template id="portal_contract_page" name="My Contract">
<t t-call="portal.portal_layout">
<t t-set="o_portal_fullwidth_alert">
<t t-call="portal.portal_back_in_edit_mode">
<t
t-set="backend_url"
t-value="'/web#return_label=Website&amp;model=contract.contract&amp;id=%s&amp;view_type=form' % (contract.id)"
/>
</t>
</t>
<t t-call="portal.portal_record_layout">
<t t-set="card_header">
<h5 class="mb-0">
<span>
Contract Order - <span t-field="contract.name" />
</span>
</h5>
</t>
<t t-set="card_body">
<div id="general_information">
<div class="row mt4">
<div
t-if="contract.partner_id"
class="col-12 col-md-6 mb-4 mb-md-0"
>
<h6>
<strong>Customer:</strong>
</h6>
<div class="row">
<div class="col flex-grow-0 pr-3">
<img
t-if="contract.partner_id.image_128"
class="rounded-circle mt-1 o_portal_contact_img"
t-att-src="image_data_uri(contract.partner_id.image_128)"
alt="Contact"
/>
<img
t-else=""
class="rounded-circle mt-1 o_portal_contact_img"
src="/web/static/src/img/user_menu_avatar.png"
alt="Contact"
/>
</div>
<div class="col pl-sm-0">
<address
t-field="contract.partner_id"
t-options='{"widget": "contact", "fields": ["name", "email", "phone"]}'
/>
</div>
</div>
</div>
<div t-if="contract.user_id" class="col-12 col-md-6">
<h6>
<strong>Responsible:</strong>
</h6>
<div class="row">
<div class="col flex-grow-0 pr-3">
<img
t-if="contract.user_id.image_128"
class="rounded-circle mt-1 o_portal_contact_img"
t-att-src="image_data_uri(contract.user_id.image_128)"
alt="Contact"
/>
<img
t-else=""
class="rounded-circle mt-1 o_portal_contact_img"
src="/web/static/src/img/user_menu_avatar.png"
alt="Contact"
/>
</div>
<div class="col pl-sm-0">
<address
t-field="contract.user_id"
t-options='{"widget": "contact", "fields": ["name", "email", "phone"]}'
/>
</div>
</div>
</div>
</div>
<div class="row mt32" id="product_information">
<div class="col-12 col-md-6 mb-4 mb-md-0">
<div t-if="contract.code" class="row mb-2 mb-sm-1">
<div class="col-12 col-sm-4">
<strong>Reference</strong>
</div>
<div class="col-12 col-sm-8">
<span t-field="contract.code" />
</div>
</div>
<div
t-if="contract.recurring_next_date"
class="row mb-2 mb-sm-1"
>
<div class="col-12 col-sm-4">
<strong>Date of Next Invoice</strong>
</div>
<div class="col-12 col-sm-8">
<span
t-field="contract.recurring_next_date"
t-options='{"widget": "date"}'
/>
</div>
</div>
<div t-if="contract.date_end" class="row mb-2 mb-sm-1">
<div class="col-12 col-sm-4">
<strong>Date end</strong>
</div>
<div class="col-12 col-sm-8">
<span
t-field="contract.date_end"
t-options='{"widget": "date"}'
/>
</div>
</div>
</div>
</div>
<div id="item_details">
<table class="table table-sm" id="sales_order_table">
<thead class="bg-100">
<tr>
<th class="text-left">Description</th>
<th class="text-right">Recurrence</th>
<th class="text-right">Date of next invoice</th>
<th class="text-right">Last date invoice</th>
</tr>
</thead>
<tbody class="contract_tbody">
<t
t-foreach="contract.contract_line_ids"
t-as="line"
>
<tr>
<td id="line_name">
<span t-field="line.name" />
</td>
<td class="text-right">
<span
t-field="line.recurring_interval"
/>
<t
t-if="line.recurring_rule_type=='daily'"
>Day(s)</t>
<t
t-if="line.recurring_rule_type=='weekly'"
>Week(s)</t>
<t
t-if="line.recurring_rule_type=='monthly'"
>Month(s)</t>
<t
t-if="line.recurring_rule_type=='monthlylastday'"
>Month(s) last day</t>
<t
t-if="line.recurring_rule_type=='quarterly'"
>Quarter(s)</t>
<t
t-if="line.recurring_rule_type=='semesterly'"
>Semester(s)</t>
<t
t-if="line.recurring_rule_type=='yearly'"
>Year(s)</t>
</td>
<td class="text-right">
<span
t-field="line.recurring_next_date"
/>
</td>
<td class="text-right">
<span
t-field="line.last_date_invoiced"
/>
</td>
</tr>
</t>
</tbody>
</table>
</div>
</div>
</t>
</t>
<!-- chatter -->
<div id="contract_communication" class="mt-4">
<h2>Communication</h2>
<t t-call="portal.message_thread">
<t t-set="object" t-value="contract" />
<t t-set="token" t-value="contract.access_token" />
<t t-set="pid" t-value="pid" />
<t t-set="hash" t-value="hash" />
</t>
</div>
</t>
</template>
</odoo>