Skip to content

Commit

Permalink
Merge pull request #19 from onesteinbv/16.0-purchase-sign
Browse files Browse the repository at this point in the history
[ADD] Added purchase_sign module
  • Loading branch information
ByteMeAsap authored May 7, 2024
2 parents 5ac3cfa + dd11ecc commit 7b730bb
Show file tree
Hide file tree
Showing 17 changed files with 432 additions and 0 deletions.
4 changes: 4 additions & 0 deletions purchase_sign/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright 2024 Onestein
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import controllers
from . import models
21 changes: 21 additions & 0 deletions purchase_sign/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright 2024 Onestein (<http://www.onestein.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

{
"name": "Purchase Sign",
"version": "16.0.1.0.0",
"license": "AGPL-3",
"category": "Purchase",
"author": "Onestein, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/vertical-association",
"depends": ["purchase"],
"data": [
"report/purchase_order_template.xml",
"templates/purchase_portal_templates.xml",
"views/purchase_view.xml",
"views/res_config_settings_view.xml",
],
"assets": {
"web.assets_tests": ["purchase_sign/static/tests/tours/purchase_signature.js"],
},
}
2 changes: 2 additions & 0 deletions purchase_sign/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
from . import main
76 changes: 76 additions & 0 deletions purchase_sign/controllers/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Copyright 2024 Onestein
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import binascii

from odoo import _, fields, http
from odoo.exceptions import AccessError, MissingError
from odoo.http import request

from odoo.addons.portal.controllers.mail import _message_post_helper
from odoo.addons.portal.controllers.portal import CustomerPortal


class PortalPurchase(CustomerPortal):
def _purchase_order_get_page_view_values(self, order, access_token, **kwargs):
response = super(PortalPurchase, self)._purchase_order_get_page_view_values(
order=order, access_token=access_token, **kwargs
)
if kwargs.get("message"):
response.update({"message": kwargs.get("message")})
return response

@http.route(
["/my/purchase/<int:order_id>/accept"], type="json", auth="public", website=True
)
def portal_purchase_accept(
self, order_id, access_token=None, name=None, signature=None
):
# get from query string if not on json param
access_token = access_token or request.httprequest.args.get("access_token")
try:
order_sudo = self._document_check_access(
"purchase.order", order_id, access_token=access_token
)
except (AccessError, MissingError):
return {"error": _("Invalid order.")}

if not order_sudo._has_to_be_signed():
return {
"error": _("The order is not in a state requiring customer signature.")
}
if not signature:
return {"error": _("Signature is missing.")}

try:
order_sudo.write(
{
"signed_by": name,
"signed_on": fields.Datetime.now(),
"signature": signature,
}
)
request.env.cr.commit()
except (TypeError, binascii.Error):
return {"error": _("Invalid signature data.")}
order_sudo.button_confirm()
pdf = (
request.env["ir.actions.report"]
.sudo()
._render_qweb_pdf("purchase.action_report_purchase_order", [order_sudo.id])[
0
]
)

_message_post_helper(
"purchase.order",
order_sudo.id,
_("Order signed by %s", name),
attachments=[("%s.pdf" % order_sudo.name, pdf)],
token=access_token,
)

query_string = "&message=sign_ok"
return {
"force_refresh": True,
"redirect_url": order_sudo.get_portal_url(query_string=query_string),
}
4 changes: 4 additions & 0 deletions purchase_sign/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
from . import purchase_order
from . import res_company
from . import res_config_settings
32 changes: 32 additions & 0 deletions purchase_sign/models/purchase_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Copyright 2024 Onestein
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models

from odoo.addons.purchase.models.purchase import PurchaseOrder as Purchase


class PurchaseOrder(models.Model):
_inherit = "purchase.order"

require_signature = fields.Boolean(
string="Online Signature",
compute="_compute_require_signature",
store=True,
readonly=False,
precompute=True,
states=Purchase.READONLY_STATES,
help="Request a online signature and/or payment to the customer in order to confirm orders automatically.",
)
signature = fields.Image(
copy=False, attachment=True, max_width=1024, max_height=1024
)
signed_by = fields.Char(copy=False)
signed_on = fields.Datetime(copy=False)

@api.depends("company_id")
def _compute_require_signature(self):
for order in self:
order.require_signature = order.company_id.purchase_portal_confirmation_sign

def _has_to_be_signed(self):
return self.state == "sent" and self.require_signature and not self.signature
11 changes: 11 additions & 0 deletions purchase_sign/models/res_company.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Copyright 2024 Onestein
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models


class ResCompany(models.Model):
_inherit = "res.company"

purchase_portal_confirmation_sign = fields.Boolean(
string="Purchase Online Signature", default=False
)
12 changes: 12 additions & 0 deletions purchase_sign/models/res_config_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Copyright 2024 Onestein
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models


class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"

purchase_portal_confirmation_sign = fields.Boolean(
related="company_id.purchase_portal_confirmation_sign",
readonly=False,
)
1 change: 1 addition & 0 deletions purchase_sign/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* `Onestein <https://www.onestein.nl>`__
1 change: 1 addition & 0 deletions purchase_sign/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This module allows to take online signatures from vendors to confirm purchase orders.It adds a global configuration (Online signature) which can be accessed through Purchase->Configuration->Settings and it can further be handled also on each of the purchase orders.When it is enabled, it shows a button 'Accept & Sign' on the portal to the vendors for RFQ which are sent by email to them.Once the vendor accepts and adds their signature, the RFQ gets confirmed into a purchase order
19 changes: 19 additions & 0 deletions purchase_sign/report/purchase_order_template.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="report_purchaseorder_document" inherit_id="purchase.report_purchaseorder_document">
<xpath expr="//p[@t-field='o.notes']" position="before">
<div t-if="o.signature" class="mt-4 ml64 mr4" name="signature">
<div class="offset-8">
<strong>Signature</strong>
</div>
<div class="offset-8">
<img t-att-src="image_data_uri(o.signature)" style="max-height: 4cm; max-width: 8cm;"/>
</div>
<div class="offset-8 text-center">
<p t-field="o.signed_by"/>
</div>
</div>
</xpath>
</template>

</odoo>
49 changes: 49 additions & 0 deletions purchase_sign/static/tests/tours/purchase_signature.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/** @odoo-module **/

import tour from "web_tour.tour";

// This tour relies on data created on the Python test.
tour.register("purchase_signature", {
test: true,
url: "/my/rfq",
},
[
{
content: "open the test PO",
trigger: "a:containsExact('test PO')",
},
{
content: "click sign",
trigger: "a:contains('Sign')",
},
{
content: "check submit is enabled",
trigger: ".o_portal_sign_submit:enabled",
run: function () {},
},
{
content: "click select style",
trigger: ".o_web_sign_auto_select_style a",
},
{
content: "click style 4",
trigger: ".o_web_sign_auto_font_selection a:eq(3)",
},
{
content: "click submit",
trigger: ".o_portal_sign_submit:enabled",
},
{
content: "check it's confirmed",
trigger: "#quote_content:contains('Thank You')",
}, {
trigger: "#quote_content",
run: function () {
window.location.href = window.location.origin + "/web";
}, // Avoid race condition at the end of the tour by returning to the home page.
},
{
trigger: "nav",
run: function() {},
}
]);
89 changes: 89 additions & 0 deletions purchase_sign/templates/purchase_portal_templates.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>

<template id="portal_my_purchase_order" name="Purchase Order Portal Template"
inherit_id="purchase.portal_my_purchase_order">
<xpath expr="//div[hasclass('o_download_pdf')]" position="before">
<a t-if="order._has_to_be_signed()" role="button" class="btn btn-primary mb8"
data-bs-toggle="modal" data-bs-target="#modalaccept" href="#" style="width:100%">
<i class="fa fa-check"/>
Accept &amp; Sign
</a>
</xpath>

<xpath expr="//div[@id='portal_purchase_content']" position="before">
<!-- modal relative to the action sign-->
<div role="dialog" class="modal fade" id="modalaccept">
<div class="modal-dialog" t-if="order._has_to_be_signed()">
<form id="accept" method="POST" t-att-data-order-id="order.id" t-att-data-token="order.access_token" class="js_accept_json modal-content js_website_submit_form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<header class="modal-header">
<h4 class="modal-title">Validate Order</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</header>
<main class="modal-body" id="sign-dialog">
<p>
<span>By signing this proposal, I agree to the following terms:</span>
<ul>
<li>
<span>Accepted on the behalf of:</span> <b t-field="order.partner_id.commercial_partner_id"/>
</li>
<li>
<span>For an amount of:</span> <b data-id="amount_total" t-field="order.amount_total"/>
</li>
<li t-if="order.payment_term_id">
<span>With payment terms:</span> <b t-field="order.payment_term_id.note"/>
</li>
</ul>
</p>
<t t-call="portal.signature_form">
<t t-set="call_url" t-value="order.get_portal_url(suffix='/accept')"/>
<t t-set="default_name" t-value="order.partner_id.name"/>
</t>
</main>
</form>
</div>
</div>

<!-- status messages -->
<div t-if="message == 'sign_ok'" class="alert alert-success alert-dismissible d-print-none" role="status">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<strong>Thank You!</strong><br/>
Order has been confirmed.
</div>
</xpath>

<xpath expr="//div[@id='portal_purchase_content']" position="after">
<div t-if="order._has_to_be_signed()" class="row justify-content-center text-center d-print-none pt-1 pb-4">
<div class="col-sm-auto mt8">
<a role="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalaccept"
href="#">
<i class="fa fa-check"/>
Accept &amp; Sign
</a>
</div>
<div class="col-sm-auto mt8">
<a role="button" class="btn btn-secondary" href="#discussion">
<i class="fa fa-comment"/>
Feedback
</a>
</div>
</div>
</xpath>
</template>

<template id="purchase_order_portal_content" inherit_id="purchase.purchase_order_portal_content">
<xpath expr="//section[@id='terms']" position="before">
<section t-if="order.signature" id="signature" name="Signature">
<div class="row mt-4" name="signature">
<div t-attf-class="#{'col-3' if report_type != 'html' else 'col-sm-7 col-md-4'} ms-auto text-center">
<h5>Signature</h5>
<img t-att-src="image_data_uri(order.signature)" style="max-height: 6rem; max-width: 100%;"/>
<p t-field="order.signed_by"/>
</div>
</div>
</section>
</xpath>
</template>

</odoo>
2 changes: 2 additions & 0 deletions purchase_sign/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
from . import test_purchase_sign
59 changes: 59 additions & 0 deletions purchase_sign/tests/test_purchase_sign.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Copyright 2024 Onestein
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo.tests import tagged

from odoo.addons.base.tests.common import HttpCaseWithUserPortal


@tagged("post_install", "-at_install")
class TestPurchaseSign(HttpCaseWithUserPortal):
def test_01_portal_purchase_signature_tour(self):
"""The goal of this test is to make sure the portal user can sign PO."""
self.user_portal.company_id.purchase_portal_confirmation_sign = True
portal_user_partner = self.partner_portal
# create a PO to be signed
purchase_order = self.env["purchase.order"].create(
{
"name": "test PO",
"partner_id": portal_user_partner.id,
"state": "sent",
}
)
self.env["purchase.order.line"].create(
{
"order_id": purchase_order.id,
"product_id": self.env["product.product"]
.create({"name": "A product"})
.id,
}
)

# must be sent to the user so he can see it
email_act = purchase_order.action_rfq_send()
email_ctx = email_act.get("context", {})
purchase_order.with_context(**email_ctx).message_post_with_template(
email_ctx.get("default_template_id")
)

self.start_tour("/", "purchase_signature", login="portal")

def test_02_purchase_has_to_be_signed(self):
"""The goal of this test is to check whether PO needs signature if Online signature is turned off for the company."""
self.user_portal.company_id.purchase_portal_confirmation_sign = False
portal_user_partner = self.partner_portal
purchase_order = self.env["purchase.order"].create(
{
"name": "test PO",
"partner_id": portal_user_partner.id,
"state": "sent",
}
)
self.env["purchase.order.line"].create(
{
"order_id": purchase_order.id,
"product_id": self.env["product.product"]
.create({"name": "A product"})
.id,
}
)
self.assertFalse(purchase_order._has_to_be_signed())
Loading

0 comments on commit 7b730bb

Please sign in to comment.