Initial commit: OCA Workflow Process packages (456 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:00 +02:00
commit d366e42934
18799 changed files with 1284507 additions and 0 deletions

View file

@ -0,0 +1,164 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association
================
Purchase Request
================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:5b19c693955170a23b931af36c7918760373e06299b80106eaa28e8c188eafbd
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpurchase--workflow-lightgray.png?logo=github
:target: https://github.com/OCA/purchase-workflow/tree/16.0/purchase_request
:alt: OCA/purchase-workflow
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/purchase-workflow-16-0/purchase-workflow-16-0-purchase_request
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/purchase-workflow&target_branch=16.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
You use this module if you wish to give notification of requirements of
materials and/or external services and keep track of such requirements.
Requests can be created either directly or indirectly. "Directly" means
that someone from the requesting department enters a purchase request
manually.
The person creating the request determines what and how much to order,
and the requested date.
"Indirectly" means that the purchase request initiated by the
application automatically, for example, from procurement orders (MO,
SO).
A purchase request is an instruction to Purchasing to procure a certain
quantity of materials services, so that they are available at a certain
point in time.
A line of a request contains the quantity and requested date of the
material to be supplied or the quantity of the service to be performed.
You can indicate the service specifications if needed.
Once request is approved go to the Purchase Request Lines from the menu
entry 'Purchase Requests', and also from the 'Purchase' menu.
Select the lines that you wish to initiate the RFQ for, then go to
'More' and press 'Create RFQ'.
You can choose to select an existing RFQ or create a new one. In the
later, you have to choose a supplier.
In case that you chose to select an existing RFQ, the application will
search for existing lines matching the request line, and will add the
extra quantity to them, recalculating the minimum order quantity, if it
exists for the supplier of that RFQ.
In case that you create a new RFQ, the request lines will also be
consolidated into as few as possible lines in the RFQ.
**Table of contents**
.. contents::
:local:
Configuration
=============
To configure the product follow this steps:
1. Go to a product form.
2. Go to *Inventory* tab.
3. Check the box *Purchase Request* along with the route *Buy*.
With this configuration, whenever a procurement order is created and the
supply rule selected is 'Buy' the application will create a Purchase
Request instead of a Purchase Order.
Usage
=====
Purchase requests are accessible through a new menu entry 'Purchase
Requests', and also from the 'Purchase' menu.
Users can access the list of Purchase Requests or Purchase Request
Lines.
It is possible to filter requests by its approval status.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/purchase-workflow/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/purchase-workflow/issues/new?body=module:%20purchase_request%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
-------
* ForgeFlow
Contributors
------------
- Jordi Ballester Alomar <jordi.ballester@forgeflow.com>
- Jonathan Nemry <jonathan.nemry@acsone.eu>
- Aaron Henriquez <ahenriquez@forgeflow.com>
- Adrien Peiffer <adrien.peiffer@acsone.eu>
- Lois Rilo <lois.rilo@forgeflow.com>
- Héctor Villarreal <hector.villarreal@forgeflow.com>
- Ben Cai <ben.cai@elico-corp.com>
- Rattapong Chokmasermkul <rattapongc@ecosoft.co.th>
- Stefan Rijnhart <stefan@opener.amsterdam>
Other credits
-------------
The development of this module has been financially supported by:
|Aleph Objects, Inc|
Images
~~~~~~
- Enric Tobella (logo)
.. |Aleph Objects, Inc| image:: https://upload.wikimedia.org/wikipedia/en/3/3b/Aleph_Objects_Logo.png
:target: https://www.alephobjects.com
Maintainers
-----------
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/purchase-workflow <https://github.com/OCA/purchase-workflow/tree/16.0/purchase_request>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View file

@ -0,0 +1,2 @@
from . import models
from . import wizard

View file

@ -0,0 +1,33 @@
# Copyright 2018-2019 ForgeFlow, S.L.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl-3.0).
{
"name": "Purchase Request",
"author": "ForgeFlow, Odoo Community Association (OCA)",
"version": "16.0.2.4.0",
"summary": "Use this module to have notification of requirements of "
"materials and/or external services and keep track of such "
"requirements.",
"website": "https://github.com/OCA/purchase-workflow",
"category": "Purchase Management",
"depends": ["purchase_stock"],
"data": [
"security/purchase_request.xml",
"security/ir.model.access.csv",
"data/purchase_request_sequence.xml",
"data/purchase_request_data.xml",
"reports/report_purchase_request.xml",
"wizard/purchase_request_line_make_purchase_order_view.xml",
"views/purchase_request_view.xml",
"views/purchase_request_line_view.xml",
"views/purchase_request_report.xml",
"views/product_template.xml",
"views/purchase_order_view.xml",
"views/stock_move_views.xml",
"views/stock_picking_views.xml",
],
"demo": ["demo/purchase_request_demo.xml"],
"license": "LGPL-3",
"installable": True,
"application": True,
}

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2018-2019 ForgeFlow, S.L.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl-3.0) -->
<odoo noupdate="1">
<!-- Request-related subtypes for messaging / Chatter -->
<record id="mt_request_to_approve" model="mail.message.subtype">
<field name="name">Purchase Request to be approved</field>
<field name="res_model">purchase.request</field>
<field name="default" eval="True" />
<field name="description">Purchase Request to be approved</field>
</record>
<record id="mt_request_approved" model="mail.message.subtype">
<field name="name">Purchase Request approved</field>
<field name="res_model">purchase.request</field>
<field name="default" eval="True" />
<field name="description">Purchase Request approved</field>
</record>
<record id="mt_request_rejected" model="mail.message.subtype">
<field name="name">Purchase Request rejected</field>
<field name="res_model">purchase.request</field>
<field name="default" eval="True" />
<field name="description">Purchase Request rejected</field>
</record>
<record id="mt_request_done" model="mail.message.subtype">
<field name="name">Purchase Request done</field>
<field name="res_model">purchase.request</field>
<field name="default" eval="True" />
<field name="description">Purchase Request is done</field>
</record>
<record id="mt_request_po_confirmed" model="mail.message.subtype">
<field name="name">Purchase Order confirmation</field>
<field name="res_model">purchase.request</field>
<field name="default" eval="True" />
<field name="internal" eval="True" />
<field name="description">Purchase Order is confirmed</field>
</record>
<record id="mt_request_picking_done" model="mail.message.subtype">
<field name="name">Purchase receipt confirmation</field>
<field name="res_model">purchase.request</field>
<field name="default" eval="True" />
<field name="internal" eval="True" />
<field name="description">Receipt is done</field>
</record>
</odoo>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2018-2019 ForgeFlow, S.L.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl-3.0) -->
<odoo noupdate="1">
<record id="seq_purchase_request" model="ir.sequence">
<field name="name">Purchase Request</field>
<field name="code">purchase.request</field>
<field name="prefix">PR</field>
<field name="padding">5</field>
<field name="company_id" />
</record>
</odoo>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2018-2019 ForgeFlow, S.L.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl-3.0) -->
<odoo noupdate="1">
<record id="base.user_demo" model="res.users">
<field eval="[(4, ref('group_purchase_request_user'))]" name="groups_id" />
</record>
<record id="base.user_admin" model="res.users">
<field eval="[(4, ref('group_purchase_request_manager'))]" name="groups_id" />
</record>
</odoo>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,17 @@
# Copyright 2025 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openupgradelib import openupgrade
_noupdate_xmlids = [
"mt_request_to_approve",
"mt_request_approved",
"mt_request_rejected",
"mt_request_done",
]
@openupgrade.migrate()
def migrate(env, version):
openupgrade.set_xml_ids_noupdate_value(
env, "purchase_request", _noupdate_xmlids, True
)

View file

@ -0,0 +1,11 @@
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
from . import purchase_request_allocation
from . import orderpoint
from . import purchase_request
from . import purchase_request_line
from . import stock_rule
from . import product_template
from . import purchase_order
from . import stock_move
from . import stock_move_line

View file

@ -0,0 +1,26 @@
# Copyright 2018-2019 ForgeFlow, S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
from odoo import models
class Orderpoint(models.Model):
_inherit = "stock.warehouse.orderpoint"
def _quantity_in_progress(self):
res = super(Orderpoint, self)._quantity_in_progress()
for prline in self.env["purchase.request.line"].search(
[
(
"request_id.state",
"in",
("draft", "approved", "to_approve", "in_progress"),
),
("orderpoint_id", "in", self.ids),
("purchase_state", "=", False),
]
):
res[prline.orderpoint_id.id] += prline.product_uom_id._compute_quantity(
prline.product_qty, prline.orderpoint_id.product_uom, round=False
)
return res

View file

@ -0,0 +1,14 @@
# Copyright 2018-2019 ForgeFlow, S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
from odoo import fields, models
class ProductTemplate(models.Model):
_inherit = "product.template"
purchase_request = fields.Boolean(
help="Check this box to generate Purchase Request instead of "
"generating Requests For Quotation from procurement.",
company_dependent=True,
)

View file

