mirror of
https://github.com/bringout/oca-workflow-process.git
synced 2026-04-19 19:52:06 +02:00
Initial commit: OCA Workflow Process packages (456 packages)
This commit is contained in:
commit
d366e42934
18799 changed files with 1284507 additions and 0 deletions
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from . import models
|
||||
from . import wizard
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
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
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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.
|
||||
|
|
@ -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>\>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
The development of this module has been financially supported by:
|
||||
|
||||
[](https://www.alephobjects.com)
|
||||
|
||||
## Images
|
||||
|
||||
- Enric Tobella (logo)
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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&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 <<a class="reference external" href="mailto:jordi.ballester@forgeflow.com">jordi.ballester@forgeflow.com</a>></li>
|
||||
<li>Jonathan Nemry <<a class="reference external" href="mailto:jonathan.nemry@acsone.eu">jonathan.nemry@acsone.eu</a>></li>
|
||||
<li>Aaron Henriquez <<a class="reference external" href="mailto:ahenriquez@forgeflow.com">ahenriquez@forgeflow.com</a>></li>
|
||||
<li>Adrien Peiffer <<a class="reference external" href="mailto:adrien.peiffer@acsone.eu">adrien.peiffer@acsone.eu</a>></li>
|
||||
<li>Lois Rilo <<a class="reference external" href="mailto:lois.rilo@forgeflow.com">lois.rilo@forgeflow.com</a>></li>
|
||||
<li>Héctor Villarreal <<a class="reference external" href="mailto:hector.villarreal@forgeflow.com">hector.villarreal@forgeflow.com</a>></li>
|
||||
<li>Ben Cai <<a class="reference external" href="mailto:ben.cai@elico-corp.com">ben.cai@elico-corp.com</a>></li>
|
||||
<li>Rattapong Chokmasermkul <<a class="reference external" href="mailto:rattapongc@ecosoft.co.th">rattapongc@ecosoft.co.th</a>></li>
|
||||
<li>Stefan Rijnhart <<a class="reference external" href="mailto:stefan@opener.amsterdam">stefan@opener.amsterdam</a>></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
Loading…
Add table
Add a link
Reference in a new issue