mirror of
https://github.com/bringout/oca-workflow-process.git
synced 2026-04-19 23:52:01 +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,46 @@
|
|||
# Purchase Request
|
||||
|
||||
Odoo addon: purchase_request
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-purchase-workflow-purchase_request
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
This addon depends on:
|
||||
- purchase_stock
|
||||
|
||||
## Manifest Information
|
||||
|
||||
- **Name**: Purchase Request
|
||||
- **Version**: 16.0.2.4.0
|
||||
- **Category**: Purchase Management
|
||||
- **License**: LGPL-3
|
||||
- **Installable**: True
|
||||
|
||||
## Source
|
||||
|
||||
Based on [OCA/purchase-workflow](https://github.com/OCA/purchase-workflow) branch 16.0, addon `purchase_request`.
|
||||
|
||||
## License
|
||||
|
||||
This package maintains the original LGPL-3 license from the upstream Odoo project.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Overview: doc/OVERVIEW.md
|
||||
- Architecture: doc/ARCHITECTURE.md
|
||||
- Models: doc/MODELS.md
|
||||
- Controllers: doc/CONTROLLERS.md
|
||||
- Wizards: doc/WIZARDS.md
|
||||
- Reports: doc/REPORTS.md
|
||||
- Security: doc/SECURITY.md
|
||||
- Install: doc/INSTALL.md
|
||||
- Usage: doc/USAGE.md
|
||||
- Configuration: doc/CONFIGURATION.md
|
||||
- Dependencies: doc/DEPENDENCIES.md
|
||||
- Troubleshooting: doc/TROUBLESHOOTING.md
|
||||
- FAQ: doc/FAQ.md
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
U[Users] -->|HTTP| V[Views and QWeb Templates]
|
||||
V --> C[Controllers]
|
||||
V --> W[Wizards – Transient Models]
|
||||
C --> M[Models and ORM]
|
||||
W --> M
|
||||
M --> R[Reports]
|
||||
DX[Data XML] --> M
|
||||
S[Security – ACLs and Groups] -. enforces .-> M
|
||||
|
||||
subgraph Purchase_request Module - purchase_request
|
||||
direction LR
|
||||
M:::layer
|
||||
W:::layer
|
||||
C:::layer
|
||||
V:::layer
|
||||
R:::layer
|
||||
S:::layer
|
||||
DX:::layer
|
||||
end
|
||||
|
||||
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
|
||||
```
|
||||
|
||||
Notes
|
||||
- Views include tree/form/kanban templates and report templates.
|
||||
- Controllers provide website/portal routes when present.
|
||||
- Wizards are UI flows implemented with `models.TransientModel`.
|
||||
- Data XML loads data/demo records; Security defines groups and access.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Configuration
|
||||
|
||||
Refer to Odoo settings for purchase_request. Configure related models, access rights, and options as needed.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Controllers
|
||||
|
||||
This module does not define custom HTTP controllers.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Dependencies
|
||||
|
||||
This addon depends on:
|
||||
|
||||
- [purchase_stock](../../odoo-bringout-oca-ocb-purchase_stock)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# FAQ
|
||||
|
||||
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
|
||||
- Q: How to enable? A: Start server with --addon purchase_request or install in UI.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Install
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-purchase-workflow-purchase_request"
|
||||
# or
|
||||
uv pip install odoo-bringout-oca-purchase-workflow-purchase_request"
|
||||
```
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# Models
|
||||
|
||||
Detected core models and extensions in purchase_request.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class purchase_request
|
||||
class purchase_request_allocation
|
||||
class purchase_request_line
|
||||
class product_template
|
||||
class purchase_order
|
||||
class purchase_order_line
|
||||
class stock_move
|
||||
class stock_move_line
|
||||
class stock_rule
|
||||
class stock_warehouse_orderpoint
|
||||
```
|
||||
|
||||
Notes
|
||||
- Classes show model technical names; fields omitted for brevity.
|
||||
- Items listed under _inherit are extensions of existing models.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# Overview
|
||||
|
||||
Packaged Odoo addon: purchase_request. Provides features documented in upstream Odoo 16 under this addon.
|
||||
|
||||
- Source: OCA/OCB 16.0, addon purchase_request
|
||||
- License: LGPL-3
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Reports
|
||||
|
||||
This module does not define custom reports.
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# Security
|
||||
|
||||
Access control and security definitions in purchase_request.
|
||||
|
||||
## Access Control Lists (ACLs)
|
||||
|
||||
Model access permissions defined in:
|
||||
- **[ir.model.access.csv](../purchase_request/security/ir.model.access.csv)**
|
||||
- 17 model access rules
|
||||
|
||||
## Record Rules
|
||||
|
||||
Row-level security rules defined in:
|
||||
|
||||
## Security Groups & Configuration
|
||||
|
||||
Security groups and permissions defined in:
|
||||
- **[purchase_request.xml](../purchase_request/security/purchase_request.xml)**
|
||||
- 2 security groups defined
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Security Layers"
|
||||
A[Users] --> B[Groups]
|
||||
B --> C[Access Control Lists]
|
||||
C --> D[Models]
|
||||
B --> E[Record Rules]
|
||||
E --> F[Individual Records]
|
||||
end
|
||||
```
|
||||
|
||||
Security files overview:
|
||||
- **[ir.model.access.csv](../purchase_request/security/ir.model.access.csv)**
|
||||
- Model access permissions (CRUD rights)
|
||||
- **[purchase_request.xml](../purchase_request/security/purchase_request.xml)**
|
||||
- Security groups, categories, and XML-based rules
|
||||
|
||||
Notes
|
||||
- Access Control Lists define which groups can access which models
|
||||
- Record Rules provide row-level security (filter records by user/group)
|
||||
- Security groups organize users and define permission sets
|
||||
- All security is enforced at the ORM level by Odoo
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Troubleshooting
|
||||
|
||||
- Ensure Python and Odoo environment matches repo guidance.
|
||||
- Check database connectivity and logs if startup fails.
|
||||
- Validate that dependent addons listed in DEPENDENCIES.md are installed.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Usage
|
||||
|
||||
Start Odoo including this addon (from repo root):
|
||||
|
||||
```bash
|
||||
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon purchase_request
|
||||
```
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Wizards
|
||||
|
||||
Transient models exposed as UI wizards in purchase_request.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class PurchaseRequestLineMakePurchaseOrder
|
||||
class PurchaseRequestLineMakePurchaseOrderItem
|
||||
```
|
||||
|
|
@ -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
|
||||
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