@ -0,0 +1,240 @@
# Copyright 2018-2019 ForgeFlow, S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
from odoo import _, api, exceptions, fields, models
class PurchaseOrder(models.Model):
_inherit = "purchase.order"
def _purchase_request_confirm_message_content(self, request, request_dict=None):
self.ensure_one()
if not request_dict:
request_dict = {}
title = _("Order confirmation %(po_name)s for your Request %(pr_name)s") % {
"po_name": self.name,
"pr_name": request.name,
}
message = "<h3>%s</h3><ul>" % title
message += _(
"The following requested items from Purchase Request %(pr_name)s "
"have now been confirmed in Purchase Order %(po_name)s:"
) % {
"po_name": self.name,
"pr_name": request.name,
}
for line in request_dict.values():
message += _(
"<li><b>%(prl_name)s</b>: Ordered quantity %(prl_qty)s %(prl_uom)s, "
"Planned date %(prl_date_planned)s</li>"
) % {
"prl_name": line["name"],
"prl_qty": line["product_qty"],
"prl_uom": line["product_uom"],
"prl_date_planned": line["date_planned"],
}
message += "</ul>"
return message
def _purchase_request_confirm_message(self):
request_obj = self.env["purchase.request"]
for po in self:
requests_dict = {}
for line in po.order_line:
for request_line in line.sudo().purchase_request_lines:
request_id = request_line.request_id.id
if request_id not in requests_dict:
requests_dict[request_id] = {}
date_planned = "%s" % line.date_planned
data = {
"name": request_line.name,
"product_qty": line.product_qty,
"product_uom": line.product_uom.name,
"date_planned": date_planned,
}
requests_dict[request_id][request_line.id] = data
for request_id in requests_dict:
request = request_obj.sudo().browse(request_id)
message = po._purchase_request_confirm_message_content(
request, requests_dict[request_id]
)
request.message_post(
body=message,
subtype_id=self.env.ref(
"purchase_request.mt_request_po_confirmed"
).id,
)
return True
def _purchase_request_line_check(self):
for po in self:
for line in po.order_line:
for request_line in line.purchase_request_lines:
if request_line.sudo().purchase_state == "done":
raise exceptions.UserError(
_("Purchase Request %s has already been completed")
% (request_line.request_id.name)
)
return True
def button_confirm(self):
self._purchase_request_line_check()
res = super(PurchaseOrder, self).button_confirm()
self._purchase_request_confirm_message()
return res
def unlink(self):
alloc_to_unlink = self.env["purchase.request.allocation"]
for rec in self:
for alloc in (
rec.order_line.mapped("purchase_request_lines")
.mapped("purchase_request_allocation_ids")
.filtered(
lambda alloc, rec=rec: alloc.purchase_line_id.order_id.id == rec.id
)
):
alloc_to_unlink += alloc
res = super().unlink()
alloc_to_unlink.unlink()
return res
class PurchaseOrderLine(models.Model):
_inherit = "purchase.order.line"
purchase_request_lines = fields.Many2many(
comodel_name="purchase.request.line",
relation="purchase_request_purchase_order_line_rel",
column1="purchase_order_line_id",
column2="purchase_request_line_id",
readonly=True,
copy=False,
)
purchase_request_allocation_ids = fields.One2many(
comodel_name="purchase.request.allocation",
inverse_name="purchase_line_id",
string="Purchase Request Allocation",
copy=False,
)
def action_open_request_line_tree_view(self):
"""
:return dict: dictionary value for created view
"""
request_line_ids = []
for line in self:
request_line_ids += line.purchase_request_lines.ids
domain = [("id", "in", request_line_ids)]
return {
"name": _("Purchase Request Lines"),
"type": "ir.actions.act_window",
"res_model": "purchase.request.line",
"view_mode": "tree,form",
"domain": domain,
}
def _prepare_stock_moves(self, picking):
self.ensure_one()
val = super(PurchaseOrderLine, self)._prepare_stock_moves(picking)
all_list = []
for v in val:
all_ids = self.env["purchase.request.allocation"].search(
[("purchase_line_id", "=", v["purchase_line_id"])]
)
for all_id in all_ids:
all_list.append((4, all_id.id))
v["purchase_request_allocation_ids"] = all_list
return val
def update_service_allocations(self, prev_qty_received):
for rec in self:
allocation = self.env["purchase.request.allocation"].search(
[
("purchase_line_id", "=", rec.id),
("purchase_line_id.product_id.type", "=", "service"),
]
)
if not allocation:
return
qty_left = rec.qty_received - prev_qty_received
for alloc in allocation:
allocated_product_qty = alloc.allocated_product_qty
if not qty_left:
alloc.purchase_request_line_id._compute_qty()
break
if alloc.open_product_qty <= qty_left:
allocated_product_qty += alloc.open_product_qty
qty_left -= alloc.open_product_qty
alloc._notify_allocation(alloc.open_product_qty)
else:
allocated_product_qty += qty_left
alloc._notify_allocation(qty_left)
qty_left = 0
alloc.write({"allocated_product_qty": allocated_product_qty})
message_data = self._prepare_request_message_data(
alloc, alloc.purchase_request_line_id, allocated_product_qty
)
message = self._purchase_request_confirm_done_message_content(
message_data
)
if message:
alloc.purchase_request_line_id.request_id.message_post(
body=message, subtype_id=self.env.ref("mail.mt_note").id
)
alloc.purchase_request_line_id._compute_qty()
return True
@api.model
def _purchase_request_confirm_done_message_content(self, message_data):
title = _("Service confirmation for Request %s") % (
message_data["request_name"]
)
message = "<h3>%s</h3>" % title
message += _(
"The following requested services from Purchase"
" Request %(request_name)s requested by %(requestor)s "
"have now been received:"
) % {
"request_name": message_data["request_name"],
"requestor": message_data["requestor"],
}
message += "<ul>"
message += _(
"<li><b>%(product_name)s</b>: "
"Received quantity %(product_qty)s %(product_uom)s</li>"
) % {
"product_name": message_data["product_name"],
"product_qty": message_data["product_qty"],
"product_uom": message_data["product_uom"],
}
message += "</ul>"
return message
def _prepare_request_message_data(self, alloc, request_line, allocated_qty):
return {
"request_name": request_line.request_id.name,
"product_name": request_line.product_id.name_get()[0][1],
"product_qty": allocated_qty,
"product_uom": alloc.product_uom_id.name,
"requestor": request_line.request_id.requested_by.partner_id.name,
}
def write(self, vals):
# As services do not generate stock move this tweak is required
# to allocate them.
prev_qty_received = {}
if vals.get("qty_received", False):
service_lines = self.filtered(lambda l: l.product_id.type == "service")
for line in service_lines:
prev_qty_received[line.id] = line.qty_received
res = super(PurchaseOrderLine, self).write(vals)
if prev_qty_received:
for line in service_lines:
line.update_service_allocations(prev_qty_received[line.id])
return res

View file

@ -0,0 +1,317 @@
# Copyright 2018-2019 ForgeFlow, S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
from odoo import _, api, fields, models
from odoo.exceptions import UserError
_STATES = [
("draft", "Draft"),
("to_approve", "To be approved"),
("approved", "Approved"),
("in_progress", "In progress"),
("done", "Done"),
("rejected", "Rejected"),
]
class PurchaseRequest(models.Model):
_name = "purchase.request"
_description = "Purchase Request"
_mail_post_access = "read"
_inherit = ["mail.thread", "mail.activity.mixin"]
_order = "id desc"
@api.model
def _company_get(self):
return self.env["res.company"].browse(self.env.company.id)
@api.model
def _get_default_requested_by(self):
return self.env["res.users"].browse(self.env.uid)
@api.model
def _get_default_name(self):
return self.env["ir.sequence"].next_by_code("purchase.request")
@api.model
def _default_picking_type(self):
type_obj = self.env["stock.picking.type"]
company_id = self.env.context.get("company_id") or self.env.company.id
types = type_obj.search(
[("code", "=", "incoming"), ("warehouse_id.company_id", "=", company_id)]
)
if not types:
types = type_obj.search(
[("code", "=", "incoming"), ("warehouse_id", "=", False)]
)
return types[:1]
@api.depends("state")
def _compute_is_editable(self):
for rec in self:
if rec.state in (
"to_approve",
"approved",
"rejected",
"in_progress",
"done",
):
rec.is_editable = False
else:
rec.is_editable = True
name = fields.Char(
string="Request Reference",
required=True,
default=lambda self: _("New"),
tracking=True,
)
is_name_editable = fields.Boolean(
default=lambda self: self.env.user.has_group("base.group_no_one"),
)
origin = fields.Char(string="Source Document")
date_start = fields.Date(
string="Creation date",
help="Date when the user initiated the request.",
default=fields.Date.context_today,
tracking=True,
)
requested_by = fields.Many2one(
comodel_name="res.users",
required=True,
copy=False,
tracking=True,
default=_get_default_requested_by,
index=True,
)
assigned_to = fields.Many2one(
comodel_name="res.users",
string="Approver",
tracking=True,
domain=lambda self: [
(
"groups_id",
"in",
self.env.ref("purchase_request.group_purchase_request_manager").id,
)
],
index=True,
)
description = fields.Text()
company_id = fields.Many2one(
comodel_name="res.company",
required=False,
default=_company_get,
tracking=True,
)
line_ids = fields.One2many(
comodel_name="purchase.request.line",
inverse_name="request_id",
string="Products to Purchase",
readonly=True,
copy=True,
tracking=True,
states={"draft": [("readonly", False)]},
)
product_id = fields.Many2one(
comodel_name="product.product",
related="line_ids.product_id",
string="Product",
readonly=True,
)
state = fields.Selection(
selection=_STATES,
string="Status",
index=True,
tracking=True,
required=True,
copy=False,
default="draft",
)
is_editable = fields.Boolean(compute="_compute_is_editable", readonly=True)
to_approve_allowed = fields.Boolean(compute="_compute_to_approve_allowed")
picking_type_id = fields.Many2one(
comodel_name="stock.picking.type",
string="Picking Type",
required=True,
default=_default_picking_type,
)
group_id = fields.Many2one(
comodel_name="procurement.group",
string="Procurement Group",
copy=False,
index=True,
)
line_count = fields.Integer(
string="Purchase Request Line count",
compute="_compute_line_count",
readonly=True,
)
move_count = fields.Integer(
string="Stock Move count", compute="_compute_move_count", readonly=True
)
purchase_count = fields.Integer(
string="Purchases count", compute="_compute_purchase_count", readonly=True
)
currency_id = fields.Many2one(related="company_id.currency_id", readonly=True)
estimated_cost = fields.Monetary(
compute="_compute_estimated_cost",
string="Total Estimated Cost",
store=True,
)
@api.depends("line_ids", "line_ids.estimated_cost")
def _compute_estimated_cost(self):
for rec in self:
rec.estimated_cost = sum(rec.line_ids.mapped("estimated_cost"))
@api.depends("line_ids")
def _compute_purchase_count(self):
for rec in self:
rec.purchase_count = len(rec.mapped("line_ids.purchase_lines.order_id"))
def action_view_purchase_order(self):
action = self.env["ir.actions.actions"]._for_xml_id("purchase.purchase_rfq")
lines = self.mapped("line_ids.purchase_lines.order_id")
if len(lines) > 1:
action["domain"] = [("id", "in", lines.ids)]
elif lines:
action["views"] = [
(self.env.ref("purchase.purchase_order_form").id, "form")
]
action["res_id"] = lines.id
return action
@api.depends("line_ids")
def _compute_move_count(self):
for rec in self:
rec.move_count = len(
rec.mapped("line_ids.purchase_request_allocation_ids.stock_move_id")
)
def action_view_stock_picking(self):
action = self.env["ir.actions.actions"]._for_xml_id(
"stock.action_picking_tree_all"
)
# remove default filters
action["context"] = {}
lines = self.mapped(
"line_ids.purchase_request_allocation_ids.stock_move_id.picking_id"
)
if len(lines) > 1:
action["domain"] = [("id", "in", lines.ids)]
elif lines:
action["views"] = [(self.env.ref("stock.view_picking_form").id, "form")]
action["res_id"] = lines.id
return action
@api.depends("line_ids")
def _compute_line_count(self):
for rec in self:
rec.line_count = len(rec.mapped("line_ids"))
def action_view_purchase_request_line(self):
action = (
self.env.ref("purchase_request.purchase_request_line_form_action")
.sudo()
.read()[0]
)
lines = self.mapped("line_ids")
if len(lines) > 1:
action["domain"] = [("id", "in", lines.ids)]
elif lines:
action["views"] = [
(self.env.ref("purchase_request.purchase_request_line_form").id, "form")
]
action["res_id"] = lines.ids[0]
return action
@api.depends("state", "line_ids.product_qty", "line_ids.cancelled")
def _compute_to_approve_allowed(self):
for rec in self:
rec.to_approve_allowed = rec.state == "draft" and any(
not line.cancelled and line.product_qty for line in rec.line_ids
)
def copy(self, default=None):
default = dict(default or {})
self.ensure_one()
default.update({"state": "draft", "name": self._get_default_name()})
return super(PurchaseRequest, self).copy(default)
@api.model
def _get_partner_id(self, request):
user_id = request.assigned_to or self.env.user
return user_id.partner_id.id
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get("name", _("New")) == _("New"):
vals["name"] = self._get_default_name()
requests = super(PurchaseRequest, self).create(vals_list)
for vals, request in zip(vals_list, requests):
if vals.get("assigned_to"):
partner_id = self._get_partner_id(request)
request.message_subscribe(partner_ids=[partner_id])
return requests
def write(self, vals):
res = super(PurchaseRequest, self).write(vals)
for request in self:
if vals.get("assigned_to"):
partner_id = self._get_partner_id(request)
request.message_subscribe(partner_ids=[partner_id])
return res
def _can_be_deleted(self):
self.ensure_one()
return self.state == "draft"
def unlink(self):
for request in self:
if not request._can_be_deleted():
raise UserError(
_("You cannot delete a purchase request which is not draft.")
)
return super(PurchaseRequest, self).unlink()
def button_draft(self):
self.mapped("line_ids").do_uncancel()
return self.write({"state": "draft"})
def button_to_approve(self):
self.to_approve_allowed_check()
return self.write({"state": "to_approve"})
def button_approved(self):
return self.write({"state": "approved"})
def button_rejected(self):
self.mapped("line_ids").do_cancel()
return self.write({"state": "rejected"})
def button_in_progress(self):
return self.write({"state": "in_progress"})
def button_done(self):
return self.write({"state": "done"})
def check_auto_reject(self):
"""When all lines are cancelled the purchase request should be
auto-rejected."""
for pr in self:
if not pr.line_ids.filtered(lambda l: l.cancelled is False):
pr.write({"state": "rejected"})
def to_approve_allowed_check(self):
for rec in self:
if not rec.to_approve_allowed:
raise UserError(
_(
"You can't request an approval for a purchase request "
"which is empty. (%s)"
)
% rec.name
)

View file

@ -0,0 +1,132 @@
# Copyright 2019 ForgeFlow, S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
from odoo import _, api, fields, models
class PurchaseRequestAllocation(models.Model):
_name = "purchase.request.allocation"
_description = "Purchase Request Allocation"
purchase_request_line_id = fields.Many2one(
string="Purchase Request Line",
comodel_name="purchase.request.line",
required=True,
ondelete="cascade",
copy=True,
index=True,
)
company_id = fields.Many2one(
string="Company",
comodel_name="res.company",
readonly=True,
related="purchase_request_line_id.request_id.company_id",
store=True,
index=True,
)
stock_move_id = fields.Many2one(
string="Stock Move",
comodel_name="stock.move",
ondelete="cascade",
index=True,
)
purchase_line_id = fields.Many2one(
string="Purchase Line",
comodel_name="purchase.order.line",
copy=True,
ondelete="cascade",
help="Service Purchase Order Line",
index=True,
)
product_id = fields.Many2one(
string="Product",
comodel_name="product.product",
related="purchase_request_line_id.product_id",
readonly=True,
)
product_uom_id = fields.Many2one(
string="UoM",
comodel_name="uom.uom",
related="purchase_request_line_id.product_uom_id",
readonly=True,
required=True,
)
requested_product_uom_qty = fields.Float(
string="Requested Quantity",
help="Quantity of the purchase request line allocated to the"
"stock move, in the UoM of the Purchase Request Line",
)
allocated_product_qty = fields.Float(
string="Allocated Quantity",
copy=False,
help="Quantity of the purchase request line allocated to the stock"
"move, in the default UoM of the product",
)
open_product_qty = fields.Float(
string="Open Quantity", compute="_compute_open_product_qty"
)
purchase_state = fields.Selection(related="purchase_line_id.state")
@api.depends(
"requested_product_uom_qty",
"allocated_product_qty",
"stock_move_id",
"stock_move_id.state",
"stock_move_id.product_uom_qty",
"stock_move_id.move_line_ids.qty_done",
"purchase_line_id",
"purchase_line_id.qty_received",
"purchase_state",
)
def _compute_open_product_qty(self):
for rec in self:
if rec.purchase_state in ["cancel", "done"]:
rec.open_product_qty = 0.0
else:
rec.open_product_qty = (
rec.requested_product_uom_qty - rec.allocated_product_qty
)
if rec.open_product_qty < 0.0:
rec.open_product_qty = 0.0
@api.model
def _purchase_request_confirm_done_message_content(self, message_data):
message = ""
message += _(
"From last reception this quantity has been "
"allocated to this purchase request"
)
message += "<ul>"
message += _(
"<li><b>%(product_name)s</b>: "
"Received quantity %(product_qty)s %(product_uom)s</li>"
) % {
"product_name": message_data["product_name"],
"product_qty": message_data["product_qty"],
"product_uom": message_data["product_uom"],
}
message += "</ul>"
return message
def _prepare_message_data(self, po_line, request, allocated_qty):
return {
"request_name": request.name,
"po_name": po_line.order_id.name,
"product_name": po_line.product_id.name_get()[0][1],
"product_qty": allocated_qty,
"product_uom": po_line.product_uom.name,
}
def _notify_allocation(self, allocated_qty):
if not allocated_qty:
return
for allocation in self:
request = allocation.purchase_request_line_id.request_id
po_line = allocation.purchase_line_id
message_data = self._prepare_message_data(po_line, request, allocated_qty)
message = self._purchase_request_confirm_done_message_content(message_data)
request.message_post(
body=message, subtype_id=self.env.ref("mail.mt_note").id
)

View file

@ -0,0 +1,433 @@
# Copyright 2018-2019 ForgeFlow, S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
from odoo import _, api, fields, models
from odoo.exceptions import UserError
_STATES = [
("draft", "Draft"),
("to_approve", "To be approved"),
("approved", "Approved"),
("in_progress", "In progress"),
("done", "Done"),
("rejected", "Rejected"),
]
class PurchaseRequestLine(models.Model):
_name = "purchase.request.line"
_description = "Purchase Request Line"
_inherit = ["mail.thread", "mail.activity.mixin", "analytic.mixin"]
_order = "id desc"
name = fields.Char(string="Description", tracking=True)
product_uom_id = fields.Many2one(
comodel_name="uom.uom",
string="UoM",
tracking=True,
domain="[('category_id', '=', product_uom_category_id)]",
)
product_uom_category_id = fields.Many2one(related="product_id.uom_id.category_id")
product_qty = fields.Float(
string="Quantity", tracking=True, digits="Product Unit of Measure"
)
request_id = fields.Many2one(
comodel_name="purchase.request",
string="Purchase Request",
ondelete="cascade",
readonly=True,
index=True,
auto_join=True,
)
company_id = fields.Many2one(
comodel_name="res.company",
related="request_id.company_id",
string="Company",
store=True,
index=True,
)
requested_by = fields.Many2one(
comodel_name="res.users",
related="request_id.requested_by",
string="Requested by",
store=True,
)
assigned_to = fields.Many2one(
comodel_name="res.users",
related="request_id.assigned_to",
string="Assigned to",
store=True,
)
date_start = fields.Date(related="request_id.date_start", store=True)
description = fields.Text(
related="request_id.description",
string="PR Description",
store=True,
readonly=False,
)
origin = fields.Char(
related="request_id.origin", string="Source Document", store=True
)
date_required = fields.Date(
string="Request Date",
required=True,
tracking=True,
default=fields.Date.context_today,
)
is_editable = fields.Boolean(compute="_compute_is_editable", readonly=True)
specifications = fields.Text()
request_state = fields.Selection(
string="Request state",
related="request_id.state",
store=True,
)
supplier_id = fields.Many2one(
comodel_name="res.partner",
string="Preferred supplier",
compute="_compute_supplier_id",
compute_sudo=True,
store=True,
)
cancelled = fields.Boolean(readonly=True, default=False, copy=False)
purchased_qty = fields.Float(
string="RFQ/PO Qty",
digits="Product Unit of Measure",
compute="_compute_purchased_qty",
)
purchase_lines = fields.Many2many(
comodel_name="purchase.order.line",
relation="purchase_request_purchase_order_line_rel",
column1="purchase_request_line_id",
column2="purchase_order_line_id",
string="Purchase Order Lines",
readonly=True,
copy=False,
)
purchase_state = fields.Selection(
compute="_compute_purchase_state",
string="Purchase Status",
selection=lambda self: self.env["purchase.order"]
._fields["state"]
._description_selection(self.env),
store=True,
)
move_dest_ids = fields.One2many(
comodel_name="stock.move",
inverse_name="created_purchase_request_line_id",
string="Downstream Moves",
)
orderpoint_id = fields.Many2one(
comodel_name="stock.warehouse.orderpoint", string="Orderpoint"
)
purchase_request_allocation_ids = fields.One2many(
comodel_name="purchase.request.allocation",
inverse_name="purchase_request_line_id",
string="Purchase Request Allocation",
)
qty_in_progress = fields.Float(
digits="Product Unit of Measure",
readonly=True,
compute="_compute_qty",
store=True,
help="Quantity in progress.",
)
qty_done = fields.Float(
digits="Product Unit of Measure",
readonly=True,
compute="_compute_qty",
store=True,
help="Quantity completed",
)
qty_cancelled = fields.Float(
digits="Product Unit of Measure",
readonly=True,
compute="_compute_qty_cancelled",
store=True,
help="Quantity cancelled",
)
qty_to_buy = fields.Boolean(
compute="_compute_qty_to_buy",
string="There is some pending qty to buy",
store=True,
)
pending_qty_to_receive = fields.Float(
compute="_compute_qty_to_buy",
digits="Product Unit of Measure",
copy=False,
string="Pending Qty to Receive",
store=True,
)
estimated_cost = fields.Monetary(
currency_field="currency_id",
default=0.0,
help="Estimated cost of Purchase Request Line, not propagated to PO.",
)
currency_id = fields.Many2one(related="company_id.currency_id", readonly=True)
product_id = fields.Many2one(
comodel_name="product.product",
string="Product",
domain=[("purchase_ok", "=", True)],
tracking=True,
)
@api.depends(
"purchase_request_allocation_ids",
"purchase_request_allocation_ids.stock_move_id.state",
"purchase_request_allocation_ids.stock_move_id",
"purchase_request_allocation_ids.purchase_line_id",
"purchase_request_allocation_ids.purchase_line_id.state",
"request_id.state",
"product_qty",
)
def _compute_qty_to_buy(self):
for pr in self:
qty_to_buy = sum(pr.mapped("product_qty")) - sum(pr.mapped("qty_done"))
pr.qty_to_buy = qty_to_buy > 0.0
pr.pending_qty_to_receive = qty_to_buy
@api.depends(
"purchase_request_allocation_ids",
"purchase_request_allocation_ids.stock_move_id.state",
"purchase_request_allocation_ids.stock_move_id",
"purchase_request_allocation_ids.purchase_line_id.state",
"purchase_request_allocation_ids.purchase_line_id",
)
def _compute_qty(self):
for request in self:
done_qty = sum(
request.purchase_request_allocation_ids.mapped("allocated_product_qty")
)
open_qty = sum(
request.purchase_request_allocation_ids.mapped("open_product_qty")
)
request.qty_done = done_qty
request.qty_in_progress = open_qty
@api.depends(
"purchase_request_allocation_ids",
"purchase_request_allocation_ids.stock_move_id.state",
"purchase_request_allocation_ids.stock_move_id",
"purchase_request_allocation_ids.purchase_line_id.order_id.state",
"purchase_request_allocation_ids.purchase_line_id",
)
def _compute_qty_cancelled(self):
for request in self:
if request.product_id.type != "service":
qty_cancelled = sum(
request.mapped("purchase_request_allocation_ids.stock_move_id")
.filtered(lambda sm: sm.state == "cancel")
.mapped("product_qty")
)
else:
qty_cancelled = sum(
request.mapped("purchase_request_allocation_ids.purchase_line_id")
.filtered(lambda sm: sm.state == "cancel")
.mapped("product_qty")
)
# done this way as i cannot track what was received before
# cancelled the purchase order
qty_cancelled -= request.qty_done
if request.product_uom_id:
request.qty_cancelled = (
max(
0,
request.product_id.uom_id._compute_quantity(
qty_cancelled, request.product_uom_id
),
)
if request.purchase_request_allocation_ids
else 0
)
else:
request.qty_cancelled = qty_cancelled
@api.depends(
"purchase_lines",
"request_id.state",
)
def _compute_is_editable(self):
for rec in self:
if rec.request_id.state in (
"to_approve",
"approved",
"rejected",
"in_progress",
"done",
):
rec.is_editable = False
else:
rec.is_editable = True
for rec in self.filtered(lambda p: p.purchase_lines):
rec.is_editable = False
@api.depends("product_id", "product_id.seller_ids")
def _compute_supplier_id(self):
for rec in self:
sellers = rec.product_id.seller_ids.filtered(
lambda si, rec=rec: not si.company_id or si.company_id == rec.company_id
)
rec.supplier_id = sellers[0].partner_id if sellers else False
@api.onchange("product_id")
def onchange_product_id(self):
if self.product_id:
name = self.product_id.name
if self.product_id.code:
name = "[{}] {}".format(self.product_id.code, name)
if self.product_id.description_purchase:
name += "\n" + self.product_id.description_purchase
self.product_uom_id = self.product_id.uom_id.id
self.product_qty = 1
self.name = name
def do_cancel(self):
"""Actions to perform when cancelling a purchase request line."""
self.write({"cancelled": True})
def do_uncancel(self):
"""Actions to perform when uncancelling a purchase request line."""
self.write({"cancelled": False})
def write(self, vals):
res = super(PurchaseRequestLine, self).write(vals)
if vals.get("cancelled"):
requests = self.mapped("request_id")
requests.check_auto_reject()
return res
def _compute_purchased_qty(self):
for rec in self:
rec.purchased_qty = 0.0
for line in rec.purchase_lines.filtered(lambda x: x.state != "cancel"):
if rec.product_uom_id and line.product_uom != rec.product_uom_id:
rec.purchased_qty += line.product_uom._compute_quantity(
line.product_qty, rec.product_uom_id
)
else:
rec.purchased_qty += line.product_qty
@api.depends("purchase_lines.state", "purchase_lines.order_id.state")
def _compute_purchase_state(self):
for rec in self:
temp_purchase_state = False
if rec.purchase_lines:
if any(po_line.state == "done" for po_line in rec.purchase_lines):
temp_purchase_state = "done"
elif all(po_line.state == "cancel" for po_line in rec.purchase_lines):
temp_purchase_state = "cancel"
elif any(po_line.state == "purchase" for po_line in rec.purchase_lines):
temp_purchase_state = "purchase"
elif any(
po_line.state == "to approve" for po_line in rec.purchase_lines
):
temp_purchase_state = "to approve"
elif any(po_line.state == "sent" for po_line in rec.purchase_lines):
temp_purchase_state = "sent"
elif all(
po_line.state in ("draft", "cancel")
for po_line in rec.purchase_lines
):
temp_purchase_state = "draft"
rec.purchase_state = temp_purchase_state
@api.model
def _get_supplier_min_qty(self, product, partner_id=False):
seller_min_qty = 0.0
if partner_id:
seller = product.seller_ids.filtered(
lambda r: r.partner_id == partner_id
).sorted(key=lambda r: r.min_qty)
else:
seller = product.seller_ids.sorted(key=lambda r: r.min_qty)
if seller:
seller_min_qty = seller[0].min_qty
return seller_min_qty
@api.model
def _calc_new_qty(self, request_line, po_line=None, new_pr_line=False):
purchase_uom = po_line.product_uom or request_line.product_id.uom_po_id
# TODO: Not implemented yet.
# Make sure we use the minimum quantity of the partner corresponding
# to the PO. This does not apply in case of dropshipping
supplierinfo_min_qty = 0.0
if not po_line.order_id.dest_address_id:
supplierinfo_min_qty = self._get_supplier_min_qty(
po_line.product_id, po_line.order_id.partner_id
)
rl_qty = 0.0
# Recompute quantity by adding existing running procurements.
if new_pr_line:
rl_qty = po_line.product_uom_qty
else:
for prl in po_line.purchase_request_lines:
for alloc in prl.purchase_request_allocation_ids:
rl_qty += alloc.product_uom_id._compute_quantity(
alloc.requested_product_uom_qty, purchase_uom
)
qty = max(rl_qty, supplierinfo_min_qty)
return qty
def _can_be_deleted(self):
self.ensure_one()
return self.request_state == "draft"
def unlink(self):
if self.mapped("purchase_lines"):
raise UserError(
_("You cannot delete a record that refers to purchase lines!")
)
for line in self:
if not line._can_be_deleted():
raise UserError(
_(
"You can only delete a purchase request line "
"if the purchase request is in draft state."
)
)
return super(PurchaseRequestLine, self).unlink()
def action_show_details(self):
self.ensure_one()
view = self.env.ref("purchase_request.view_purchase_request_line_details")
return {
"name": _("Detailed Line"),
"type": "ir.actions.act_window",
"view_mode": "form",
"res_model": "purchase.request.line",
"views": [(view.id, "form")],
"view_id": view.id,
"target": "new",
"res_id": self.id,
"context": dict(
self.env.context,
),
}
@api.model
def _get_analytic_name(self):
return (
[
"%(name)s (%(value)s)"
% {
"name": self.env["account.analytic.account"]
.browse(int(key))
.display_name,
"value": value,
}
for key, value in self.analytic_distribution.items()
]
if self.analytic_distribution
else [""]
)
@api.model
def _get_analytic_distribution(self):
self.ensure_one()
name = ", ".join(filter(None, self._get_analytic_name()))
return name

View file

@ -0,0 +1,158 @@
# Copyright 2018-2019 ForgeFlow, S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools import float_compare
class StockMove(models.Model):
_inherit = "stock.move"
created_purchase_request_line_id = fields.Many2one(
comodel_name="purchase.request.line",
string="Created Purchase Request Line",
ondelete="set null",
readonly=True,
copy=False,
index=True,
)
purchase_request_allocation_ids = fields.One2many(
comodel_name="purchase.request.allocation",
inverse_name="stock_move_id",
copy=False,
string="Purchase Request Allocation",
)
purchase_request_ids = fields.One2many(
comodel_name="purchase.request",
string="Purchase Requests",
compute="_compute_purchase_request_ids",
)
@api.model
def _prepare_merge_moves_distinct_fields(self):
distinct_fields = super(StockMove, self)._prepare_merge_moves_distinct_fields()
distinct_fields += ["created_purchase_request_line_id"]
return distinct_fields
def _action_cancel_create_mail_activity(self):
"""Create an activity on the request for the cancelled procurement move"""
for move in self:
if move.created_purchase_request_line_id:
try:
activity_type_id = self.env.ref("mail.mail_activity_data_todo").id
except ValueError:
activity_type_id = False
pr_line = move.created_purchase_request_line_id
if pr_line.product_id.responsible_id:
activity_user = pr_line.product_id.responsible_id
elif pr_line.request_id.assigned_to:
activity_user = pr_line.request_id.assigned_to
elif move.picking_id.user_id:
activity_user = move.picking_id.user_id
else:
activity_user = self.env.user
self.env["mail.activity"].sudo().create(
{
"activity_type_id": activity_type_id,
"note": _(
"A sale/manufacturing order that generated this "
"purchase request has been cancelled/deleted. "
"Check if an action is needed."
),
"user_id": activity_user.id,
"res_id": pr_line.request_id.id,
"res_model_id": self.env.ref(
"purchase_request.model_purchase_request"
).id,
}
)
def _action_cancel(self):
self._action_cancel_create_mail_activity()
return super(StockMove, self)._action_cancel()
@api.depends("purchase_request_allocation_ids")
def _compute_purchase_request_ids(self):
for rec in self:
rec.purchase_request_ids = (
rec.purchase_request_allocation_ids.purchase_request_line_id.request_id
)
def _merge_moves_fields(self):
res = super(StockMove, self)._merge_moves_fields()
res["purchase_request_allocation_ids"] = [
(4, m.id) for m in self.mapped("purchase_request_allocation_ids")
]
return res
@api.constrains("company_id")
def _check_company_purchase_request(self):
if not self.ids:
return
self.env.cr.execute(
"""
SELECT 1
FROM purchase_request_allocation pra
INNER JOIN stock_move sm
ON sm.id=pra.stock_move_id
WHERE pra.company_id != sm.company_id
AND sm.id IN %s
LIMIT 1
""",
(tuple(self.ids),),
)
if self.env.cr.fetchone():
raise ValidationError(
_(
"The company of the purchase request must match with "
"that of the location."
)
)
def copy_data(self, default=None):
"""Propagate request allocation on copy.
If this move is being split, or if this move is processed and there is
a remaining allocation, move the appropriate quantity over to the new move.
"""
if default is None:
default = {}
if not default.get("purchase_request_allocation_ids") and (
default.get("product_uom_qty") or self.state in ("done", "cancel")
):
default["purchase_request_allocation_ids"] = []
new_move_qty = default.get("product_uom_qty") or self.product_uom_qty
rounding = self.product_id.uom_id.rounding
for alloc in self.purchase_request_allocation_ids.filtered(
"open_product_qty"
):
if (
float_compare(
new_move_qty,
0,
precision_rounding=self.product_id.uom_id.rounding,
)
<= 0
or float_compare(
alloc.open_product_qty, 0, precision_rounding=rounding
)
<= 0
):
break
open_qty = min(new_move_qty, alloc.open_product_qty)
new_move_qty -= open_qty
default["purchase_request_allocation_ids"].append(
(
0,
0,
{
"purchase_request_line_id": alloc.purchase_request_line_id.id,
"requested_product_uom_qty": open_qty,
},
)
)
alloc.requested_product_uom_qty -= open_qty
return super(StockMove, self).copy_data(default)

View file

@ -0,0 +1,131 @@
# Copyright 2017 ForgeFlow, S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
from odoo import _, api, models
class StockMoveLine(models.Model):
_inherit = "stock.move.line"
@api.model
def _purchase_request_confirm_done_message_content(self, message_data):
title = _(
"Receipt confirmation %(picking_name)s for your Request %(request_name)s"
) % {
"picking_name": message_data["picking_name"],
"request_name": message_data["request_name"],
}
message = "<h3>%s</h3>" % title
message += _(
"The following requested items from Purchase Request %(request_name)s "
"have now been received in %(location_name)s using Picking %(picking_name)s:"
) % {
"request_name": message_data["request_name"],
"location_name": message_data["location_name"],
"picking_name": message_data["picking_name"],
}
message += "<ul>"
message += _(
"<li><b>%(product_name)s</b>: "
"Transferred quantity %(product_qty)s %(product_uom)s</li>"
) % {
"product_name": message_data["product_name"],
"product_qty": message_data["product_qty"],
"product_uom": message_data["product_uom"],
}
message += "</ul>"
return message
@api.model
def _picking_confirm_done_message_content(self, message_data):
title = _("Receipt confirmation for Request %s") % (
message_data["request_name"]
)
message = "<h3>%s</h3>" % title
message += _(
"The following requested items from Purchase Request %(request_name)s "
"requested by %(requestor)s "
"have now been received in %(location_name)s:"
) % {
"request_name": message_data["request_name"],
"requestor": message_data["requestor"],
"location_name": message_data["location_name"],
}
message += "<ul>"
message += _(
"<li><b>%(product_name)s</b>: "
"Transferred quantity %(product_qty)s %(product_uom)s</li>"
) % {
"product_name": message_data["product_name"],
"product_qty": message_data["product_qty"],
"product_uom": message_data["product_uom"],
}
message += "</ul>"
return message
def _prepare_message_data(self, ml, request, allocated_qty):
return {
"request_name": request.name,
"picking_name": ml.picking_id.name,
"product_name": ml.product_id.name_get()[0][1],
"product_qty": allocated_qty,
"product_uom": ml.product_uom_id.name,
"location_name": ml.location_dest_id.name_get()[0][1],
"requestor": request.requested_by.partner_id.name,
}
def allocate(self):
for ml in self.filtered(
lambda m: m.exists() and m.move_id.purchase_request_allocation_ids
):
# We do sudo because potentially the user that completes the move
# may not have permissions for purchase.request.
to_allocate_qty = ml.qty_done
to_allocate_uom = ml.product_uom_id
for allocation in ml.move_id.purchase_request_allocation_ids.sudo():
allocated_qty = 0.0
if allocation.open_product_qty and to_allocate_qty:
to_allocate_uom_qty = to_allocate_uom._compute_quantity(
to_allocate_qty, allocation.product_uom_id
)
allocated_qty = min(
allocation.open_product_qty, to_allocate_uom_qty
)
allocation.allocated_product_qty += allocated_qty
to_allocate_uom_qty -= allocated_qty
to_allocate_qty = allocation.product_uom_id._compute_quantity(
to_allocate_uom_qty, to_allocate_uom
)
request = allocation.purchase_request_line_id.request_id
if allocated_qty:
message_data = self._prepare_message_data(
ml, request, allocated_qty
)
message = self._purchase_request_confirm_done_message_content(
message_data
)
if message:
request.message_post(
body=message,
subtype_id=self.env.ref(
"purchase_request.mt_request_picking_done"
).id,
)
picking_message = self._picking_confirm_done_message_content(
message_data
)
if picking_message:
ml.move_id.picking_id.message_post(
body=picking_message,
subtype_id=self.env.ref("mail.mt_note").id,
)
allocation._compute_open_product_qty()
def _action_done(self):
res = super(StockMoveLine, self)._action_done()
self.allocate()
return res

View file

@ -0,0 +1,131 @@
# Copyright 2018-2020 ForgeFlow, S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
from odoo import api, fields, models
class StockRule(models.Model):
_inherit = "stock.rule"
@api.model
def _prepare_purchase_request_line(self, request_id, procurement):
procurement_uom_po_qty = procurement.product_uom._compute_quantity(
procurement.product_qty, procurement.product_id.uom_po_id
)
return {
"product_id": procurement.product_id.id,
"name": procurement.product_id.name,
"date_required": "date_planned" in procurement.values
and procurement.values["date_planned"]
or fields.Datetime.now(),
"product_uom_id": procurement.product_id.uom_po_id.id,
"product_qty": procurement_uom_po_qty,
"request_id": request_id.id,
"move_dest_ids": [
(4, x.id) for x in procurement.values.get("move_dest_ids", [])
],
"orderpoint_id": procurement.values.get("orderpoint_id", False)
and procurement.values.get("orderpoint_id").id,
}
@api.model
def _prepare_purchase_request(self, origin, values):
gpo = self.group_propagation_option
group_id = (
(gpo == "fixed" and self.group_id.id)
or (gpo == "propagate" and values.get("group_id") and values["group_id"].id)
or False
)
return {
"origin": origin,
"company_id": values["company_id"].id,
"picking_type_id": self.picking_type_id.id,
"group_id": group_id or False,
"requested_by": self.env.context.get("uid", self.env.uid),
"assigned_to": False,
}
@api.model
def _make_pr_get_domain(self, values):
"""
This method is to be implemented by other modules that can
provide a criteria to select the appropriate purchase request to be
extended.
:return: False
"""
domain = (
("state", "=", "draft"),
("picking_type_id", "=", self.picking_type_id.id),
("company_id", "=", values["company_id"].id),
)
gpo = self.group_propagation_option
group_id = (
(gpo == "fixed" and self.group_id.id)
or (gpo == "propagate" and values["group_id"].id)
or False
)
if group_id:
domain += (("group_id", "=", group_id),)
return domain
def is_create_purchase_request_allowed(self, procurement):
"""
Tell if current procurement order should
create a purchase request or not.
:return: boolean
"""
return (
procurement[1].action == "buy"
and procurement[0].product_id.purchase_request
)
def _run_buy(self, procurements):
indexes_to_pop = []
for i, procurement in enumerate(procurements):
if self.is_create_purchase_request_allowed(procurement):
self.create_purchase_request(procurement)
indexes_to_pop.append(i)
if indexes_to_pop:
indexes_to_pop.reverse()
for index in indexes_to_pop:
procurements.pop(index)
if not procurements:
return
return super(StockRule, self)._run_buy(procurements)
def create_purchase_request(self, procurement_group):
"""
Create a purchase request containing procurement order product.
"""
procurement = procurement_group[0]
rule = procurement_group[1]
purchase_request_model = self.env["purchase.request"]
purchase_request_line_model = self.env["purchase.request.line"]
cache = {}
pr = self.env["purchase.request"]
domain = rule._make_pr_get_domain(procurement.values)
if domain in cache:
pr = cache[domain]
elif domain:
pr = self.env["purchase.request"].search([dom for dom in domain])
pr = pr[0] if pr else False
cache[domain] = pr
if not pr:
request_data = rule._prepare_purchase_request(
procurement.origin, procurement.values
)
pr = purchase_request_model.create(request_data)
cache[domain] = pr
elif (
not pr.origin
or procurement.origin not in pr.origin.split(", ")
and procurement.origin != "/"
):
if pr.origin:
if procurement.origin:
pr.write({"origin": pr.origin + ", " + procurement.origin})
else:
pr.write({"origin": procurement.origin})
# Create Line
request_line_data = rule._prepare_purchase_request_line(pr, procurement)
purchase_request_line_model.create(request_line_data)

View file

@ -0,0 +1,9 @@
To configure the product follow this steps:
1. Go to a product form.
2. Go to *Inventory* tab.
3. Check the box *Purchase Request* along with the route *Buy*.
With this configuration, whenever a procurement order is created and the
supply rule selected is 'Buy' the application will create a Purchase
Request instead of a Purchase Order.

View file

@ -0,0 +1,9 @@
- Jordi Ballester Alomar \<<jordi.ballester@forgeflow.com>\>
- Jonathan Nemry \<<jonathan.nemry@acsone.eu>\>
- Aaron Henriquez \<<ahenriquez@forgeflow.com>\>
- Adrien Peiffer \<<adrien.peiffer@acsone.eu>\>
- Lois Rilo \<<lois.rilo@forgeflow.com>\>
- Héctor Villarreal \<<hector.villarreal@forgeflow.com>\>
- Ben Cai \<<ben.cai@elico-corp.com>\>
- Rattapong Chokmasermkul \<<rattapongc@ecosoft.co.th>\>
- Stefan Rijnhart \<<stefan@opener.amsterdam>\>

View file

@ -0,0 +1,7 @@
The development of this module has been financially supported by:
[![Aleph Objects, Inc](https://upload.wikimedia.org/wikipedia/en/3/3b/Aleph_Objects_Logo.png)](https://www.alephobjects.com)
## Images
- Enric Tobella (logo)

View file

@ -0,0 +1,38 @@
You use this module if you wish to give notification of requirements of
materials and/or external services and keep track of such requirements.
Requests can be created either directly or indirectly. "Directly" means
that someone from the requesting department enters a purchase request
manually.
The person creating the request determines what and how much to order,
and the requested date.
"Indirectly" means that the purchase request initiated by the
application automatically, for example, from procurement orders (MO,
SO).
A purchase request is an instruction to Purchasing to procure a certain
quantity of materials services, so that they are available at a certain
point in time.
A line of a request contains the quantity and requested date of the
material to be supplied or the quantity of the service to be performed.
You can indicate the service specifications if needed.
Once request is approved go to the Purchase Request Lines from the menu
entry 'Purchase Requests', and also from the 'Purchase' menu.
Select the lines that you wish to initiate the RFQ for, then go to
'More' and press 'Create RFQ'.
You can choose to select an existing RFQ or create a new one. In the
later, you have to choose a supplier.
In case that you chose to select an existing RFQ, the application will
search for existing lines matching the request line, and will add the
extra quantity to them, recalculating the minimum order quantity, if it
exists for the supplier of that RFQ.
In case that you create a new RFQ, the request lines will also be
consolidated into as few as possible lines in the RFQ.

View file

@ -0,0 +1,7 @@
Purchase requests are accessible through a new menu entry 'Purchase
Requests', and also from the 'Purchase' menu.
Users can access the list of Purchase Requests or Purchase Request
Lines.
It is possible to filter requests by its approval status.

View file

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2018-2019 ForgeFlow, S.L.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl-3.0) -->
<odoo>
<template id="report_purchase_request">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="web.external_layout">
<div class="page">
<div class="oe_structure" />
<h2>
Purchase Request
<span t-field="o.name" />
</h2>
<div class="row mt32 mb32">
<div class="col-auto col-3">
<strong>Request Reference:</strong>
<br />
<span t-field="o.name" />
</div>
<div class="col-auto col-3">
<strong>Creation Date:</strong>
<br />
<span t-field="o.date_start" />
</div>
<div class="col-auto col-3">
<strong>Source:</strong>
<br />
<span t-field="o.origin" />
</div>
<div class="col-auto col-3">
<strong>Description:</strong>
<br />
<span t-field="o.description" />
</div>
</div>
<div class="row mt32 mb32">
<div class="col-auto col-3">
<strong>Requested by:</strong>
<br />
<span t-field="o.requested_by" />
</div>
<div class="col-auto col-3">
<strong>Assigned to:</strong>
<br />
<span t-field="o.assigned_to" />
</div>
<div class="col-auto col-3">
<strong>Picking Type:</strong>
<br />
<span t-field="o.picking_type_id" />
</div>
</div>
<div class="row mt32 mb32">
<div class="col-12">
<strong>Description:</strong>
<br />
<span t-field="o.description" />
</div>
</div>
<t t-if="o.line_ids">
<h3>Products</h3>
<table class="table table-condensed">
<thead>
<tr>
<th>
<strong>Description</strong>
</th>
<th groups="analytic.group_analytic_accounting">
<strong>Analytic Distribution</strong>
</th>
<th class="text-right">
<strong>Qty</strong>
</th>
<th class="text-center" groups="uom.group_uom">
<strong>Product UoM</strong>
</th>
<th>
<strong>Estimated Cost</strong>
</th>
<th class="text-right">
<strong>Requested Date</strong>
</th>
<th>
<strong>Specifications</strong>
</th>
</tr>
</thead>
<tbody>
<tr t-foreach="o.line_ids" t-as="line_id">
<td>
<span t-field="line_id.name" />
</td>
<td groups="analytic.group_analytic_accounting">
<span
t-esc="line_id._get_analytic_distribution()"
/>
</td>
<td class="text-right">
<span t-field="line_id.product_qty" />
</td>
<t>
<td
class="text-center"
groups="uom.group_uom"
>
<span
t-field="line_id.product_uom_id.category_id.name"
/>
</td>
</t>
<td>
<span t-field="line_id.estimated_cost" />
</td>
<td class="text-right">
<span t-field="line_id.date_required" />
</td>
<td>
<span t-field="line_id.specifications" />
</td>
</tr>
</tbody>
</table>
</t>
<div class="oe_structure" />
</div>
</t>
</t>
</t>
</template>
</odoo>

View file

@ -0,0 +1,18 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_purchase_request_user,purchase.request,model_purchase_request,group_purchase_request_user,1,1,1,1
access_purchase_request_manager,purchase.request,model_purchase_request,group_purchase_request_manager,1,1,1,1
access_purchase_request_line_user,purchase.request.line,model_purchase_request_line,group_purchase_request_user,1,1,1,1
access_purchase_request_line_manager,purchase.request.line,model_purchase_request_line,group_purchase_request_manager,1,1,1,1
access_stock_move_purchase_request_user,stock.move,stock.model_stock_move,group_purchase_request_user,1,0,0,0
access_purchase_order_user,access.purchase.order.user,model_purchase_order,purchase_request.group_purchase_request_user,1,0,0,0
access_purchase_order_line_user,access.purchase.order.line.user,model_purchase_order_line,purchase_request.group_purchase_request_user,1,0,0,0
access_purchase_order_manager,access.purchase.order.manager,model_purchase_order,purchase_request.group_purchase_request_manager,1,0,0,0
access_purchase_order_line_manager,access.purchase.order.line.manager,model_purchase_order_line,purchase_request.group_purchase_request_manager,1,0,0,0
access_purchase_request_allocation_manager,purchase.request.allocation,model_purchase_request_allocation,group_purchase_request_manager,1,1,1,1
access_purchase_request_allocation_user,purchase.request.allocation,model_purchase_request_allocation,base.group_user,1,1,1,0
access_purchase_request_line_stock_user,purchase.request.line.stock,model_purchase_request_line,stock.group_stock_user,1,1,1,0
access_purchase_request_stock_user,purchase.request.stock,model_purchase_request,stock.group_stock_user,1,0,0,0
access_purchase_request_purchase_user,purchase.request,model_purchase_request,purchase.group_purchase_user,1,0,0,0
access_purchase_request_line_purchase_user,purchase.request.line,model_purchase_request_line,purchase.group_purchase_user,1,0,0,0
access_purchase_request_line_make_purchase_order,purchase.request.line.make.purchase.order,model_purchase_request_line_make_purchase_order,purchase.group_purchase_user,1,1,1,0
access_purchase_request_line_make_purchase_order_item,access_purchase_request_line_make_purchase_order_item,model_purchase_request_line_make_purchase_order_item,purchase.group_purchase_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_purchase_request_user purchase.request model_purchase_request group_purchase_request_user 1 1 1 1
3 access_purchase_request_manager purchase.request model_purchase_request group_purchase_request_manager 1 1 1 1
4 access_purchase_request_line_user purchase.request.line model_purchase_request_line group_purchase_request_user 1 1 1 1
5 access_purchase_request_line_manager purchase.request.line model_purchase_request_line group_purchase_request_manager 1 1 1 1
6 access_stock_move_purchase_request_user stock.move stock.model_stock_move group_purchase_request_user 1 0 0 0
7 access_purchase_order_user access.purchase.order.user model_purchase_order purchase_request.group_purchase_request_user 1 0 0 0
8 access_purchase_order_line_user access.purchase.order.line.user model_purchase_order_line purchase_request.group_purchase_request_user 1 0 0 0
9 access_purchase_order_manager access.purchase.order.manager model_purchase_order purchase_request.group_purchase_request_manager 1 0 0 0
10 access_purchase_order_line_manager access.purchase.order.line.manager model_purchase_order_line purchase_request.group_purchase_request_manager 1 0 0 0
11 access_purchase_request_allocation_manager purchase.request.allocation model_purchase_request_allocation group_purchase_request_manager 1 1 1 1
12 access_purchase_request_allocation_user purchase.request.allocation model_purchase_request_allocation base.group_user 1 1 1 0
13 access_purchase_request_line_stock_user purchase.request.line.stock model_purchase_request_line stock.group_stock_user 1 1 1 0
14 access_purchase_request_stock_user purchase.request.stock model_purchase_request stock.group_stock_user 1 0 0 0
15 access_purchase_request_purchase_user purchase.request model_purchase_request purchase.group_purchase_user 1 0 0 0
16 access_purchase_request_line_purchase_user purchase.request.line model_purchase_request_line purchase.group_purchase_user 1 0 0 0
17 access_purchase_request_line_make_purchase_order purchase.request.line.make.purchase.order model_purchase_request_line_make_purchase_order purchase.group_purchase_user 1 1 1 0
18 access_purchase_request_line_make_purchase_order_item access_purchase_request_line_make_purchase_order_item model_purchase_request_line_make_purchase_order_item purchase.group_purchase_user 1 1 1 1

View file

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2018-2019 ForgeFlow, S.L.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl-3.0) -->
<odoo>
<record model="ir.module.category" id="module_category_purchase_request">
<field name="name">Purchase Request</field>
<field name="parent_id" ref="base.module_category_purchase_management" />
<field name="sequence">10</field>
</record>
<record id="group_purchase_request_user" model="res.groups">
<field name="name">Purchase Request User</field>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]" />
<field name="category_id" ref="module_category_purchase_request" />
</record>
<record id="group_purchase_request_manager" model="res.groups">
<field name="name">Purchase Request Manager</field>
<field
name="implied_ids"
eval="[(4, ref('purchase_request.group_purchase_request_user'))]"
/>
<field name="category_id" ref="module_category_purchase_request" />
</record>
<record model="ir.rule" id="purchase_request_comp_rule">
<field name="name">Purchase Request multi-company</field>
<field name="model_id" ref="model_purchase_request" />
<field name="global" eval="True" />
<field name="domain_force">
['|',('company_id','=',False),('company_id', 'in', company_ids)]
</field>
</record>
<record model="ir.rule" id="purchase_request_line_comp_rule">
<field name="name">Purchase Request Line multi-company</field>
<field name="model_id" ref="model_purchase_request_line" />
<field name="global" eval="True" />
<field name="domain_force">
['|',('company_id','=',False),('company_id', 'in', company_ids)]
</field>
</record>
<record id="purchase_request_followers_rule" model="ir.rule">
<field name="name">Follow Purchase Request</field>
<field name="model_id" ref="model_purchase_request" />
<field name="groups" eval="[(6,0, [ref('group_purchase_request_user')])]" />
<field name="perm_read" eval="True" />
<field name="perm_write" eval="False" />
<field name="perm_create" eval="False" />
<field name="perm_unlink" eval="False" />
<field name="domain_force">
['|',('requested_by','=',user.id),
('message_partner_ids', 'in',
[user.partner_id.id])]
</field>
</record>
<record id="purchase_request_rule" model="ir.rule">
<field name="name">Purchase Request User</field>
<field name="model_id" ref="model_purchase_request" />
<field name="groups" eval="[(6,0, [ref('group_purchase_request_user')])]" />
<field name="perm_read" eval="True" />
<field name="perm_write" eval="True" />
<field name="perm_create" eval="True" />
<field name="perm_unlink" eval="True" />
<field name="domain_force">[('requested_by','=',user.id)]</field>
</record>
<record id="purchase_request_manager_rule" model="ir.rule">
<field name="name">Purchase Request Manager</field>
<field name="model_id" ref="model_purchase_request" />
<field name="groups" eval="[(6,0, [ref('group_purchase_request_manager')])]" />
<field name="perm_read" eval="True" />
<field name="perm_write" eval="True" />
<field name="perm_create" eval="True" />
<field name="perm_unlink" eval="True" />
</record>
<record id="purchase_request_line_followers_rule" model="ir.rule">
<field name="name">Follow Purchase Request Line</field>
<field name="model_id" ref="model_purchase_request_line" />
<field name="groups" eval="[(6,0, [ref('group_purchase_request_user')])]" />
<field name="perm_read" eval="True" />
<field name="perm_write" eval="False" />
<field name="perm_create" eval="False" />
<field name="perm_unlink" eval="False" />
<field name="domain_force">
['|',('request_id.requested_by','=',user.id),
('request_id.message_partner_ids', 'in',
[user.partner_id.id])]
</field>
</record>
<record id="purchase_request_line_rule" model="ir.rule">
<field name="name">Purchase Request Line User</field>
<field name="model_id" ref="model_purchase_request_line" />
<field name="groups" eval="[(6,0, [ref('group_purchase_request_user')])]" />
<field name="perm_read" eval="True" />
<field name="perm_write" eval="True" />
<field name="perm_create" eval="True" />
<field name="perm_unlink" eval="True" />
<field name="domain_force">[('request_id.requested_by','=',user.id)]</field>
</record>
<record id="purchase_request_line_manager_rule" model="ir.rule">
<field name="name">Purchase Request Line Manager</field>
<field name="model_id" ref="model_purchase_request_line" />
<field name="groups" eval="[(6,0, [ref('group_purchase_request_manager')])]" />
<field name="perm_read" eval="True" />
<field name="perm_write" eval="True" />
<field name="perm_create" eval="True" />
<field name="perm_unlink" eval="True" />
</record>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View file

@ -0,0 +1,235 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="70"
height="70"
viewBox="0 0 70 70"
version="1.1"
id="svg1411"
sodipodi:docname="icon.svg"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
inkscape:export-filename="icon.png"
inkscape:export-xdpi="702.17145"
inkscape:export-ydpi="702.17145"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1413"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="3.6678571"
inkscape:cx="34.488802"
inkscape:cy="56.299903"
inkscape:window-width="2560"
inkscape:window-height="1336"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg1411" />
<defs
id="defs1390">
<path
id="a"
d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z" />
<linearGradient
id="c"
x1="100%"
x2="0%"
y1="0%"
y2="100%">
<stop
offset="0"
stop-color="#DA956B"
id="stop1383"
style="stop-color:#0098e9;stop-opacity:1;" />
<stop
offset="1"
stop-color="#CC7039"
id="stop1385"
style="stop-color:#005ee9;stop-opacity:1;" />
</linearGradient>
<path
id="d"
d="M56.243 52.279c.578 0 1.05.537 1.05 1.193v.978c0 .656-.472 1.194-1.05 1.194H13.532c-.578 0-1.05-.538-1.05-1.194v-35.8c0-.657.472-1.194 1.05-1.194h1.5c.578 0 1.05.537 1.05 1.194v33.629h40.161zM39 23.025l4.981 4.963-6.302 7.25-4.866-5.53c-.411-.467-1.068-.467-1.48 0L20.92 41.423a1.31 1.31 0 0 0-.018 1.68l2.494 2.924c.412.477 1.086.487 1.497.01l7.186-8.165 4.857 5.52a.965.965 0 0 0 1.488 0l9.558-10.86L53 37.664 55 20l-16 3.025z" />
<path
id="e"
d="M56.243 50.279c.578 0 1.05.537 1.05 1.193v.978c0 .656-.472 1.194-1.05 1.194H13.532c-.578 0-1.05-.538-1.05-1.194v-35.8c0-.657.472-1.194 1.05-1.194h1.5c.578 0 1.05.537 1.05 1.194v33.629h40.161zM39 21.025l4.981 4.963-6.302 7.25-4.866-5.53c-.411-.467-1.068-.467-1.48 0L20.92 39.423a1.31 1.31 0 0 0-.018 1.68l2.494 2.924c.412.477 1.086.487 1.497.01l7.186-8.165 4.857 5.52a.965.965 0 0 0 1.488 0l9.558-10.86L53 35.664 55 18l-16 3.025z" />
</defs>
<g
fill="none"
fill-rule="evenodd"
id="g1409">
<mask
id="b"
fill="#fff">
<use
xlink:href="#a"
id="use1392" />
</mask>
<g
mask="url(#b)"
id="g1407">
<path
fill="url(#c)"
d="M0 0H70V70H0z"
id="path1395" />
<path
fill="#FFF"
fill-opacity=".383"
d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z"
id="path1397" />
<path
fill="#000"
fill-opacity=".383"
d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z"
id="path1401" />
</g>
</g>
<path
fill="#393939"
d="M 41.584593,70 H 4 C 2,70 0,69.854 0,65.923 V 38.315 l 11.287123,-19.013428 8.80888,1.904975 0.76947,21.004737 29.58972,0.04306 -2.73058,2.728796 2.65153,3.638445 -2.03926,2.144418 -4.08192,2.252829 v 0 l 6.4009,3.943336 z"
opacity="0.324"
id="path8694"
sodipodi:nodetypes="csscccccccccccc"
style="display:inline" />
<g
id="g18778-9"
transform="translate(1.67419,5.635197)"
style="display:inline;opacity:0.3;fill:#000000;fill-opacity:1">
<g
id="g1286-4"
style="fill:#000000;fill-opacity:1">
<g
id="g13446-4"
transform="matrix(1.1801658,0,0,1.1801658,-622.80849,-548.98328)"
style="display:inline;fill:#000000;fill-opacity:1">
<path
d="m 464,408.69265 c 26.50969,0 47.99998,21.49033 47.99998,48 0,26.50967 -21.49029,48 -47.99998,48 -26.50969,0 -47.99998,-21.49033 -47.99998,-48 0,-26.50967 21.49029,-48 47.99998,-48 z"
id="path6127-1"
style="display:inline;fill:#000000;fill-opacity:1"
transform="matrix(0.06706427,0,0,0.06706427,535.65039,477.6631)" />
<path
d="m 142.61469,456.69265 c 0,-26.50969 21.49033,-47.99998 48,-47.99998 26.50967,0 48,21.49029 48,47.99998 0,26.50969 -21.49033,47.99998 -48,47.99998 -26.50967,0 -48,-21.49029 -48,-47.99998 z"
id="path6125-8"
style="display:inline;fill:#000000;fill-opacity:1"
transform="matrix(0.06706427,0,0,0.06706427,535.65039,477.6631)" />
<path
d="M 0,24 C 0,10.7 10.7,0 24,0 h 45.5 c 22,0 41.5,12.8 50.6,32 l 37.5597,190.02535 c 2.17189,11.42869 10.03716,17.95307 22.81158,17.95307 h 274.98536 c 17.3825,0 28.78399,-4.08957 34.35915,-24.89636 L 531.54692,60.622762 c 9.6899,-35.8652 50.8989,-23.813459 41.1191,10.759554 L 528.7,234.7 c -8.5,31.4 -37,53.3 -69.5,53.3 H 170.7 l 2.74324,14.47822 L 176.1,316.5 c 2.2,11.3 12.1,19.5 23.6,19.5 H 488 c 13.3,0 24,10.7 24,24 0,13.3 -10.7,24 -24,24 H 199.7 C 165.1,384 135.4,359.4 129,325.5 L 124.36989,301.18293 77.4,54.5 C 76.7,50.7 73.4,48 69.5,48 H 24 C 10.7,48 0,37.3 0,24 Z"
id="path1320-6"
style="display:inline;fill:#000000;fill-opacity:1"
sodipodi:nodetypes="sssccsscsccscccssssscccsss"
transform="matrix(0.06706427,0,0,0.06706427,535.65039,477.6631)" />
</g>
<path
d="m 31.906385,16.272701 c 0.126946,-0.287304 0.414247,-0.474383 0.731617,-0.474383 h 1.336282 v -0.534513 c 0,-2.361881 1.914225,-4.276105 4.276104,-4.276105 h 14.435974 c 0.410908,-0.945422 1.352987,-1.603541 2.448739,-1.603541 1.476594,0 2.672567,1.195974 2.672567,2.672566 0,1.476595 -1.195973,2.672567 -2.672567,2.672567 -1.095752,0 -2.037831,-0.658119 -2.448739,-1.603541 H 38.250388 c -1.179269,0 -2.138051,0.958785 -2.138051,2.138054 v 0.534513 h 1.336282 c 0.317367,0 0.604668,0.187079 0.731614,0.474383 0.126947,0.2873 0.07684,0.628052 -0.136969,0.861901 l -2.405307,2.672567 c -0.153673,0.170376 -0.36748,0.263915 -0.594648,0.263915 -0.227169,0 -0.444314,-0.09688 -0.594646,-0.263915 l -2.40531,-2.672567 c -0.210464,-0.233849 -0.263915,-0.574601 -0.136968,-0.861901 z m 24.030486,-4.215976 c 0,-0.444314 -0.357455,-0.801769 -0.80177,-0.801769 -0.444313,0 -0.801768,0.357455 -0.801768,0.801769 0,0.444314 0.357455,0.80177 0.801768,0.80177 0.444315,0 0.80177,-0.357456 0.80177,-0.80177 z"
id="path19267-0"
sodipodi:nodetypes="sscsscssscsscssccsssssssss"
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.0334071" />
<g
id="g19248-9"
transform="matrix(1.1243102,0,0,1.1243102,-3.0997689,-4.0959205)"
inkscape:label="g19248"
style="fill:#000000;fill-opacity:1" />
</g>
</g>
<g
id="g18778"
transform="translate(1.67419,3.635197)"
style="display:inline;fill:#ffffff;fill-opacity:1">
<g
id="g1286"
style="fill:#ffffff;fill-opacity:1">
<g
id="g13446"
transform="matrix(1.1801658,0,0,1.1801658,-622.80849,-548.98328)"
style="display:inline;fill:#ffffff;fill-opacity:1">
<path
d="m 464,408.69265 c 26.50969,0 47.99998,21.49033 47.99998,48 0,26.50967 -21.49029,48 -47.99998,48 -26.50969,0 -47.99998,-21.49033 -47.99998,-48 0,-26.50967 21.49029,-48 47.99998,-48 z"
id="path6127"
style="display:inline;fill:#ffffff;fill-opacity:1"
transform="matrix(0.06706427,0,0,0.06706427,535.65039,477.6631)" />
<path
d="m 142.61469,456.69265 c 0,-26.50969 21.49033,-47.99998 48,-47.99998 26.50967,0 48,21.49029 48,47.99998 0,26.50969 -21.49033,47.99998 -48,47.99998 -26.50967,0 -48,-21.49029 -48,-47.99998 z"
id="path6125"
style="display:inline;fill:#ffffff;fill-opacity:1"
transform="matrix(0.06706427,0,0,0.06706427,535.65039,477.6631)" />
<path
d="M 0,24 C 0,10.7 10.7,0 24,0 h 45.5 c 22,0 41.5,12.8 50.6,32 l 37.5597,190.02535 c 2.17189,11.42869 10.03716,17.95307 22.81158,17.95307 h 274.98536 c 17.3825,0 28.78399,-4.08957 34.35915,-24.89636 L 531.54692,60.622762 c 9.6899,-35.8652 50.8989,-23.813459 41.1191,10.759554 L 528.7,234.7 c -8.5,31.4 -37,53.3 -69.5,53.3 H 170.7 l 2.74324,14.47822 L 176.1,316.5 c 2.2,11.3 12.1,19.5 23.6,19.5 H 488 c 13.3,0 24,10.7 24,24 0,13.3 -10.7,24 -24,24 H 199.7 C 165.1,384 135.4,359.4 129,325.5 L 124.36989,301.18293 77.4,54.5 C 76.7,50.7 73.4,48 69.5,48 H 24 C 10.7,48 0,37.3 0,24 Z"
id="path1320"
style="display:inline;fill:#ffffff;fill-opacity:1"
sodipodi:nodetypes="sssccsscsccscccssssscccsss"
transform="matrix(0.06706427,0,0,0.06706427,535.65039,477.6631)" />
</g>
<path
d="m 31.906385,16.272701 c 0.126946,-0.287304 0.414247,-0.474383 0.731617,-0.474383 h 1.336282 v -0.534513 c 0,-2.361881 1.914225,-4.276105 4.276104,-4.276105 h 14.435974 c 0.410908,-0.945422 1.352987,-1.603541 2.448739,-1.603541 1.476594,0 2.672567,1.195974 2.672567,2.672566 0,1.476595 -1.195973,2.672567 -2.672567,2.672567 -1.095752,0 -2.037831,-0.658119 -2.448739,-1.603541 H 38.250388 c -1.179269,0 -2.138051,0.958785 -2.138051,2.138054 v 0.534513 h 1.336282 c 0.317367,0 0.604668,0.187079 0.731614,0.474383 0.126947,0.2873 0.07684,0.628052 -0.136969,0.861901 l -2.405307,2.672567 c -0.153673,0.170376 -0.36748,0.263915 -0.594648,0.263915 -0.227169,0 -0.444314,-0.09688 -0.594646,-0.263915 l -2.40531,-2.672567 c -0.210464,-0.233849 -0.263915,-0.574601 -0.136968,-0.861901 z m 24.030486,-4.215976 c 0,-0.444314 -0.357455,-0.801769 -0.80177,-0.801769 -0.444313,0 -0.801768,0.357455 -0.801768,0.801769 0,0.444314 0.357455,0.80177 0.801768,0.80177 0.444315,0 0.80177,-0.357456 0.80177,-0.80177 z"
id="path19267"
sodipodi:nodetypes="sscsscssscsscssccsssssssss"
style="display:inline;fill:#ffffff;fill-opacity:1;stroke-width:0.0334071" />
<g
id="g5363"
transform="matrix(1.0702131,0,0,1.0702131,-2.6916321,-3.7728587)">
<g
id="g5304-8"
style="display:inline;opacity:0.3;fill:#000000;fill-opacity:1"
transform="translate(0,2)">
<g
id="g19248-17"
transform="matrix(1.4231766,0,0,1.1243102,-18.651769,-4.3689323)"
inkscape:label="g19248"
style="fill:#000000;fill-opacity:1">
<path
d="m 31.69128,27.474573 -0.620504,1.242232 h 2.545656 v -1.566558 h -1.398887 c -0.222744,0 -0.425908,0.126059 -0.526265,0.324326 z m 2.316791,1.242232 h 2.545656 l -0.620503,-1.242232 c -0.100358,-0.198267 -0.303521,-0.324326 -0.526266,-0.324326 h -1.398887 z m 2.545656,0.391639 h -5.482951 v 2.741476 c 0,0.432027 0.351252,0.783279 0.783279,0.783279 h 3.916394 c 0.432027,0 0.783278,-0.351252 0.783278,-0.783279 z"
id="path19063-0-2"
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.0122387"
transform="translate(0.10396099,-0.85039097)" />
</g>
<g
id="g19248-1-1"
transform="matrix(1.4231766,0,0,1.1243102,-7.4339209,-4.3689323)"
inkscape:label="g19248"
style="display:inline;fill:#000000;fill-opacity:1">
<path
d="m 31.69128,27.474573 -0.620504,1.242232 h 2.545656 v -1.566558 h -1.398887 c -0.222744,0 -0.425908,0.126059 -0.526265,0.324326 z m 2.316791,1.242232 h 2.545656 l -0.620503,-1.242232 c -0.100358,-0.198267 -0.303521,-0.324326 -0.526266,-0.324326 h -1.398887 z m 2.545656,0.391639 h -5.482951 v 2.741476 c 0,0.432027 0.351252,0.783279 0.783279,0.783279 h 3.916394 c 0.432027,0 0.783278,-0.351252 0.783278,-0.783279 z"
id="path19063-0-3-4"
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.0122387"
transform="translate(0.10396099,-0.85039097)" />
</g>
</g>
<g
id="g5304"
style="display:inline;fill:#ffffff;fill-opacity:1">
<g
id="g19248"
transform="matrix(1.4231766,0,0,1.1243102,-18.651769,-4.3689323)"
inkscape:label="g19248"
style="fill:#ffffff;fill-opacity:1">
<path
d="m 31.69128,27.474573 -0.620504,1.242232 h 2.545656 v -1.566558 h -1.398887 c -0.222744,0 -0.425908,0.126059 -0.526265,0.324326 z m 2.316791,1.242232 h 2.545656 l -0.620503,-1.242232 c -0.100358,-0.198267 -0.303521,-0.324326 -0.526266,-0.324326 h -1.398887 z m 2.545656,0.391639 h -5.482951 v 2.741476 c 0,0.432027 0.351252,0.783279 0.783279,0.783279 h 3.916394 c 0.432027,0 0.783278,-0.351252 0.783278,-0.783279 z"
id="path19063-0"
style="display:inline;fill:#ffffff;fill-opacity:1;stroke-width:0.0122387"
transform="translate(0.10396099,-0.85039097)" />
</g>
<g
id="g19248-1"
transform="matrix(1.4231766,0,0,1.1243102,-7.4339209,-4.3689323)"
inkscape:label="g19248"
style="display:inline;fill:#ffffff;fill-opacity:1">
<path
d="m 31.69128,27.474573 -0.620504,1.242232 h 2.545656 v -1.566558 h -1.398887 c -0.222744,0 -0.425908,0.126059 -0.526265,0.324326 z m 2.316791,1.242232 h 2.545656 l -0.620503,-1.242232 c -0.100358,-0.198267 -0.303521,-0.324326 -0.526266,-0.324326 h -1.398887 z m 2.545656,0.391639 h -5.482951 v 2.741476 c 0,0.432027 0.351252,0.783279 0.783279,0.783279 h 3.916394 c 0.432027,0 0.783278,-0.351252 0.783278,-0.783279 z"
id="path19063-0-3"
style="display:inline;fill:#ffffff;fill-opacity:1;stroke-width:0.0122387"
transform="translate(0.10396099,-0.85039097)" />
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,501 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>README.rst</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document">
<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
</a>
<div class="section" id="purchase-request">
<h1>Purchase Request</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:5b19c693955170a23b931af36c7918760373e06299b80106eaa28e8c188eafbd
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/license-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/purchase-workflow/tree/16.0/purchase_request"><img alt="OCA/purchase-workflow" src="https://img.shields.io/badge/github-OCA%2Fpurchase--workflow-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/purchase-workflow-16-0/purchase-workflow-16-0-purchase_request"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/purchase-workflow&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>You use this module if you wish to give notification of requirements of
materials and/or external services and keep track of such requirements.</p>
<p>Requests can be created either directly or indirectly. “Directly” means
that someone from the requesting department enters a purchase request
manually.</p>
<p>The person creating the request determines what and how much to order,
and the requested date.</p>
<p>“Indirectly” means that the purchase request initiated by the
application automatically, for example, from procurement orders (MO,
SO).</p>
<p>A purchase request is an instruction to Purchasing to procure a certain
quantity of materials services, so that they are available at a certain
point in time.</p>
<p>A line of a request contains the quantity and requested date of the
material to be supplied or the quantity of the service to be performed.
You can indicate the service specifications if needed.</p>
<p>Once request is approved go to the Purchase Request Lines from the menu
entry Purchase Requests, and also from the Purchase menu.</p>
<p>Select the lines that you wish to initiate the RFQ for, then go to
More and press Create RFQ.</p>
<p>You can choose to select an existing RFQ or create a new one. In the
later, you have to choose a supplier.</p>
<p>In case that you chose to select an existing RFQ, the application will
search for existing lines matching the request line, and will add the
extra quantity to them, recalculating the minimum order quantity, if it
exists for the supplier of that RFQ.</p>
<p>In case that you create a new RFQ, the request lines will also be
consolidated into as few as possible lines in the RFQ.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="toc-entry-2">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-3">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-4">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-5">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-6">Contributors</a></li>
<li><a class="reference internal" href="#other-credits" id="toc-entry-7">Other credits</a><ul>
<li><a class="reference internal" href="#images" id="toc-entry-8">Images</a></li>
</ul>
</li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-9">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configuration">
<h2><a class="toc-backref" href="#toc-entry-1">Configuration</a></h2>
<p>To configure the product follow this steps:</p>
<ol class="arabic simple">
<li>Go to a product form.</li>
<li>Go to <em>Inventory</em> tab.</li>
<li>Check the box <em>Purchase Request</em> along with the route <em>Buy</em>.</li>
</ol>
<p>With this configuration, whenever a procurement order is created and the
supply rule selected is Buy the application will create a Purchase
Request instead of a Purchase Order.</p>
</div>
<div class="section" id="usage">
<h2><a class="toc-backref" href="#toc-entry-2">Usage</a></h2>
<p>Purchase requests are accessible through a new menu entry Purchase
Requests, and also from the Purchase menu.</p>
<p>Users can access the list of Purchase Requests or Purchase Request
Lines.</p>
<p>It is possible to filter requests by its approval status.</p>
</div>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#toc-entry-3">Bug Tracker</a></h2>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/purchase-workflow/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/purchase-workflow/issues/new?body=module:%20purchase_request%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h2><a class="toc-backref" href="#toc-entry-4">Credits</a></h2>
<div class="section" id="authors">
<h3><a class="toc-backref" href="#toc-entry-5">Authors</a></h3>
<ul class="simple">
<li>ForgeFlow</li>
</ul>
</div>
<div class="section" id="contributors">
<h3><a class="toc-backref" href="#toc-entry-6">Contributors</a></h3>
<ul class="simple">
<li>Jordi Ballester Alomar &lt;<a class="reference external" href="mailto:jordi.ballester&#64;forgeflow.com">jordi.ballester&#64;forgeflow.com</a>&gt;</li>
<li>Jonathan Nemry &lt;<a class="reference external" href="mailto:jonathan.nemry&#64;acsone.eu">jonathan.nemry&#64;acsone.eu</a>&gt;</li>
<li>Aaron Henriquez &lt;<a class="reference external" href="mailto:ahenriquez&#64;forgeflow.com">ahenriquez&#64;forgeflow.com</a>&gt;</li>
<li>Adrien Peiffer &lt;<a class="reference external" href="mailto:adrien.peiffer&#64;acsone.eu">adrien.peiffer&#64;acsone.eu</a>&gt;</li>
<li>Lois Rilo &lt;<a class="reference external" href="mailto:lois.rilo&#64;forgeflow.com">lois.rilo&#64;forgeflow.com</a>&gt;</li>
<li>Héctor Villarreal &lt;<a class="reference external" href="mailto:hector.villarreal&#64;forgeflow.com">hector.villarreal&#64;forgeflow.com</a>&gt;</li>
<li>Ben Cai &lt;<a class="reference external" href="mailto:ben.cai&#64;elico-corp.com">ben.cai&#64;elico-corp.com</a>&gt;</li>
<li>Rattapong Chokmasermkul &lt;<a class="reference external" href="mailto:rattapongc&#64;ecosoft.co.th">rattapongc&#64;ecosoft.co.th</a>&gt;</li>
<li>Stefan Rijnhart &lt;<a class="reference external" href="mailto:stefan&#64;opener.amsterdam">stefan&#64;opener.amsterdam</a>&gt;</li>
</ul>
</div>
<div class="section" id="other-credits">
<h3><a class="toc-backref" href="#toc-entry-7">Other credits</a></h3>
<p>The development of this module has been financially supported by:</p>
<p><a class="reference external image-reference" href="https://www.alephobjects.com"><img alt="Aleph Objects, Inc" src="https://upload.wikimedia.org/wikipedia/en/3/3b/Aleph_Objects_Logo.png" /></a></p>
<div class="section" id="images">
<h4><a class="toc-backref" href="#toc-entry-8">Images</a></h4>
<ul class="simple">
<li>Enric Tobella (logo)</li>
</ul>
</div>
</div>
<div class="section" id="maintainers">
<h3><a class="toc-backref" href="#toc-entry-9">Maintainers</a></h3>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/purchase-workflow/tree/16.0/purchase_request">OCA/purchase-workflow</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</div>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show more