Initial commit: OCA Workflow Process packages (456 packages)
48
odoo-bringout-oca-sale-workflow-sale_blanket_order/README.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Sale Blanket Orders
|
||||
|
||||
Odoo addon: sale_blanket_order
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-sale-workflow-sale_blanket_order
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
This addon depends on:
|
||||
- uom
|
||||
- sale_management
|
||||
- web_action_conditionable
|
||||
|
||||
## Manifest Information
|
||||
|
||||
- **Name**: Sale Blanket Orders
|
||||
- **Version**: 16.0.1.3.1
|
||||
- **Category**: Sale
|
||||
- **License**: AGPL-3
|
||||
- **Installable**: True
|
||||
|
||||
## Source
|
||||
|
||||
Based on [OCA/sale-workflow](https://github.com/OCA/sale-workflow) branch 16.0, addon `sale_blanket_order`.
|
||||
|
||||
## License
|
||||
|
||||
This package maintains the original AGPL-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 Sale_blanket_order Module - sale_blanket_order
|
||||
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 sale_blanket_order. 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,7 @@
|
|||
# Dependencies
|
||||
|
||||
This addon depends on:
|
||||
|
||||
- [uom](../../odoo-bringout-oca-ocb-uom)
|
||||
- [sale_management](../../odoo-bringout-oca-ocb-sale_management)
|
||||
- [web_action_conditionable](../../odoo-bringout-oca-web-web_action_conditionable)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# FAQ
|
||||
|
||||
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
|
||||
- Q: How to enable? A: Start server with --addon sale_blanket_order or install in UI.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Install
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-sale-workflow-sale_blanket_order"
|
||||
# or
|
||||
uv pip install odoo-bringout-oca-sale-workflow-sale_blanket_order"
|
||||
```
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# Models
|
||||
|
||||
Detected core models and extensions in sale_blanket_order.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class sale_blanket_order
|
||||
class sale_blanket_order_line
|
||||
class res_config_settings
|
||||
class sale_order
|
||||
class sale_order_line
|
||||
```
|
||||
|
||||
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: sale_blanket_order. Provides features documented in upstream Odoo 16 under this addon.
|
||||
|
||||
- Source: OCA/OCB 16.0, addon sale_blanket_order
|
||||
- License: LGPL-3
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# Reports
|
||||
|
||||
Report definitions and templates in sale_blanket_order.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
```
|
||||
|
||||
## Available Reports
|
||||
|
||||
### PDF/Document Reports
|
||||
- **Blanket Order** (PDF/Print)
|
||||
|
||||
|
||||
## Report Files
|
||||
|
||||
- **report.xml** (XML template/definition)
|
||||
- **templates.xml** (XML template/definition)
|
||||
|
||||
## Notes
|
||||
- Named reports above are accessible through Odoo's reporting menu
|
||||
- Python files define report logic and data processing
|
||||
- XML files contain report templates, definitions, and formatting
|
||||
- Reports are integrated with Odoo's printing and email systems
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# Security
|
||||
|
||||
Access control and security definitions in sale_blanket_order.
|
||||
|
||||
## Access Control Lists (ACLs)
|
||||
|
||||
Model access permissions defined in:
|
||||
- **[ir.model.access.csv](../sale_blanket_order/security/ir.model.access.csv)**
|
||||
- 9 model access rules
|
||||
|
||||
## Record Rules
|
||||
|
||||
Row-level security rules defined in:
|
||||
|
||||
## Security Groups & Configuration
|
||||
|
||||
Security groups and permissions defined in:
|
||||
- **[security.xml](../sale_blanket_order/security/security.xml)**
|
||||
- 1 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](../sale_blanket_order/security/ir.model.access.csv)**
|
||||
- Model access permissions (CRUD rights)
|
||||
- **[security.xml](../sale_blanket_order/security/security.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 sale_blanket_order
|
||||
```
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Wizards
|
||||
|
||||
Transient models exposed as UI wizards in sale_blanket_order.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class BlanketOrderWizard
|
||||
class BlanketOrderWizardLine
|
||||
```
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
[project]
|
||||
name = "odoo-bringout-oca-sale-workflow-sale_blanket_order"
|
||||
version = "16.0.0"
|
||||
description = "Sale Blanket Orders - Blanket Orders"
|
||||
authors = [
|
||||
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
|
||||
]
|
||||
dependencies = [
|
||||
"odoo-bringout-oca-ocb-uom>=16.0.0",
|
||||
"odoo-bringout-oca-sale-workflow-sale_management>=16.0.0",
|
||||
"odoo-bringout-oca-sale-workflow-web_action_conditionable>=16.0.0",
|
||||
"requests>=2.25.1"
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">= 3.11"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Office/Business",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://github.com/bringout/0"
|
||||
repository = "https://github.com/bringout/0"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.metadata]
|
||||
allow-direct-references = true
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["sale_blanket_order"]
|
||||
|
||||
[tool.rye]
|
||||
managed = true
|
||||
dev-dependencies = [
|
||||
"pytest>=8.4.1",
|
||||
]
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
.. image:: https://odoo-community.org/readme-banner-image
|
||||
:target: https://odoo-community.org/get-involved?utm_source=readme
|
||||
:alt: Odoo Community Association
|
||||
|
||||
===================
|
||||
Sale Blanket Orders
|
||||
===================
|
||||
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:2304d65f97e02bd0fec986276cb77baf60ddd090d6bc436ae7004c8276b7c666
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |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-AGPL--3-blue.png
|
||||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||
:alt: License: AGPL-3
|
||||
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsale--workflow-lightgray.png?logo=github
|
||||
:target: https://github.com/OCA/sale-workflow/tree/16.0/sale_blanket_order
|
||||
:alt: OCA/sale-workflow
|
||||
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
||||
:target: https://translation.odoo-community.org/projects/sale-workflow-16-0/sale-workflow-16-0-sale_blanket_order
|
||||
: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/sale-workflow&target_branch=16.0
|
||||
:alt: Try me on Runboat
|
||||
|
||||
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||
|
||||
A blanket order is a pre-agreement to sell a certain number of quantities of
|
||||
products at a specific price. From a confirmed blanket order, the users can
|
||||
create new sale orders at such price, until the blanket order expires, either
|
||||
due to reaching the validity date or exhausting all the quantities of products.
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
A new menu in the Sales area is created, allowing users to create new blanket orders.
|
||||
|
||||
To create a new Sale Blanket Order go to the sale menu in the Sales section:
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/sale-workflow/16.0/sale_blanket_order/static/description/BO_menu.png
|
||||
:alt: Blanket Orders menu
|
||||
|
||||
Hitting the button create will open the form view in which we can introduce the following
|
||||
information:
|
||||
|
||||
* Vendor
|
||||
* Salesperson
|
||||
* Payment Terms
|
||||
* Validity date
|
||||
* Order lines:
|
||||
* Product
|
||||
* Accorded price
|
||||
* Original, Ordered, Invoiced, Received and Remaining quantities
|
||||
* Terms and Conditions of the Blanket Order
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/sale-workflow/16.0/sale_blanket_order/static/description/BO_form.png
|
||||
:alt: Blanket Orders form
|
||||
|
||||
From the form, once the Blanket Order has been confirmed and its state is open, the user can
|
||||
create a Sale Order, check the Sale Orders associated to the Blanket Order and/or
|
||||
see the Blanket Order lines associated to the BO.
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/sale-workflow/16.0/sale_blanket_order/static/description/BO_actions.png
|
||||
:alt: Actions that can be done from Blanket Order
|
||||
|
||||
Hitting the button Create Sale Order will open a wizard that will ask for the amount of each
|
||||
product in the BO lines for which the Sale Order will be created.
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/sale-workflow/16.0/sale_blanket_order/static/description/PO_from_BO.png
|
||||
:alt: Create Sale Order from Blanket Order
|
||||
|
||||
Installing this module will add an additional menu which will show all the blanket order lines
|
||||
currently defined in the system. From this list the user can create customized Sale Orders
|
||||
selecting the lines for which the PO (or POs if the customers are different) is (are) created.
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/sale-workflow/16.0/sale_blanket_order/static/description/BO_lines.png
|
||||
:alt: Blanket Order lines and actions
|
||||
|
||||
In the Sale Order form one field is added in the PO lines, the Blanket Order line field. This
|
||||
field keeps track to which Blanket Order line the PO line is associated. Upon adding a new product
|
||||
in a newly created Sale Order a blanket order line will be suggested depending on the following
|
||||
factors:
|
||||
|
||||
* Closer Validity date
|
||||
* Remaining quantity > Quantity introduced in the Sale Order line
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/sale-workflow/16.0/sale_blanket_order/static/description/PO_BOLine.png
|
||||
:alt: New field added in Sale Order Line
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/sale-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/sale-workflow/issues/new?body=module:%20sale_blanket_order%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
|
||||
~~~~~~~
|
||||
|
||||
* Acsone SA/NV
|
||||
|
||||
Contributors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* André Pereira <github@andreparames.com> (https://www.acsone.eu/)
|
||||
* Adrià Gil Sorribes <adria.gil@eficent.com> (https://www.eficent.com/)
|
||||
* Jordi Ballester Alomar <jordi.ballester@eficent.com>
|
||||
* Alex Comba <alex.comba@agilebg.com> (https://www.agilebg.com/)
|
||||
* Jasper Jumelet <jasper.jumelet@codeforward.nl> (https://www.codeforward.nl/)
|
||||
* `Trobz <https://trobz.com>`_:
|
||||
|
||||
* Nguyễn Minh Chiến <chien@trobz.com>
|
||||
|
||||
Other credits
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
The migration of this module from 15.0 to 16.0 was financially supported by Camptocamp
|
||||
|
||||
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/sale-workflow <https://github.com/OCA/sale-workflow/tree/16.0/sale_blanket_order>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import models
|
||||
from . import wizard
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# Copyright 2018 Acsone
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
{
|
||||
"name": "Sale Blanket Orders",
|
||||
"category": "Sale",
|
||||
"license": "AGPL-3",
|
||||
"author": "Acsone SA/NV, Odoo Community Association (OCA)",
|
||||
"version": "16.0.1.3.1",
|
||||
"website": "https://github.com/OCA/sale-workflow",
|
||||
"summary": "Blanket Orders",
|
||||
"depends": ["uom", "sale_management", "web_action_conditionable"],
|
||||
"data": [
|
||||
"security/security.xml",
|
||||
"security/ir.model.access.csv",
|
||||
"data/sequence.xml",
|
||||
"data/ir_cron.xml",
|
||||
"wizard/create_sale_orders.xml",
|
||||
"views/sale_config_settings.xml",
|
||||
"views/sale_blanket_order_views.xml",
|
||||
"views/sale_blanket_order_line_views.xml",
|
||||
"views/sale_order_views.xml",
|
||||
"report/templates.xml",
|
||||
"report/report.xml",
|
||||
],
|
||||
"installable": True,
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!-- Copyright 2018 ACSONE SA/NV
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
<odoo noupdate="1">
|
||||
<record forcecreate="True" id="expired_blanket_orders_cron" model="ir.cron">
|
||||
<field name="name">Expire Blanket Orders</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field
|
||||
name="nextcall"
|
||||
eval="(DateTime.now() + relativedelta(hour=00, minute=1, second=0)).strftime('%Y-%m-%d %H:%M:%S')"
|
||||
/>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False" />
|
||||
<field name="model_id" ref="model_sale_blanket_order" />
|
||||
<field name="state">code</field>
|
||||
<field name="code">model.expire_orders()</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<!-- Sequences for sale.blanket.order -->
|
||||
<record id="seq_blanket_order" model="ir.sequence">
|
||||
<field name="name">Blanket Order</field>
|
||||
<field name="code">sale.blanket.order</field>
|
||||
<field name="prefix">BO</field>
|
||||
<field name="padding">3</field>
|
||||
<field name="company_id" eval="False" />
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from . import blanket_orders
|
||||
from . import sale_orders
|
||||
from . import sale_config_settings
|
||||
|
|
@ -0,0 +1,716 @@
|
|||
# Copyright 2018 ACSONE SA/NV
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import SUPERUSER_ID, _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import float_is_zero
|
||||
from odoo.tools.misc import format_date
|
||||
|
||||
from odoo.addons.sale.models.sale_order import READONLY_FIELD_STATES
|
||||
|
||||
|
||||
class BlanketOrder(models.Model):
|
||||
_name = "sale.blanket.order"
|
||||
_inherit = ["mail.thread", "mail.activity.mixin"]
|
||||
_description = "Blanket Order"
|
||||
_check_company_auto = True
|
||||
|
||||
@api.model
|
||||
def _default_note(self):
|
||||
return (
|
||||
self.env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param("account.use_invoice_terms")
|
||||
and self.env.company.invoice_terms
|
||||
or ""
|
||||
)
|
||||
|
||||
@api.depends("line_ids.price_total")
|
||||
def _compute_amount_all(self):
|
||||
for order in self.filtered("currency_id"):
|
||||
amount_untaxed = amount_tax = 0.0
|
||||
for line in order.line_ids:
|
||||
amount_untaxed += line.price_subtotal
|
||||
amount_tax += line.price_tax
|
||||
order.update(
|
||||
{
|
||||
"amount_untaxed": order.currency_id.round(amount_untaxed),
|
||||
"amount_tax": order.currency_id.round(amount_tax),
|
||||
"amount_total": amount_untaxed + amount_tax,
|
||||
}
|
||||
)
|
||||
|
||||
name = fields.Char(default="Draft", readonly=True, copy=False)
|
||||
partner_id = fields.Many2one(
|
||||
"res.partner",
|
||||
string="Partner",
|
||||
states=READONLY_FIELD_STATES,
|
||||
)
|
||||
line_ids = fields.One2many(
|
||||
"sale.blanket.order.line", "order_id", string="Order lines", copy=True
|
||||
)
|
||||
line_count = fields.Integer(
|
||||
string="Sale Blanket Order Line count",
|
||||
compute="_compute_line_count",
|
||||
readonly=True,
|
||||
)
|
||||
product_id = fields.Many2one(
|
||||
"product.product",
|
||||
related="line_ids.product_id",
|
||||
string="Product",
|
||||
)
|
||||
pricelist_id = fields.Many2one(
|
||||
"product.pricelist",
|
||||
string="Pricelist",
|
||||
required=True,
|
||||
states=READONLY_FIELD_STATES,
|
||||
)
|
||||
currency_id = fields.Many2one("res.currency", related="pricelist_id.currency_id")
|
||||
analytic_account_id = fields.Many2one(
|
||||
comodel_name="account.analytic.account",
|
||||
string="Analytic Account",
|
||||
copy=False,
|
||||
check_company=True,
|
||||
states=READONLY_FIELD_STATES,
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
||||
)
|
||||
payment_term_id = fields.Many2one(
|
||||
"account.payment.term",
|
||||
string="Payment Terms",
|
||||
states=READONLY_FIELD_STATES,
|
||||
)
|
||||
confirmed = fields.Boolean(copy=False)
|
||||
state = fields.Selection(
|
||||
selection=[
|
||||
("draft", "Draft"),
|
||||
("open", "Open"),
|
||||
("done", "Done"),
|
||||
("expired", "Expired"),
|
||||
],
|
||||
compute="_compute_state",
|
||||
store=True,
|
||||
copy=False,
|
||||
)
|
||||
validity_date = fields.Date(
|
||||
states=READONLY_FIELD_STATES,
|
||||
)
|
||||
client_order_ref = fields.Char(
|
||||
string="Customer Reference",
|
||||
copy=False,
|
||||
states=READONLY_FIELD_STATES,
|
||||
)
|
||||
note = fields.Text(default=_default_note, states=READONLY_FIELD_STATES)
|
||||
user_id = fields.Many2one(
|
||||
"res.users",
|
||||
string="Salesperson",
|
||||
states=READONLY_FIELD_STATES,
|
||||
)
|
||||
team_id = fields.Many2one(
|
||||
"crm.team",
|
||||
string="Sales Team",
|
||||
change_default=True,
|
||||
default=lambda self: self.env["crm.team"]._get_default_team_id(),
|
||||
states=READONLY_FIELD_STATES,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
comodel_name="res.company",
|
||||
required=True,
|
||||
index=True,
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
sale_count = fields.Integer(compute="_compute_sale_count")
|
||||
|
||||
fiscal_position_id = fields.Many2one(
|
||||
"account.fiscal.position", string="Fiscal Position"
|
||||
)
|
||||
|
||||
amount_untaxed = fields.Monetary(
|
||||
string="Untaxed Amount",
|
||||
store=True,
|
||||
readonly=True,
|
||||
compute="_compute_amount_all",
|
||||
tracking=True,
|
||||
)
|
||||
amount_tax = fields.Monetary(
|
||||
string="Taxes", store=True, readonly=True, compute="_compute_amount_all"
|
||||
)
|
||||
amount_total = fields.Monetary(
|
||||
string="Total", store=True, readonly=True, compute="_compute_amount_all"
|
||||
)
|
||||
|
||||
# Fields use to filter in tree view
|
||||
original_uom_qty = fields.Float(
|
||||
string="Original quantity",
|
||||
compute="_compute_uom_qty",
|
||||
search="_search_original_uom_qty",
|
||||
default=0.0,
|
||||
)
|
||||
ordered_uom_qty = fields.Float(
|
||||
string="Ordered quantity",
|
||||
compute="_compute_uom_qty",
|
||||
search="_search_ordered_uom_qty",
|
||||
default=0.0,
|
||||
)
|
||||
invoiced_uom_qty = fields.Float(
|
||||
string="Invoiced quantity",
|
||||
compute="_compute_uom_qty",
|
||||
search="_search_invoiced_uom_qty",
|
||||
default=0.0,
|
||||
)
|
||||
remaining_uom_qty = fields.Float(
|
||||
string="Remaining quantity",
|
||||
compute="_compute_uom_qty",
|
||||
search="_search_remaining_uom_qty",
|
||||
default=0.0,
|
||||
)
|
||||
delivered_uom_qty = fields.Float(
|
||||
string="Delivered quantity",
|
||||
compute="_compute_uom_qty",
|
||||
search="_search_delivered_uom_qty",
|
||||
default=0.0,
|
||||
)
|
||||
|
||||
def _get_sale_orders(self):
|
||||
return self.mapped("line_ids.sale_lines.order_id")
|
||||
|
||||
@api.depends("line_ids")
|
||||
def _compute_line_count(self):
|
||||
self.line_count = len(self.mapped("line_ids"))
|
||||
|
||||
def _compute_sale_count(self):
|
||||
for blanket_order in self:
|
||||
blanket_order.sale_count = len(blanket_order._get_sale_orders())
|
||||
|
||||
@api.depends(
|
||||
"line_ids.remaining_uom_qty",
|
||||
"validity_date",
|
||||
"confirmed",
|
||||
)
|
||||
def _compute_state(self):
|
||||
today = fields.Date.today()
|
||||
precision = self.env["decimal.precision"].precision_get(
|
||||
"Product Unit of Measure"
|
||||
)
|
||||
for order in self:
|
||||
if not order.confirmed:
|
||||
order.state = "draft"
|
||||
elif order.validity_date <= today:
|
||||
order.state = "expired"
|
||||
elif float_is_zero(
|
||||
sum(
|
||||
order.line_ids.filtered(lambda l: not l.display_type).mapped(
|
||||
"remaining_uom_qty"
|
||||
)
|
||||
),
|
||||
precision_digits=precision,
|
||||
):
|
||||
order.state = "done"
|
||||
else:
|
||||
order.state = "open"
|
||||
|
||||
def _compute_uom_qty(self):
|
||||
for bo in self:
|
||||
bo.original_uom_qty = sum(bo.mapped("line_ids.original_uom_qty"))
|
||||
bo.ordered_uom_qty = sum(bo.mapped("line_ids.ordered_uom_qty"))
|
||||
bo.invoiced_uom_qty = sum(bo.mapped("line_ids.invoiced_uom_qty"))
|
||||
bo.delivered_uom_qty = sum(bo.mapped("line_ids.delivered_uom_qty"))
|
||||
bo.remaining_uom_qty = sum(bo.mapped("line_ids.remaining_uom_qty"))
|
||||
|
||||
@api.onchange("partner_id")
|
||||
def onchange_partner_id(self):
|
||||
"""
|
||||
Update the following fields when the partner is changed:
|
||||
- Pricelist
|
||||
- Payment term
|
||||
- Fiscal position
|
||||
"""
|
||||
if not self.partner_id:
|
||||
self.payment_term_id = False
|
||||
self.fiscal_position_id = False
|
||||
return
|
||||
|
||||
values = {
|
||||
"pricelist_id": (
|
||||
self.partner_id.property_product_pricelist
|
||||
and self.partner_id.property_product_pricelist.id
|
||||
or False
|
||||
),
|
||||
"payment_term_id": (
|
||||
self.partner_id.property_payment_term_id
|
||||
and self.partner_id.property_payment_term_id.id
|
||||
or False
|
||||
),
|
||||
"fiscal_position_id": self.env["account.fiscal.position"]
|
||||
.with_context(company_id=self.company_id.id)
|
||||
._get_fiscal_position(self.partner_id),
|
||||
}
|
||||
|
||||
if self.partner_id.user_id:
|
||||
values["user_id"] = self.partner_id.user_id.id
|
||||
if self.partner_id.team_id:
|
||||
values["team_id"] = self.partner_id.team_id.id
|
||||
self.update(values)
|
||||
|
||||
def unlink(self):
|
||||
for order in self:
|
||||
if order.state not in ("draft", "expired") or order._check_active_orders():
|
||||
raise UserError(
|
||||
_(
|
||||
"You can not delete an open blanket or "
|
||||
"with active sale orders! "
|
||||
"Try to cancel it before."
|
||||
)
|
||||
)
|
||||
return super().unlink()
|
||||
|
||||
def _validate(self):
|
||||
try:
|
||||
today = fields.Date.today()
|
||||
for order in self:
|
||||
assert order.validity_date, _("Validity date is mandatory")
|
||||
assert order.validity_date > today, _(
|
||||
"Validity date must be in the future"
|
||||
)
|
||||
assert order.partner_id, _("Partner is mandatory")
|
||||
assert len(order.line_ids) > 0, _("Must have some lines")
|
||||
order.line_ids._validate()
|
||||
except AssertionError as e:
|
||||
raise UserError(e) from e
|
||||
|
||||
def set_to_draft(self):
|
||||
for order in self:
|
||||
order.write({"state": "draft", "confirmed": False})
|
||||
return True
|
||||
|
||||
def action_confirm(self):
|
||||
self._validate()
|
||||
for order in self:
|
||||
sequence_obj = self.env["ir.sequence"]
|
||||
if order.company_id:
|
||||
sequence_obj = sequence_obj.with_company(order.company_id.id)
|
||||
name = sequence_obj.next_by_code("sale.blanket.order")
|
||||
order.write({"confirmed": True, "name": name})
|
||||
return True
|
||||
|
||||
def _check_active_orders(self):
|
||||
for order in self.filtered("sale_count"):
|
||||
for so in order._get_sale_orders():
|
||||
if so.state not in ("cancel"):
|
||||
return True
|
||||
return False
|
||||
|
||||
def action_cancel(self):
|
||||
for order in self:
|
||||
if order._check_active_orders():
|
||||
raise UserError(
|
||||
_(
|
||||
"You can not delete a blanket order with opened "
|
||||
"sale orders! "
|
||||
"Try to cancel them before."
|
||||
)
|
||||
)
|
||||
order.write({"state": "expired"})
|
||||
return True
|
||||
|
||||
def action_view_sale_orders(self):
|
||||
sale_orders = self._get_sale_orders()
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id("sale.action_orders")
|
||||
if len(sale_orders) > 0:
|
||||
action["domain"] = [("id", "in", sale_orders.ids)]
|
||||
action["context"] = [("id", "in", sale_orders.ids)]
|
||||
else:
|
||||
action = {"type": "ir.actions.act_window_close"}
|
||||
return action
|
||||
|
||||
def action_view_sale_blanket_order_line(self):
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"sale_blanket_order.act_open_sale_blanket_order_lines_view_tree"
|
||||
)
|
||||
lines = self.mapped("line_ids")
|
||||
if len(lines) > 0:
|
||||
action["domain"] = [("id", "in", lines.ids)]
|
||||
return action
|
||||
|
||||
@api.model
|
||||
def expire_orders(self):
|
||||
today = fields.Date.today()
|
||||
expired_orders = self.search(
|
||||
[("state", "=", "open"), ("validity_date", "<=", today)]
|
||||
)
|
||||
expired_orders.modified(["validity_date"])
|
||||
expired_orders.flush_recordset()
|
||||
|
||||
@api.model
|
||||
def _search_original_uom_qty(self, operator, value):
|
||||
bo_line_obj = self.env["sale.blanket.order.line"]
|
||||
res = []
|
||||
bo_lines = bo_line_obj.search([("original_uom_qty", operator, value)])
|
||||
order_ids = bo_lines.mapped("order_id")
|
||||
res.append(("id", "in", order_ids.ids))
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _search_ordered_uom_qty(self, operator, value):
|
||||
bo_line_obj = self.env["sale.blanket.order.line"]
|
||||
res = []
|
||||
bo_lines = bo_line_obj.search([("ordered_uom_qty", operator, value)])
|
||||
order_ids = bo_lines.mapped("order_id")
|
||||
res.append(("id", "in", order_ids.ids))
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _search_invoiced_uom_qty(self, operator, value):
|
||||
bo_line_obj = self.env["sale.blanket.order.line"]
|
||||
res = []
|
||||
bo_lines = bo_line_obj.search([("invoiced_uom_qty", operator, value)])
|
||||
order_ids = bo_lines.mapped("order_id")
|
||||
res.append(("id", "in", order_ids.ids))
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _search_delivered_uom_qty(self, operator, value):
|
||||
bo_line_obj = self.env["sale.blanket.order.line"]
|
||||
res = []
|
||||
bo_lines = bo_line_obj.search([("delivered_uom_qty", operator, value)])
|
||||
order_ids = bo_lines.mapped("order_id")
|
||||
res.append(("id", "in", order_ids.ids))
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _search_remaining_uom_qty(self, operator, value):
|
||||
bo_line_obj = self.env["sale.blanket.order.line"]
|
||||
res = []
|
||||
bo_lines = bo_line_obj.search([("remaining_uom_qty", operator, value)])
|
||||
order_ids = bo_lines.mapped("order_id")
|
||||
res.append(("id", "in", order_ids.ids))
|
||||
return res
|
||||
|
||||
|
||||
class BlanketOrderLine(models.Model):
|
||||
_name = "sale.blanket.order.line"
|
||||
_description = "Blanket Order Line"
|
||||
_inherit = ["mail.thread", "mail.activity.mixin", "analytic.mixin"]
|
||||
|
||||
@api.depends(
|
||||
"original_uom_qty",
|
||||
"price_unit",
|
||||
"taxes_id",
|
||||
"order_id.partner_id",
|
||||
"product_id",
|
||||
"currency_id",
|
||||
)
|
||||
def _compute_amount(self):
|
||||
for line in self:
|
||||
price = line.price_unit
|
||||
taxes = line.taxes_id.compute_all(
|
||||
price,
|
||||
line.currency_id,
|
||||
line.original_uom_qty,
|
||||
product=line.product_id,
|
||||
partner=line.order_id.partner_id,
|
||||
)
|
||||
line.update(
|
||||
{
|
||||
"price_tax": sum(
|
||||
t.get("amount", 0.0) for t in taxes.get("taxes", [])
|
||||
),
|
||||
"price_total": taxes["total_included"],
|
||||
"price_subtotal": taxes["total_excluded"],
|
||||
}
|
||||
)
|
||||
|
||||
name = fields.Char("Description", tracking=True)
|
||||
sequence = fields.Integer()
|
||||
order_id = fields.Many2one("sale.blanket.order", required=True, ondelete="cascade")
|
||||
product_id = fields.Many2one(
|
||||
"product.product",
|
||||
string="Product",
|
||||
domain=[("sale_ok", "=", True)],
|
||||
)
|
||||
product_uom = fields.Many2one("uom.uom", string="Unit of Measure")
|
||||
price_unit = fields.Float(string="Price", digits="Product Price")
|
||||
taxes_id = fields.Many2many(
|
||||
"account.tax",
|
||||
string="Taxes",
|
||||
domain=["|", ("active", "=", False), ("active", "=", True)],
|
||||
)
|
||||
date_schedule = fields.Date(string="Scheduled Date")
|
||||
original_uom_qty = fields.Float(
|
||||
string="Original quantity", default=1, digits="Product Unit of Measure"
|
||||
)
|
||||
ordered_uom_qty = fields.Float(
|
||||
string="Ordered quantity", compute="_compute_quantities", store=True
|
||||
)
|
||||
invoiced_uom_qty = fields.Float(
|
||||
string="Invoiced quantity", compute="_compute_quantities", store=True
|
||||
)
|
||||
remaining_uom_qty = fields.Float(
|
||||
string="Remaining quantity", compute="_compute_quantities", store=True
|
||||
)
|
||||
remaining_qty = fields.Float(
|
||||
string="Remaining quantity in base UoM",
|
||||
compute="_compute_quantities",
|
||||
store=True,
|
||||
)
|
||||
delivered_uom_qty = fields.Float(
|
||||
string="Delivered quantity", compute="_compute_quantities", store=True
|
||||
)
|
||||
sale_lines = fields.One2many(
|
||||
"sale.order.line",
|
||||
"blanket_order_line",
|
||||
string="Sale order lines",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
related="order_id.company_id", store=True, index=True, precompute=True
|
||||
)
|
||||
currency_id = fields.Many2one("res.currency", related="order_id.currency_id")
|
||||
partner_id = fields.Many2one(related="order_id.partner_id", string="Customer")
|
||||
user_id = fields.Many2one(related="order_id.user_id", string="Responsible")
|
||||
payment_term_id = fields.Many2one(
|
||||
related="order_id.payment_term_id", string="Payment Terms"
|
||||
)
|
||||
pricelist_id = fields.Many2one(related="order_id.pricelist_id", string="Pricelist")
|
||||
|
||||
price_subtotal = fields.Monetary(
|
||||
compute="_compute_amount", string="Subtotal", store=True
|
||||
)
|
||||
price_total = fields.Monetary(compute="_compute_amount", string="Total", store=True)
|
||||
price_tax = fields.Float(compute="_compute_amount", string="Tax", store=True)
|
||||
display_type = fields.Selection(
|
||||
[("line_section", "Section"), ("line_note", "Note")],
|
||||
default=False,
|
||||
help="Technical field for UX purpose.",
|
||||
)
|
||||
|
||||
def name_get(self):
|
||||
result = []
|
||||
if self.env.context.get("from_sale_order"):
|
||||
for record in self:
|
||||
res = "[%s]" % record.order_id.name
|
||||
if record.date_schedule:
|
||||
formatted_date = format_date(record.env, record.date_schedule)
|
||||
res += " - {}: {}".format(_("Date Scheduled"), formatted_date)
|
||||
res += " ({}: {} {})".format(
|
||||
_("remaining"),
|
||||
record.remaining_uom_qty,
|
||||
record.product_uom.name,
|
||||
)
|
||||
result.append((record.id, res))
|
||||
return result
|
||||
return super().name_get()
|
||||
|
||||
def _get_real_price_currency(self, product, rule_id, qty, uom, pricelist_id):
|
||||
"""Retrieve the price before applying the pricelist
|
||||
:param obj product: object of current product record
|
||||
:param float qty: total quentity of product
|
||||
:param tuple price_and_rule: tuple(price, suitable_rule) coming
|
||||
from pricelist computation
|
||||
:param obj uom: unit of measure of current order line
|
||||
:param integer pricelist_id: pricelist id of sale order"""
|
||||
# Copied and adapted from the sale module
|
||||
PricelistItem = self.env["product.pricelist.item"]
|
||||
field_name = "lst_price"
|
||||
currency_id = None
|
||||
product_currency = None
|
||||
if rule_id:
|
||||
pricelist_item = PricelistItem.browse(rule_id)
|
||||
if pricelist_item.pricelist_id.discount_policy == "without_discount":
|
||||
while (
|
||||
pricelist_item.base == "pricelist"
|
||||
and pricelist_item.base_pricelist_id
|
||||
and pricelist_item.base_pricelist_id.discount_policy
|
||||
== "without_discount"
|
||||
):
|
||||
price, rule_id = pricelist_item.base_pricelist_id.with_context(
|
||||
uom=uom.id
|
||||
)._get_product_price_rule(product, qty, uom)
|
||||
pricelist_item = PricelistItem.browse(rule_id)
|
||||
|
||||
if pricelist_item.base == "standard_price":
|
||||
field_name = "standard_price"
|
||||
if pricelist_item.base == "pricelist" and pricelist_item.base_pricelist_id:
|
||||
field_name = "price"
|
||||
product = product.with_context(
|
||||
pricelist=pricelist_item.base_pricelist_id.id
|
||||
)
|
||||
product_currency = pricelist_item.base_pricelist_id.currency_id
|
||||
currency_id = pricelist_item.pricelist_id.currency_id
|
||||
|
||||
product_currency = (
|
||||
product_currency
|
||||
or (product.company_id and product.company_id.currency_id)
|
||||
or self.env.company.currency_id
|
||||
)
|
||||
if not currency_id:
|
||||
currency_id = product_currency
|
||||
cur_factor = 1.0
|
||||
else:
|
||||
if currency_id.id == product_currency.id:
|
||||
cur_factor = 1.0
|
||||
else:
|
||||
cur_factor = currency_id._get_conversion_rate(
|
||||
product_currency, currency_id
|
||||
)
|
||||
|
||||
product_uom = product.uom_id.id
|
||||
if uom and uom.id != product_uom:
|
||||
# the unit price is in a different uom
|
||||
uom_factor = uom._compute_price(1.0, product.uom_id)
|
||||
else:
|
||||
uom_factor = 1.0
|
||||
|
||||
return product[field_name] * uom_factor * cur_factor, currency_id.id
|
||||
|
||||
def _get_display_price(self, product):
|
||||
# Copied and adapted from the sale module
|
||||
self.ensure_one()
|
||||
pricelist = self.order_id.pricelist_id
|
||||
partner = self.order_id.partner_id
|
||||
if self.order_id.pricelist_id.discount_policy == "with_discount":
|
||||
return product.with_context(pricelist=pricelist.id).lst_price
|
||||
final_price, rule_id = pricelist._get_product_price_rule(
|
||||
self.product_id, self.original_uom_qty or 1.0, self.product_uom
|
||||
)
|
||||
context_partner = dict(
|
||||
self.env.context, partner_id=partner.id, date=fields.Date.today()
|
||||
)
|
||||
base_price, currency_id = self.with_context(
|
||||
**context_partner
|
||||
)._get_real_price_currency(
|
||||
self.product_id,
|
||||
rule_id,
|
||||
self.original_uom_qty,
|
||||
self.product_uom,
|
||||
pricelist.id,
|
||||
)
|
||||
if currency_id != pricelist.currency_id.id:
|
||||
currency = self.env["res.currency"].browse(currency_id)
|
||||
base_price = currency.with_context(**context_partner).compute(
|
||||
base_price, pricelist.currency_id
|
||||
)
|
||||
# negative discounts (= surcharge) are included in the display price
|
||||
return max(base_price, final_price)
|
||||
|
||||
@api.onchange("product_id", "original_uom_qty")
|
||||
def onchange_product(self):
|
||||
precision = self.env["decimal.precision"].precision_get(
|
||||
"Product Unit of Measure"
|
||||
)
|
||||
if self.product_id:
|
||||
name = self.product_id.name
|
||||
if not self.product_uom:
|
||||
self.product_uom = self.product_id.uom_id.id
|
||||
if self.order_id.partner_id and float_is_zero(
|
||||
self.price_unit, precision_digits=precision
|
||||
):
|
||||
self.price_unit = self._get_display_price(self.product_id)
|
||||
if self.product_id.code:
|
||||
name = "[{}] {}".format(name, self.product_id.code)
|
||||
if self.product_id.description_sale:
|
||||
name += "\n" + self.product_id.description_sale
|
||||
self.name = name
|
||||
|
||||
fpos = self.order_id.fiscal_position_id
|
||||
if self.env.uid == SUPERUSER_ID:
|
||||
company_id = self.env.company.id
|
||||
self.taxes_id = fpos.map_tax(
|
||||
self.product_id.taxes_id.filtered(
|
||||
lambda r: r.company_id.id == company_id
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.taxes_id = fpos.map_tax(self.product_id.taxes_id)
|
||||
|
||||
@api.depends(
|
||||
"sale_lines.order_id.state",
|
||||
"sale_lines.blanket_order_line",
|
||||
"sale_lines.product_uom_qty",
|
||||
"sale_lines.product_uom",
|
||||
"sale_lines.qty_delivered",
|
||||
"sale_lines.qty_invoiced",
|
||||
"original_uom_qty",
|
||||
"product_uom",
|
||||
)
|
||||
def _compute_quantities(self):
|
||||
for line in self:
|
||||
sale_lines = line.sale_lines
|
||||
line.ordered_uom_qty = sum(
|
||||
sl.product_uom._compute_quantity(sl.product_uom_qty, line.product_uom)
|
||||
for sl in sale_lines
|
||||
if sl.order_id.state != "cancel" and sl.product_id == line.product_id
|
||||
)
|
||||
line.invoiced_uom_qty = sum(
|
||||
sl.product_uom._compute_quantity(sl.qty_invoiced, line.product_uom)
|
||||
for sl in sale_lines
|
||||
if sl.order_id.state != "cancel" and sl.product_id == line.product_id
|
||||
)
|
||||
line.delivered_uom_qty = sum(
|
||||
sl.product_uom._compute_quantity(sl.qty_delivered, line.product_uom)
|
||||
for sl in sale_lines
|
||||
if sl.order_id.state != "cancel" and sl.product_id == line.product_id
|
||||
)
|
||||
line.remaining_uom_qty = line.original_uom_qty - line.ordered_uom_qty
|
||||
line.remaining_qty = line.product_uom._compute_quantity(
|
||||
line.remaining_uom_qty, line.product_id.uom_id
|
||||
)
|
||||
|
||||
def _validate(self):
|
||||
try:
|
||||
for line in self:
|
||||
assert (
|
||||
not line.display_type and line.original_uom_qty > 0.0
|
||||
) or line.display_type, _("Quantity must be greater than zero")
|
||||
except AssertionError as e:
|
||||
raise UserError(e) from e
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for values in vals_list:
|
||||
if values.get(
|
||||
"display_type", self.default_get(["display_type"])["display_type"]
|
||||
):
|
||||
values.update(product_id=False, price_unit=0, product_uom=False)
|
||||
|
||||
return super().create(vals_list)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"accountable_required_fields",
|
||||
"""
|
||||
CHECK(
|
||||
display_type IS NOT NULL OR (
|
||||
product_id IS NOT NULL AND product_uom IS NOT NULL
|
||||
)
|
||||
)
|
||||
""",
|
||||
"Missing required fields on accountable sale order line.",
|
||||
),
|
||||
(
|
||||
"non_accountable_null_fields",
|
||||
"""
|
||||
CHECK(
|
||||
display_type IS NULL OR (
|
||||
product_id IS NULL AND price_unit = 0 AND product_uom IS NULL
|
||||
)
|
||||
)
|
||||
""",
|
||||
"Forbidden values on non-accountable sale order line",
|
||||
),
|
||||
]
|
||||
|
||||
def write(self, values):
|
||||
if "display_type" in values and self.filtered(
|
||||
lambda line: line.display_type != values.get("display_type")
|
||||
):
|
||||
raise UserError(
|
||||
_(
|
||||
"""
|
||||
You cannot change the type of a sale order line.
|
||||
Instead you should delete the current line and create a new line
|
||||
of the proper type.
|
||||
"""
|
||||
)
|
||||
)
|
||||
return super(BlanketOrderLine, self).write(values)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# Copyright 2018 ACSONE SA/NV
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class SaleConfigSettings(models.TransientModel):
|
||||
|
||||
_inherit = "res.config.settings"
|
||||
|
||||
group_blanket_disable_adding_lines = fields.Boolean(
|
||||
string="Disable adding more lines to SOs",
|
||||
implied_group="sale_blanket_order.blanket_orders_disable_adding_lines",
|
||||
)
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
# Copyright 2018 ACSONE SA/NV
|
||||
# Copyright 2019 Eficent and IT Consulting Services, S.L.
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
from datetime import date, timedelta
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = "sale.order"
|
||||
|
||||
blanket_order_id = fields.Many2one(
|
||||
"sale.blanket.order",
|
||||
string="Origin blanket order",
|
||||
compute="_compute_blanket_order_id",
|
||||
)
|
||||
disable_adding_lines = fields.Boolean(
|
||||
compute="_compute_disable_adding_lines",
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _check_exchausted_blanket_order_line(self):
|
||||
return any(
|
||||
line.blanket_order_line.remaining_qty < 0.0 for line in self.order_line
|
||||
)
|
||||
|
||||
def action_confirm(self):
|
||||
res = super().action_confirm()
|
||||
for order in self:
|
||||
if order._check_exchausted_blanket_order_line():
|
||||
raise ValidationError(
|
||||
_(
|
||||
"Cannot confirm order %s as one of the lines refers "
|
||||
"to a blanket order that has no remaining quantity."
|
||||
)
|
||||
% order.name
|
||||
)
|
||||
return res
|
||||
|
||||
@api.constrains("partner_id")
|
||||
def check_partner_id(self):
|
||||
for line in self.order_line:
|
||||
if line.blanket_order_line:
|
||||
if line.blanket_order_line.partner_id != self.partner_id:
|
||||
raise ValidationError(
|
||||
_(
|
||||
"The customer must be equal to the "
|
||||
"blanket order lines customer"
|
||||
)
|
||||
)
|
||||
|
||||
@api.depends("order_line.blanket_order_line.order_id")
|
||||
def _compute_blanket_order_id(self):
|
||||
for order in self:
|
||||
blanket_order = order.order_line.mapped("blanket_order_line.order_id")
|
||||
order.blanket_order_id = blanket_order[:1]
|
||||
|
||||
@api.depends("blanket_order_id")
|
||||
@api.depends_context("uid")
|
||||
def _compute_disable_adding_lines(self):
|
||||
self.disable_adding_lines = False
|
||||
if self.env.user.has_group(
|
||||
"sale_blanket_order.blanket_orders_disable_adding_lines"
|
||||
):
|
||||
for order in self:
|
||||
order.disable_adding_lines = order.blanket_order_id
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = "sale.order.line"
|
||||
|
||||
blanket_order_line = fields.Many2one(
|
||||
"sale.blanket.order.line", string="Blanket Order line", copy=False
|
||||
)
|
||||
|
||||
def _get_assigned_bo_line(self, bo_lines):
|
||||
# We get the blanket order line with enough quantity and closest
|
||||
# scheduled date
|
||||
assigned_bo_line = False
|
||||
date_planned = date.today()
|
||||
date_delta = timedelta(days=365)
|
||||
for line in bo_lines.filtered(lambda l: l.date_schedule):
|
||||
date_schedule = line.date_schedule
|
||||
if date_schedule and abs(date_schedule - date_planned) < date_delta:
|
||||
assigned_bo_line = line
|
||||
date_delta = abs(date_schedule - date_planned)
|
||||
if assigned_bo_line:
|
||||
return assigned_bo_line
|
||||
non_date_bo_lines = bo_lines.filtered(lambda l: not l.date_schedule)
|
||||
if non_date_bo_lines:
|
||||
return non_date_bo_lines[0]
|
||||
|
||||
def _get_eligible_bo_lines_domain(self, base_qty):
|
||||
filters = [
|
||||
("product_id", "=", self.product_id.id),
|
||||
("remaining_qty", ">=", base_qty),
|
||||
("currency_id", "=", self.order_id.currency_id.id),
|
||||
("order_id.state", "=", "open"),
|
||||
]
|
||||
if self.order_id.partner_id:
|
||||
filters.append(("partner_id", "=", self.order_id.partner_id.id))
|
||||
return filters
|
||||
|
||||
def _get_eligible_bo_lines(self):
|
||||
base_qty = self.product_uom._compute_quantity(
|
||||
self.product_uom_qty, self.product_id.uom_id
|
||||
)
|
||||
filters = self._get_eligible_bo_lines_domain(base_qty)
|
||||
return self.env["sale.blanket.order.line"].search(filters)
|
||||
|
||||
def get_assigned_bo_line(self):
|
||||
self.ensure_one()
|
||||
eligible_bo_lines = self._get_eligible_bo_lines()
|
||||
if eligible_bo_lines:
|
||||
if (
|
||||
not self.blanket_order_line
|
||||
or self.blanket_order_line not in eligible_bo_lines
|
||||
):
|
||||
self.blanket_order_line = self._get_assigned_bo_line(eligible_bo_lines)
|
||||
else:
|
||||
self.blanket_order_line = False
|
||||
self.onchange_blanket_order_line()
|
||||
return {"domain": {"blanket_order_line": [("id", "in", eligible_bo_lines.ids)]}}
|
||||
|
||||
@api.onchange("product_id", "order_partner_id")
|
||||
def onchange_product_id(self):
|
||||
# If product has changed remove the relation with blanket order line
|
||||
if self.product_id:
|
||||
return self.get_assigned_bo_line()
|
||||
return
|
||||
|
||||
@api.onchange("product_uom", "product_uom_qty")
|
||||
def product_uom_change(self):
|
||||
if not self.product_uom or not self.product_id:
|
||||
self.price_unit = 0.0
|
||||
return
|
||||
if self.order_id.pricelist_id and self.order_id.partner_id:
|
||||
product = self.product_id.with_context(
|
||||
lang=self.order_id.partner_id.lang,
|
||||
partner=self.order_id.partner_id,
|
||||
quantity=self.product_uom_qty,
|
||||
date=self.order_id.date_order,
|
||||
pricelist=self.order_id.pricelist_id.id,
|
||||
uom=self.product_uom.id,
|
||||
fiscal_position=self.env.context.get("fiscal_position"),
|
||||
)
|
||||
self.price_unit = product._get_tax_included_unit_price(
|
||||
self.company_id or self.order_id.company_id,
|
||||
self.order_id.currency_id,
|
||||
self.order_id.date_order,
|
||||
"sale",
|
||||
fiscal_position=self.order_id.fiscal_position_id,
|
||||
product_price_unit=self._get_display_price(),
|
||||
product_currency=self.order_id.currency_id,
|
||||
)
|
||||
if self.product_id and not self.env.context.get("skip_blanket_find", False):
|
||||
return self.get_assigned_bo_line()
|
||||
return
|
||||
|
||||
@api.onchange("blanket_order_line")
|
||||
def onchange_blanket_order_line(self):
|
||||
bol = self.blanket_order_line
|
||||
if bol:
|
||||
self.product_id = bol.product_id
|
||||
if bol.product_uom != self.product_uom:
|
||||
price_unit = bol.product_uom._compute_price(
|
||||
bol.price_unit, self.product_uom
|
||||
)
|
||||
else:
|
||||
price_unit = bol.price_unit
|
||||
self.price_unit = price_unit
|
||||
if bol.taxes_id:
|
||||
self.tax_id = bol.taxes_id
|
||||
else:
|
||||
if not self.tax_id:
|
||||
self._compute_tax_id()
|
||||
self.with_context(skip_blanket_find=True).product_uom_change()
|
||||
|
||||
@api.constrains("product_id")
|
||||
def check_product_id(self):
|
||||
for line in self:
|
||||
if (
|
||||
line.blanket_order_line
|
||||
and line.product_id != line.blanket_order_line.product_id
|
||||
):
|
||||
raise ValidationError(
|
||||
_(
|
||||
"The product in the blanket order and in the "
|
||||
"sales order must match"
|
||||
)
|
||||
)
|
||||
|
||||
@api.constrains("currency_id")
|
||||
def check_currency(self):
|
||||
for line in self:
|
||||
if line.blanket_order_line:
|
||||
if line.currency_id != line.blanket_order_line.order_id.currency_id:
|
||||
raise ValidationError(
|
||||
_(
|
||||
"The currency of the blanket order must match with "
|
||||
"that of the sale order."
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
* André Pereira <github@andreparames.com> (https://www.acsone.eu/)
|
||||
* Adrià Gil Sorribes <adria.gil@eficent.com> (https://www.eficent.com/)
|
||||
* Jordi Ballester Alomar <jordi.ballester@eficent.com>
|
||||
* Alex Comba <alex.comba@agilebg.com> (https://www.agilebg.com/)
|
||||
* Jasper Jumelet <jasper.jumelet@codeforward.nl> (https://www.codeforward.nl/)
|
||||
* `Trobz <https://trobz.com>`_:
|
||||
|
||||
* Nguyễn Minh Chiến <chien@trobz.com>
|
||||
|
|
@ -0,0 +1 @@
|
|||
The migration of this module from 15.0 to 16.0 was financially supported by Camptocamp
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
A blanket order is a pre-agreement to sell a certain number of quantities of
|
||||
products at a specific price. From a confirmed blanket order, the users can
|
||||
create new sale orders at such price, until the blanket order expires, either
|
||||
due to reaching the validity date or exhausting all the quantities of products.
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
A new menu in the Sales area is created, allowing users to create new blanket orders.
|
||||
|
||||
To create a new Sale Blanket Order go to the sale menu in the Sales section:
|
||||
|
||||
.. figure:: ../static/description/BO_menu.png
|
||||
:alt: Blanket Orders menu
|
||||
|
||||
Hitting the button create will open the form view in which we can introduce the following
|
||||
information:
|
||||
|
||||
* Vendor
|
||||
* Salesperson
|
||||
* Payment Terms
|
||||
* Validity date
|
||||
* Order lines:
|
||||
* Product
|
||||
* Accorded price
|
||||
* Original, Ordered, Invoiced, Received and Remaining quantities
|
||||
* Terms and Conditions of the Blanket Order
|
||||
|
||||
.. figure:: ../static/description/BO_form.png
|
||||
:alt: Blanket Orders form
|
||||
|
||||
From the form, once the Blanket Order has been confirmed and its state is open, the user can
|
||||
create a Sale Order, check the Sale Orders associated to the Blanket Order and/or
|
||||
see the Blanket Order lines associated to the BO.
|
||||
|
||||
.. figure:: ../static/description/BO_actions.png
|
||||
:alt: Actions that can be done from Blanket Order
|
||||
|
||||
Hitting the button Create Sale Order will open a wizard that will ask for the amount of each
|
||||
product in the BO lines for which the Sale Order will be created.
|
||||
|
||||
.. figure:: ../static/description/PO_from_BO.png
|
||||
:alt: Create Sale Order from Blanket Order
|
||||
|
||||
Installing this module will add an additional menu which will show all the blanket order lines
|
||||
currently defined in the system. From this list the user can create customized Sale Orders
|
||||
selecting the lines for which the PO (or POs if the customers are different) is (are) created.
|
||||
|
||||
.. figure:: ../static/description/BO_lines.png
|
||||
:alt: Blanket Order lines and actions
|
||||
|
||||
In the Sale Order form one field is added in the PO lines, the Blanket Order line field. This
|
||||
field keeps track to which Blanket Order line the PO line is associated. Upon adding a new product
|
||||
in a newly created Sale Order a blanket order line will be suggested depending on the following
|
||||
factors:
|
||||
|
||||
* Closer Validity date
|
||||
* Remaining quantity > Quantity introduced in the Sale Order line
|
||||
|
||||
.. figure:: ../static/description/PO_BOLine.png
|
||||
:alt: New field added in Sale Order Line
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="report_blanket_order" model="ir.actions.report">
|
||||
<field name="name">Blanket Order</field>
|
||||
<field name="model">sale.blanket.order</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">sale_blanket_order.report_blanketorder</field>
|
||||
<field name="report_file">sale_blanket_order.report_blanketorder</field>
|
||||
<field name="print_report_name">'Blanket Order - %s' % object.name</field>
|
||||
<field
|
||||
name="binding_model_id"
|
||||
ref="sale_blanket_order.model_sale_blanket_order"
|
||||
/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<template id="report_blanketorder_document">
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="doc" t-value="doc.with_context({'lang':doc.partner_id.lang})" />
|
||||
<div class="page">
|
||||
<div class="oe_structure" />
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<div
|
||||
t-field="doc.partner_id"
|
||||
t-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": True, "phone_icons": True}'
|
||||
/>
|
||||
<p t-if="doc.partner_id.vat">VAT: <span
|
||||
t-field="doc.partner_id.vat"
|
||||
/></p>
|
||||
</div>
|
||||
</div>
|
||||
<h2>
|
||||
<span>Blanket Order # </span>
|
||||
<span t-field="doc.name" />
|
||||
</h2>
|
||||
<div class="row mt32 mb32" id="informations">
|
||||
<div t-if="doc.client_order_ref" class="col-3">
|
||||
<strong>Your Reference:</strong>
|
||||
<p t-field="doc.client_order_ref" />
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<strong>Validity Date:</strong>
|
||||
<p t-field="doc.validity_date" />
|
||||
</div>
|
||||
<div t-if="doc.user_id.name" class="col-3">
|
||||
<strong>Salesperson:</strong>
|
||||
<p t-field="doc.user_id" />
|
||||
</div>
|
||||
<div t-if="doc.currency_id" class="col-3">
|
||||
<strong>Currency:</strong>
|
||||
<p t-field="doc.currency_id" />
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-condensed table-borderless">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th class="text-end">Unit Price</th>
|
||||
<th class="text-center">Scheduled Date</th>
|
||||
<th class="text-end">Original Qty</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="sale_tbody">
|
||||
<t t-set="current_subtotal" t-value="0" />
|
||||
<t t-foreach="doc.line_ids" t-as="l">
|
||||
<t
|
||||
t-set="current_subtotal"
|
||||
t-value="current_subtotal + l.price_subtotal"
|
||||
groups="account.group_show_line_subtotals_tax_excluded"
|
||||
/>
|
||||
<t
|
||||
t-set="current_subtotal"
|
||||
t-value="current_subtotal + l.price_total"
|
||||
groups="account.group_show_line_subtotals_tax_included"
|
||||
/>
|
||||
<tr
|
||||
t-att-class="'bg-200 fw-bold o_line_section' if l.display_type == 'line_section' else 'fst-italic o_line_note' if l.display_type == 'line_note' else ''"
|
||||
>
|
||||
<t t-if="not l.display_type">
|
||||
<td>
|
||||
<span t-field="l.product_id" />
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-field="l.price_unit" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="l.date_schedule" />
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-field="l.original_uom_qty" />
|
||||
<span
|
||||
t-field="l.product_uom"
|
||||
groups="uom.group_uom"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span
|
||||
t-field="l.price_subtotal"
|
||||
t-options='{"widget": "monetary", "display_currency": l.currency_id}'
|
||||
/>
|
||||
</td>
|
||||
</t>
|
||||
<t t-if="l.display_type == 'line_section'">
|
||||
<td name="td_section_line" colspan="99">
|
||||
<span t-field="l.name" />
|
||||
</td>
|
||||
<t t-set="current_section" t-value="l" />
|
||||
<t t-set="current_subtotal" t-value="0" />
|
||||
</t>
|
||||
<t t-if="l.display_type == 'line_note'">
|
||||
<td name="td_note_line" colspan="99">
|
||||
<span t-field="l.name" />
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
<t
|
||||
t-if="current_section and (l_last or doc.line_ids[l_index+1].display_type == 'line_section')"
|
||||
>
|
||||
<tr class="is-subtotal text-end">
|
||||
<td name="td_section_subtotal" colspan="99">
|
||||
<strong class="mr16">Subtotal</strong>
|
||||
<span
|
||||
t-esc="current_subtotal"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.pricelist_id.currency_id}'
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="clearfix">
|
||||
<div class="row">
|
||||
<div
|
||||
t-attf-class="#{'col-6' if report_type != 'html' else 'col-sm-7 col-md-6'} ms-auto"
|
||||
>
|
||||
|
||||
|
||||
<table class="table table-sm">
|
||||
<tr class="border-black">
|
||||
<td>
|
||||
<strong>Subtotal</strong>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span
|
||||
t-field="doc.amount_untaxed"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.currency_id}'
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Taxes</td>
|
||||
<td class="text-end">
|
||||
<span
|
||||
t-field="doc.amount_tax"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.currency_id}'
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-black">
|
||||
<td>
|
||||
<strong>Total</strong>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span
|
||||
t-field="doc.amount_total"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.currency_id}'
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p t-field="doc.note" />
|
||||
<div class="oe_structure" />
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
<template id="report_blanketorder">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t
|
||||
t-call="sale_blanket_order.report_blanketorder_document"
|
||||
t-lang="doc.partner_id.lang"
|
||||
/>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_sale_blanket_order,sale.order,model_sale_blanket_order,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_sale_blanket_order_line,sale.order.line,model_sale_blanket_order_line,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_sale_blanket_order_manager,sale.order.manager,model_sale_blanket_order,sales_team.group_sale_manager,1,1,1,1
|
||||
access_sale_blanket_order_line_manager,sale.order.line.manager,model_sale_blanket_order_line,sales_team.group_sale_manager,1,1,1,1
|
||||
access_sale_blanket_order_accountant,sale.order.accountant,model_sale_blanket_order,account.group_account_user,1,1,0,0
|
||||
access_sale_blanket_order_line_accountant,sale.order.line accountant,model_sale_blanket_order_line,account.group_account_user,1,1,0,0
|
||||
access_sale_blanket_order_line_user,sale.order.line user,model_sale_blanket_order_line,base.group_user,1,0,0,0
|
||||
access_sale_blanket_order_wizard_user,sale.blanket.order.wizard user,model_sale_blanket_order_wizard,base.group_user,1,1,1,1
|
||||
access_sale_blanket_order_wizard_line_user,sale.blanket.order.wizard.line user,model_sale_blanket_order_wizard_line,base.group_user,1,1,1,1
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record model="res.groups" id="blanket_orders_disable_adding_lines">
|
||||
<field name="name">Disable adding more lines to SOs from Blanket Orders</field>
|
||||
</record>
|
||||
<!-- Multi - Company Rules -->
|
||||
<record model="ir.rule" id="blanket_order_comp_rule">
|
||||
<field name="name">Blanket Order multi-company</field>
|
||||
<field name="model_id" ref="model_sale_blanket_order" />
|
||||
<field name="global" eval="True" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
<record model="ir.rule" id="blanket_order_line_comp_rule">
|
||||
<field name="name">Blanket Order Line multi-company</field>
|
||||
<field name="model_id" ref="model_sale_blanket_order_line" />
|
||||
<field name="global" eval="True" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
|
@ -0,0 +1,512 @@
|
|||
<!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="sale-blanket-orders">
|
||||
<h1>Sale Blanket Orders</h1>
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:2304d65f97e02bd0fec986276cb77baf60ddd090d6bc436ae7004c8276b7c666
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<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/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/sale-workflow/tree/16.0/sale_blanket_order"><img alt="OCA/sale-workflow" src="https://img.shields.io/badge/github-OCA%2Fsale--workflow-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/sale-workflow-16-0/sale-workflow-16-0-sale_blanket_order"><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/sale-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>A blanket order is a pre-agreement to sell a certain number of quantities of
|
||||
products at a specific price. From a confirmed blanket order, the users can
|
||||
create new sale orders at such price, until the blanket order expires, either
|
||||
due to reaching the validity date or exhausting all the quantities of products.</p>
|
||||
<p><strong>Table of contents</strong></p>
|
||||
<div class="contents local topic" id="contents">
|
||||
<ul class="simple">
|
||||
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-2">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="toc-entry-3">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="toc-entry-4">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="toc-entry-5">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#other-credits" id="toc-entry-6">Other credits</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-7">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="usage">
|
||||
<h2><a class="toc-backref" href="#toc-entry-1">Usage</a></h2>
|
||||
<p>A new menu in the Sales area is created, allowing users to create new blanket orders.</p>
|
||||
<p>To create a new Sale Blanket Order go to the sale menu in the Sales section:</p>
|
||||
<div class="figure">
|
||||
<img alt="Blanket Orders menu" src="https://raw.githubusercontent.com/OCA/sale-workflow/16.0/sale_blanket_order/static/description/BO_menu.png" />
|
||||
</div>
|
||||
<p>Hitting the button create will open the form view in which we can introduce the following
|
||||
information:</p>
|
||||
<ul class="simple">
|
||||
<li>Vendor</li>
|
||||
<li>Salesperson</li>
|
||||
<li>Payment Terms</li>
|
||||
<li>Validity date</li>
|
||||
<li><dl class="first docutils">
|
||||
<dt>Order lines:</dt>
|
||||
<dd><ul class="first last">
|
||||
<li>Product</li>
|
||||
<li>Accorded price</li>
|
||||
<li>Original, Ordered, Invoiced, Received and Remaining quantities</li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</li>
|
||||
<li>Terms and Conditions of the Blanket Order</li>
|
||||
</ul>
|
||||
<div class="figure">
|
||||
<img alt="Blanket Orders form" src="https://raw.githubusercontent.com/OCA/sale-workflow/16.0/sale_blanket_order/static/description/BO_form.png" />
|
||||
</div>
|
||||
<p>From the form, once the Blanket Order has been confirmed and its state is open, the user can
|
||||
create a Sale Order, check the Sale Orders associated to the Blanket Order and/or
|
||||
see the Blanket Order lines associated to the BO.</p>
|
||||
<div class="figure">
|
||||
<img alt="Actions that can be done from Blanket Order" src="https://raw.githubusercontent.com/OCA/sale-workflow/16.0/sale_blanket_order/static/description/BO_actions.png" />
|
||||
</div>
|
||||
<p>Hitting the button Create Sale Order will open a wizard that will ask for the amount of each
|
||||
product in the BO lines for which the Sale Order will be created.</p>
|
||||
<div class="figure">
|
||||
<img alt="Create Sale Order from Blanket Order" src="https://raw.githubusercontent.com/OCA/sale-workflow/16.0/sale_blanket_order/static/description/PO_from_BO.png" />
|
||||
</div>
|
||||
<p>Installing this module will add an additional menu which will show all the blanket order lines
|
||||
currently defined in the system. From this list the user can create customized Sale Orders
|
||||
selecting the lines for which the PO (or POs if the customers are different) is (are) created.</p>
|
||||
<div class="figure">
|
||||
<img alt="Blanket Order lines and actions" src="https://raw.githubusercontent.com/OCA/sale-workflow/16.0/sale_blanket_order/static/description/BO_lines.png" />
|
||||
</div>
|
||||
<p>In the Sale Order form one field is added in the PO lines, the Blanket Order line field. This
|
||||
field keeps track to which Blanket Order line the PO line is associated. Upon adding a new product
|
||||
in a newly created Sale Order a blanket order line will be suggested depending on the following
|
||||
factors:</p>
|
||||
<ul class="simple">
|
||||
<li>Closer Validity date</li>
|
||||
<li>Remaining quantity > Quantity introduced in the Sale Order line</li>
|
||||
</ul>
|
||||
<div class="figure">
|
||||
<img alt="New field added in Sale Order Line" src="https://raw.githubusercontent.com/OCA/sale-workflow/16.0/sale_blanket_order/static/description/PO_BOLine.png" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h2><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h2>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/sale-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/sale-workflow/issues/new?body=module:%20sale_blanket_order%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-3">Credits</a></h2>
|
||||
<div class="section" id="authors">
|
||||
<h3><a class="toc-backref" href="#toc-entry-4">Authors</a></h3>
|
||||
<ul class="simple">
|
||||
<li>Acsone SA/NV</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="contributors">
|
||||
<h3><a class="toc-backref" href="#toc-entry-5">Contributors</a></h3>
|
||||
<ul>
|
||||
<li><p class="first">André Pereira <<a class="reference external" href="mailto:github@andreparames.com">github@andreparames.com</a>> (<a class="reference external" href="https://www.acsone.eu/">https://www.acsone.eu/</a>)</p>
|
||||
</li>
|
||||
<li><p class="first">Adrià Gil Sorribes <<a class="reference external" href="mailto:adria.gil@eficent.com">adria.gil@eficent.com</a>> (<a class="reference external" href="https://www.eficent.com/">https://www.eficent.com/</a>)</p>
|
||||
</li>
|
||||
<li><p class="first">Jordi Ballester Alomar <<a class="reference external" href="mailto:jordi.ballester@eficent.com">jordi.ballester@eficent.com</a>></p>
|
||||
</li>
|
||||
<li><p class="first">Alex Comba <<a class="reference external" href="mailto:alex.comba@agilebg.com">alex.comba@agilebg.com</a>> (<a class="reference external" href="https://www.agilebg.com/">https://www.agilebg.com/</a>)</p>
|
||||
</li>
|
||||
<li><p class="first">Jasper Jumelet <<a class="reference external" href="mailto:jasper.jumelet@codeforward.nl">jasper.jumelet@codeforward.nl</a>> (<a class="reference external" href="https://www.codeforward.nl/">https://www.codeforward.nl/</a>)</p>
|
||||
</li>
|
||||
<li><p class="first"><a class="reference external" href="https://trobz.com">Trobz</a>:</p>
|
||||
<blockquote>
|
||||
<ul class="simple">
|
||||
<li>Nguyễn Minh Chiến <<a class="reference external" href="mailto:chien@trobz.com">chien@trobz.com</a>></li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="other-credits">
|
||||
<h3><a class="toc-backref" href="#toc-entry-6">Other credits</a></h3>
|
||||
<p>The migration of this module from 15.0 to 16.0 was financially supported by Camptocamp</p>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h3><a class="toc-backref" href="#toc-entry-7">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/sale-workflow/tree/16.0/sale_blanket_order">OCA/sale-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>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import test_blanket_orders
|
||||
from . import test_sale_order
|
||||
|
|
@ -0,0 +1,426 @@
|
|||
# Copyright (C) 2018 Eficent Business and IT Consulting Services S.L.
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
from datetime import date, timedelta
|
||||
|
||||
from odoo import fields
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import common
|
||||
|
||||
|
||||
class TestSaleBlanketOrders(common.TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.blanket_order_obj = cls.env["sale.blanket.order"]
|
||||
cls.blanket_order_line_obj = cls.env["sale.blanket.order.line"]
|
||||
cls.blanket_order_wiz_obj = cls.env["sale.blanket.order.wizard"]
|
||||
cls.so_obj = cls.env["sale.order"]
|
||||
|
||||
cls.payment_term = cls.env.ref("account.account_payment_term_immediate")
|
||||
cls.sale_pricelist = cls.env["product.pricelist"].create(
|
||||
{"name": "Test Pricelist", "currency_id": cls.env.ref("base.USD").id}
|
||||
)
|
||||
|
||||
# UoM
|
||||
cls.categ_unit = cls.env.ref("uom.product_uom_categ_unit")
|
||||
cls.uom_dozen = cls.env["uom.uom"].create(
|
||||
{
|
||||
"name": "Test-DozenA",
|
||||
"category_id": cls.categ_unit.id,
|
||||
"factor_inv": 12,
|
||||
"uom_type": "bigger",
|
||||
"rounding": 0.001,
|
||||
}
|
||||
)
|
||||
|
||||
cls.partner = cls.env["res.partner"].create(
|
||||
{
|
||||
"name": "TEST CUSTOMER",
|
||||
"property_product_pricelist": cls.sale_pricelist.id,
|
||||
}
|
||||
)
|
||||
|
||||
cls.product = cls.env["product.product"].create(
|
||||
{
|
||||
"name": "Demo",
|
||||
"categ_id": cls.env.ref("product.product_category_1").id,
|
||||
"standard_price": 35.0,
|
||||
"type": "consu",
|
||||
"uom_id": cls.env.ref("uom.product_uom_unit").id,
|
||||
"default_code": "PROD_DEL01",
|
||||
}
|
||||
)
|
||||
cls.product2 = cls.env["product.product"].create(
|
||||
{
|
||||
"name": "Demo 2",
|
||||
"categ_id": cls.env.ref("product.product_category_1").id,
|
||||
"standard_price": 50.0,
|
||||
"type": "consu",
|
||||
"uom_id": cls.env.ref("uom.product_uom_unit").id,
|
||||
"default_code": "PROD_DEL02",
|
||||
}
|
||||
)
|
||||
|
||||
cls.yesterday = date.today() - timedelta(days=1)
|
||||
cls.tomorrow = date.today() + timedelta(days=1)
|
||||
|
||||
def test_01_create_blanket_order(self):
|
||||
"""We create a blanket order and check constrains to confirm BO"""
|
||||
blanket_order = self.blanket_order_obj.create(
|
||||
{
|
||||
"partner_id": self.partner.id,
|
||||
"validity_date": fields.Date.to_string(self.yesterday),
|
||||
"payment_term_id": self.payment_term.id,
|
||||
"pricelist_id": self.sale_pricelist.id,
|
||||
"line_ids": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product.id,
|
||||
"product_uom": self.product.uom_id.id,
|
||||
"original_uom_qty": 20.0,
|
||||
"price_unit": 0.0, # will be updated later
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "My section",
|
||||
"display_type": "line_section",
|
||||
},
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
blanket_order.sudo().onchange_partner_id()
|
||||
blanket_order.pricelist_id.discount_policy = "without_discount"
|
||||
blanket_order.line_ids[0].sudo().onchange_product()
|
||||
blanket_order.pricelist_id.discount_policy = "with_discount"
|
||||
blanket_order.line_ids[0].sudo().onchange_product()
|
||||
blanket_order.line_ids[0].sudo()._get_display_price(self.product)
|
||||
|
||||
self.assertEqual(blanket_order.state, "draft")
|
||||
|
||||
# date in the past
|
||||
with self.assertRaises(UserError):
|
||||
blanket_order.sudo().action_confirm()
|
||||
|
||||
blanket_order.validity_date = fields.Date.to_string(self.tomorrow)
|
||||
blanket_order.sudo().action_confirm()
|
||||
self.assertEqual(blanket_order.state, "open")
|
||||
|
||||
blanket_order.sudo().action_cancel()
|
||||
self.assertEqual(blanket_order.state, "expired")
|
||||
|
||||
blanket_order.sudo().set_to_draft()
|
||||
self.assertEqual(blanket_order.state, "draft")
|
||||
|
||||
blanket_order.sudo().action_confirm()
|
||||
|
||||
def test_02_create_sale_orders_from_blanket_order(self):
|
||||
"""We create a blanket order and create two sale orders"""
|
||||
blanket_order = self.blanket_order_obj.create(
|
||||
{
|
||||
"partner_id": self.partner.id,
|
||||
"validity_date": fields.Date.to_string(self.tomorrow),
|
||||
"payment_term_id": self.payment_term.id,
|
||||
"pricelist_id": self.sale_pricelist.id,
|
||||
"line_ids": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": False,
|
||||
"product_uom": False,
|
||||
"name": "My section",
|
||||
"display_type": "line_section",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product.id,
|
||||
"product_uom": self.product.uom_id.id,
|
||||
"original_uom_qty": 20.0,
|
||||
"price_unit": 30.0,
|
||||
},
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
blanket_order.sudo().onchange_partner_id()
|
||||
blanket_order.sudo().action_confirm()
|
||||
|
||||
wizard1 = self.blanket_order_wiz_obj.with_context(
|
||||
active_id=blanket_order.id, active_model="sale.blanket.order"
|
||||
).create({})
|
||||
wizard1.line_ids[0].write({"qty": 10.0})
|
||||
wizard1.sudo().create_sale_order()
|
||||
|
||||
wizard2 = self.blanket_order_wiz_obj.with_context(
|
||||
active_id=blanket_order.id, active_model="sale.blanket.order"
|
||||
).create({})
|
||||
wizard2.line_ids[0].write({"qty": 10.0})
|
||||
wizard2.sudo().create_sale_order()
|
||||
|
||||
self.assertEqual(blanket_order.state, "done")
|
||||
|
||||
self.assertEqual(blanket_order.sale_count, 2)
|
||||
|
||||
view_action = blanket_order.action_view_sale_orders()
|
||||
domain_ids = view_action["domain"][0][2]
|
||||
self.assertEqual(len(domain_ids), 2)
|
||||
|
||||
sos = self.so_obj.browse(domain_ids)
|
||||
for so in sos:
|
||||
self.assertEqual(so.origin, blanket_order.name)
|
||||
|
||||
def test_03_create_sale_orders_from_blanket_order_line(self):
|
||||
"""We create a blanket order and create two sale orders
|
||||
from the blanket order lines"""
|
||||
blanket_order = self.blanket_order_obj.create(
|
||||
{
|
||||
"partner_id": self.partner.id,
|
||||
"validity_date": fields.Date.to_string(self.tomorrow),
|
||||
"payment_term_id": self.payment_term.id,
|
||||
"pricelist_id": self.sale_pricelist.id,
|
||||
"line_ids": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product.id,
|
||||
"product_uom": self.product.uom_id.id,
|
||||
"original_uom_qty": 20.0,
|
||||
"price_unit": 30.0,
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product2.id,
|
||||
"product_uom": self.product2.uom_id.id,
|
||||
"original_uom_qty": 50.0,
|
||||
"price_unit": 60.0,
|
||||
},
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
blanket_order.sudo().onchange_partner_id()
|
||||
blanket_order.sudo().action_confirm()
|
||||
bo_lines = blanket_order.line_ids
|
||||
self.assertEqual(len(bo_lines), 2)
|
||||
|
||||
wizard1 = self.blanket_order_wiz_obj.with_context(
|
||||
active_ids=[bo_lines[0].id, bo_lines[1].id]
|
||||
).create({})
|
||||
self.assertEqual(len(wizard1.line_ids), 2)
|
||||
wizard1.line_ids[0].write({"qty": 10.0})
|
||||
wizard1.line_ids[1].write({"qty": 20.0})
|
||||
wizard1.sudo().create_sale_order()
|
||||
|
||||
self.assertEqual(bo_lines[0].remaining_uom_qty, 10.0)
|
||||
self.assertEqual(bo_lines[1].remaining_uom_qty, 30.0)
|
||||
|
||||
def test_04_create_sale_order_add_blanket_order_line(self):
|
||||
"""We create a blanket order and the separately we create
|
||||
a sale order and see if blanket order lines have been
|
||||
correctly assigned"""
|
||||
blanket_order = self.blanket_order_obj.create(
|
||||
{
|
||||
"partner_id": self.partner.id,
|
||||
"validity_date": fields.Date.to_string(self.tomorrow),
|
||||
"payment_term_id": self.payment_term.id,
|
||||
"pricelist_id": self.sale_pricelist.id,
|
||||
"currency_id": self.sale_pricelist.currency_id.id,
|
||||
"line_ids": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product.id,
|
||||
"product_uom": self.product.uom_id.id,
|
||||
"original_uom_qty": 20.0,
|
||||
"price_unit": 30.0,
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product2.id,
|
||||
"product_uom": self.product2.uom_id.id,
|
||||
"original_uom_qty": 50.0,
|
||||
"price_unit": 60.0,
|
||||
},
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
blanket_order.sudo().onchange_partner_id()
|
||||
blanket_order.sudo().action_confirm()
|
||||
|
||||
bo_lines = blanket_order.line_ids
|
||||
|
||||
sale_order = self.so_obj.create(
|
||||
{
|
||||
"partner_id": self.partner.id,
|
||||
"payment_term_id": self.payment_term.id,
|
||||
"pricelist_id": self.sale_pricelist.id,
|
||||
"order_line": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product.id,
|
||||
"product_uom": self.product.uom_id.id,
|
||||
"product_uom_qty": 10.0,
|
||||
"price_unit": 30.0,
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product2.id,
|
||||
"product_uom": self.product2.uom_id.id,
|
||||
"product_uom_qty": 50.0,
|
||||
"price_unit": 60.0,
|
||||
},
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
sale_order.order_line[0].onchange_product_id()
|
||||
self.assertEqual(bo_lines[0].remaining_uom_qty, 10.0)
|
||||
|
||||
def test_05_create_sale_order_blanket_order_with_different_uom(self):
|
||||
"""We create a blanket order and the separately we create
|
||||
a sale order with different uom and see if blanket order
|
||||
lines have been correctly assigned"""
|
||||
blanket_order = self.blanket_order_obj.create(
|
||||
{
|
||||
"partner_id": self.partner.id,
|
||||
"validity_date": fields.Date.to_string(self.tomorrow),
|
||||
"payment_term_id": self.payment_term.id,
|
||||
"pricelist_id": self.sale_pricelist.id,
|
||||
"line_ids": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product.id,
|
||||
"product_uom": self.uom_dozen.id,
|
||||
"original_uom_qty": 2.0,
|
||||
"price_unit": 240.0,
|
||||
},
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
blanket_order.sudo().onchange_partner_id()
|
||||
blanket_order.sudo().action_confirm()
|
||||
|
||||
sale_order = self.so_obj.create(
|
||||
{
|
||||
"partner_id": self.partner.id,
|
||||
"payment_term_id": self.payment_term.id,
|
||||
"pricelist_id": self.sale_pricelist.id,
|
||||
"order_line": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product.id,
|
||||
"product_uom": self.product.uom_id.id,
|
||||
"product_uom_qty": 12.0,
|
||||
"price_unit": 30.0,
|
||||
},
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
sale_order.order_line[0].onchange_product_id()
|
||||
sale_order.order_line[0].onchange_blanket_order_line()
|
||||
self.assertEqual(blanket_order.line_ids[0].remaining_qty, 12.0)
|
||||
self.assertEqual(sale_order.order_line[0].price_unit, 20.0)
|
||||
|
||||
def test_06_create_sale_orders_from_blanket_order(self):
|
||||
"""We create a blanket order and create three sale orders
|
||||
where the first two consume the first blanket order line
|
||||
"""
|
||||
blanket_order = self.blanket_order_obj.create(
|
||||
{
|
||||
"partner_id": self.partner.id,
|
||||
"validity_date": fields.Date.to_string(self.tomorrow),
|
||||
"payment_term_id": self.payment_term.id,
|
||||
"pricelist_id": self.sale_pricelist.id,
|
||||
"line_ids": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product.id,
|
||||
"product_uom": self.product.uom_id.id,
|
||||
"original_uom_qty": 30.0,
|
||||
"price_unit": 30.0,
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product2.id,
|
||||
"product_uom": self.product2.uom_id.id,
|
||||
"original_uom_qty": 20.0,
|
||||
"price_unit": 60.0,
|
||||
},
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
blanket_order.sudo().onchange_partner_id()
|
||||
blanket_order.sudo().action_confirm()
|
||||
|
||||
wizard1 = self.blanket_order_wiz_obj.with_context(
|
||||
active_id=blanket_order.id, active_model="sale.blanket.order"
|
||||
).create({})
|
||||
wizard1.line_ids.filtered(lambda l: l.product_id == self.product).write(
|
||||
{"qty": 10.0}
|
||||
)
|
||||
wizard1.line_ids.filtered(lambda l: l.product_id == self.product2).write(
|
||||
{"qty": 10.0}
|
||||
)
|
||||
wizard1.sudo().create_sale_order()
|
||||
|
||||
wizard2 = self.blanket_order_wiz_obj.with_context(
|
||||
active_id=blanket_order.id, active_model="sale.blanket.order"
|
||||
).create({})
|
||||
wizard2.line_ids.filtered(lambda l: l.product_id == self.product).write(
|
||||
{"qty": 20.0}
|
||||
)
|
||||
wizard2.line_ids.filtered(lambda l: l.product_id == self.product2).write(
|
||||
{"qty": 0}
|
||||
)
|
||||
wizard2.sudo().create_sale_order()
|
||||
|
||||
wizard3 = self.blanket_order_wiz_obj.with_context(
|
||||
active_id=blanket_order.id, active_model="sale.blanket.order"
|
||||
).create({})
|
||||
wizard3.line_ids.filtered(lambda l: l.product_id == self.product2).write(
|
||||
{"qty": 10.0}
|
||||
)
|
||||
wizard3.sudo().create_sale_order()
|
||||
|
||||
self.assertEqual(blanket_order.state, "done")
|
||||
|
||||
self.assertEqual(blanket_order.sale_count, 3)
|
||||
|
||||
view_action = blanket_order.action_view_sale_orders()
|
||||
domain_ids = view_action["domain"][0][2]
|
||||
self.assertEqual(len(domain_ids), 3)
|
||||
|
|
@ -0,0 +1,255 @@
|
|||
# Copyright (C) 2018 Eficent Business and IT Consulting Services S.L.
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
from datetime import date, timedelta
|
||||
|
||||
from odoo import fields
|
||||
from odoo.tests import common
|
||||
|
||||
|
||||
class TestSaleOrder(common.TransactionCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.blanket_order_obj = self.env["sale.blanket.order"]
|
||||
self.blanket_order_line_obj = self.env["sale.blanket.order.line"]
|
||||
self.sale_order_obj = self.env["sale.order"]
|
||||
self.sale_order_line_obj = self.env["sale.order.line"]
|
||||
|
||||
self.partner = self.env["res.partner"].create({"name": "TEST CUSTOMER"})
|
||||
self.payment_term = self.env.ref("account.account_payment_term_immediate")
|
||||
self.sale_pricelist = self.env["product.pricelist"].create(
|
||||
{"name": "Test Pricelist", "currency_id": self.env.ref("base.USD").id}
|
||||
)
|
||||
|
||||
self.product = self.env["product.product"].create(
|
||||
{
|
||||
"name": "Demo",
|
||||
"categ_id": self.env.ref("product.product_category_1").id,
|
||||
"standard_price": 40.0,
|
||||
"type": "consu",
|
||||
"uom_id": self.env.ref("uom.product_uom_unit").id,
|
||||
"default_code": "PROD_DEL01",
|
||||
}
|
||||
)
|
||||
self.product_2 = self.env["product.product"].create(
|
||||
{
|
||||
"name": "Demo 2",
|
||||
"categ_id": self.env.ref("product.product_category_1").id,
|
||||
"standard_price": 35.0,
|
||||
"type": "consu",
|
||||
"uom_id": self.env.ref("uom.product_uom_unit").id,
|
||||
"default_code": "PROD_DEL02",
|
||||
}
|
||||
)
|
||||
self.validity = date.today() + timedelta(days=365)
|
||||
self.date_schedule_1 = date.today() + timedelta(days=10)
|
||||
self.date_schedule_2 = date.today() + timedelta(days=20)
|
||||
|
||||
def create_blanket_order_01(self):
|
||||
blanket_order = self.blanket_order_obj.create(
|
||||
{
|
||||
"partner_id": self.partner.id,
|
||||
"validity_date": fields.Date.to_string(self.validity),
|
||||
"payment_term_id": self.payment_term.id,
|
||||
"pricelist_id": self.sale_pricelist.id,
|
||||
"line_ids": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product.id,
|
||||
"product_uom": self.product.uom_id.id,
|
||||
"date_schedule": fields.Date.to_string(
|
||||
self.date_schedule_1
|
||||
),
|
||||
"original_uom_qty": 20.0,
|
||||
"price_unit": 30.0,
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product.id,
|
||||
"product_uom": self.product.uom_id.id,
|
||||
"date_schedule": fields.Date.to_string(
|
||||
self.date_schedule_2
|
||||
),
|
||||
"original_uom_qty": 20.0,
|
||||
"price_unit": 30.0,
|
||||
},
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
blanket_order.sudo().onchange_partner_id()
|
||||
return blanket_order
|
||||
|
||||
def create_blanket_order_02(self):
|
||||
blanket_order = self.blanket_order_obj.create(
|
||||
{
|
||||
"partner_id": self.partner.id,
|
||||
"validity_date": fields.Date.to_string(self.validity),
|
||||
"payment_term_id": self.payment_term.id,
|
||||
"pricelist_id": self.sale_pricelist.id,
|
||||
"line_ids": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product.id,
|
||||
"product_uom": self.product.uom_id.id,
|
||||
"original_uom_qty": 20.0,
|
||||
"price_unit": 30.0,
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product_2.id,
|
||||
"product_uom": self.product.uom_id.id,
|
||||
"original_uom_qty": 20.0,
|
||||
"price_unit": 30.0,
|
||||
},
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
blanket_order.sudo().onchange_partner_id()
|
||||
return blanket_order
|
||||
|
||||
def test_01_create_sale_order(self):
|
||||
blanket_order = self.create_blanket_order_01()
|
||||
blanket_order.sudo().action_confirm()
|
||||
bo_lines = self.blanket_order_line_obj.search(
|
||||
[("order_id", "=", blanket_order.id)]
|
||||
)
|
||||
self.assertEqual(len(bo_lines), 2)
|
||||
|
||||
so = self.sale_order_obj.create(
|
||||
{
|
||||
"partner_id": self.partner.id,
|
||||
"order_line": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": self.product.name,
|
||||
"product_id": self.product.id,
|
||||
"product_uom_qty": 5.0,
|
||||
"product_uom": self.product.uom_po_id.id,
|
||||
"price_unit": 10.0,
|
||||
},
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
so_line = so.order_line[0]
|
||||
so_line.with_context(from_sale_order=True).name_get()
|
||||
so_line.onchange_product_id()
|
||||
self.assertEqual(so_line._get_eligible_bo_lines(), bo_lines)
|
||||
bo_line_assigned = self.blanket_order_line_obj.search(
|
||||
[("date_schedule", "=", fields.Date.to_string(self.date_schedule_1))]
|
||||
)
|
||||
self.assertEqual(so_line.blanket_order_line, bo_line_assigned)
|
||||
|
||||
def test_02_create_sale_order(self):
|
||||
blanket_order = self.create_blanket_order_02()
|
||||
blanket_order.sudo().action_confirm()
|
||||
bo_lines = self.blanket_order_line_obj.search(
|
||||
[("order_id", "=", blanket_order.id)]
|
||||
)
|
||||
self.assertEqual(len(bo_lines), 2)
|
||||
|
||||
so = self.sale_order_obj.create(
|
||||
{
|
||||
"partner_id": self.partner.id,
|
||||
"order_line": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": self.product.name,
|
||||
"product_id": self.product.id,
|
||||
"product_uom_qty": 5.0,
|
||||
"product_uom": self.product.uom_po_id.id,
|
||||
"price_unit": 10.0,
|
||||
},
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
so_line = so.order_line[0]
|
||||
so_line.with_context(from_sale_order=True).name_get()
|
||||
so_line.onchange_product_id()
|
||||
self.assertEqual(
|
||||
so_line._get_eligible_bo_lines(),
|
||||
bo_lines.filtered(lambda l: l.product_id == self.product),
|
||||
)
|
||||
bo_line_assigned = self.blanket_order_line_obj.search(
|
||||
[
|
||||
("order_id", "=", blanket_order.id),
|
||||
("product_id", "=", self.product.id),
|
||||
("date_schedule", "=", False),
|
||||
]
|
||||
)
|
||||
self.assertEqual(so_line.blanket_order_line, bo_line_assigned)
|
||||
|
||||
def test_03_create_sale_order(self):
|
||||
blanket_order = self.create_blanket_order_01()
|
||||
blanket_order.sudo().action_confirm()
|
||||
bo_lines = self.blanket_order_line_obj.search(
|
||||
[("order_id", "=", blanket_order.id)]
|
||||
)
|
||||
self.assertEqual(len(bo_lines), 2)
|
||||
|
||||
so = self.sale_order_obj.create(
|
||||
{
|
||||
"partner_id": self.partner.id,
|
||||
"order_line": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": self.product_2.name,
|
||||
"product_id": self.product_2.id,
|
||||
"product_uom_qty": 5.0,
|
||||
"product_uom": self.product_2.uom_po_id.id,
|
||||
"price_unit": 10.0,
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": self.product.name,
|
||||
"product_id": self.product.id,
|
||||
"product_uom_qty": 5.0,
|
||||
"product_uom": self.product.uom_po_id.id,
|
||||
"price_unit": 10.0,
|
||||
},
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
so_line_1 = so.order_line[0]
|
||||
so_line_2 = so.order_line[1]
|
||||
so_line_1.with_context(from_sale_order=True).name_get()
|
||||
so_line_1.onchange_product_id()
|
||||
self.assertFalse(so_line_1._get_eligible_bo_lines())
|
||||
so_line_2.with_context(from_sale_order=True).name_get()
|
||||
so_line_2.onchange_product_id()
|
||||
self.assertEqual(
|
||||
so_line_2._get_eligible_bo_lines(),
|
||||
bo_lines.filtered(lambda l: l.product_id == self.product),
|
||||
)
|
||||
bo_line_assigned = self.blanket_order_line_obj.search(
|
||||
[
|
||||
("order_id", "=", blanket_order.id),
|
||||
("product_id", "=", self.product.id),
|
||||
("date_schedule", "=", fields.Date.to_string(self.date_schedule_1)),
|
||||
]
|
||||
)
|
||||
self.assertFalse(so_line_1.blanket_order_line)
|
||||
self.assertEqual(so_line_2.blanket_order_line, bo_line_assigned)
|
||||
self.assertEqual(so.blanket_order_id, blanket_order)
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<!-- VIEWS -->
|
||||
|
||||
<record id="sale_blanket_order_line_tree" model="ir.ui.view">
|
||||
<field name="name">sale.blanket.order.line.tree</field>
|
||||
<field name="model">sale.blanket.order.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree create="false">
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="name" invisible="1" />
|
||||
<field name="order_id" />
|
||||
<field
|
||||
name="product_id"
|
||||
context="{'partner_id':parent.partner_id, 'quantity':original_uom_qty, 'company_id': company_id}"
|
||||
/>
|
||||
<field name="product_uom" invisible="1" />
|
||||
<field name="price_unit" />
|
||||
<field name="date_schedule" />
|
||||
<field
|
||||
name="original_uom_qty"
|
||||
string="Original Qty"
|
||||
context="{'partner_id':parent.partner_id, 'quantity':original_uom_qty, 'company_id': company_id}"
|
||||
/>
|
||||
<field name="ordered_uom_qty" />
|
||||
<field name="invoiced_uom_qty" />
|
||||
<field name="delivered_uom_qty" />
|
||||
<field name="remaining_uom_qty" />
|
||||
<field name="company_id" invisible="1" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_blanket_order_line_form" model="ir.ui.view">
|
||||
<field name="name">sale.blanket.order.line.form</field>
|
||||
<field name="model">sale.blanket.order.line</field>
|
||||
<field name="priority" eval="20" />
|
||||
<field name="arch" type="xml">
|
||||
<form string="Sale Blanket Order Line" duplicate="false">
|
||||
<sheet>
|
||||
<h1>
|
||||
<field name="order_id" />
|
||||
</h1>
|
||||
<group>
|
||||
<group>
|
||||
<field name="product_id" readonly="1" />
|
||||
<field name="price_unit" readonly="1" />
|
||||
<field name="partner_id" readonly="1" />
|
||||
<field name="date_schedule" readonly="1" />
|
||||
<field
|
||||
name="company_id"
|
||||
groups="base.group_multi_company"
|
||||
optional="show"
|
||||
options="{'no_create': True}"
|
||||
/>
|
||||
</group>
|
||||
<group>
|
||||
<label for="original_uom_qty" />
|
||||
<div>
|
||||
<field
|
||||
name="original_uom_qty"
|
||||
class="oe_inline"
|
||||
readonly="1"
|
||||
/>
|
||||
<field
|
||||
name="product_uom"
|
||||
groups="uom.group_uom"
|
||||
class="oe_inline"
|
||||
readonly="1"
|
||||
/>
|
||||
</div>
|
||||
<label for="ordered_uom_qty" />
|
||||
<div>
|
||||
<field
|
||||
name="ordered_uom_qty"
|
||||
class="oe_inline"
|
||||
readonly="1"
|
||||
/>
|
||||
<field
|
||||
name="product_uom"
|
||||
groups="uom.group_uom"
|
||||
class="oe_inline"
|
||||
readonly="1"
|
||||
/>
|
||||
</div>
|
||||
<label for="invoiced_uom_qty" />
|
||||
<div>
|
||||
<field
|
||||
name="invoiced_uom_qty"
|
||||
class="oe_inline"
|
||||
readonly="1"
|
||||
/>
|
||||
<field
|
||||
name="product_uom"
|
||||
groups="uom.group_uom"
|
||||
class="oe_inline"
|
||||
readonly="1"
|
||||
/>
|
||||
</div>
|
||||
<label for="remaining_uom_qty" />
|
||||
<div>
|
||||
<field
|
||||
name="remaining_uom_qty"
|
||||
class="oe_inline"
|
||||
readonly="1"
|
||||
/>
|
||||
<field
|
||||
name="product_uom"
|
||||
groups="uom.group_uom"
|
||||
class="oe_inline"
|
||||
readonly="1"
|
||||
/>
|
||||
</div>
|
||||
<label for="remaining_qty" />
|
||||
<div>
|
||||
<field
|
||||
name="remaining_qty"
|
||||
class="oe_inline"
|
||||
readonly="1"
|
||||
/>
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page name="sale_lines" string="Sale Order Lines">
|
||||
<field
|
||||
name="sale_lines"
|
||||
mode="tree"
|
||||
readonly="1"
|
||||
domain="[('product_id', '=', product_id)]"
|
||||
/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" widget="mail_followers" />
|
||||
<field name="activity_ids" widget="mail_activity" />
|
||||
<field name="message_ids" widget="mail_thread" />
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_blanket_order_line_search" model="ir.ui.view">
|
||||
<field name="name">sale.blanket.order.line.search</field>
|
||||
<field name="model">sale.blanket.order.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Sale Blanket Order Line">
|
||||
<field name="order_id" />
|
||||
<field name="product_id" />
|
||||
<field name="date_schedule" />
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ACTIONS (SERVER) -->
|
||||
|
||||
<record
|
||||
id="act_open_sale_blanket_order_lines_view_tree"
|
||||
model="ir.actions.act_window"
|
||||
>
|
||||
<field name="name">Blanket Order Lines</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">sale.blanket.order.line</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="search_view_id" ref="sale_blanket_order_line_search" />
|
||||
</record>
|
||||
|
||||
<!-- MENU'S -->
|
||||
|
||||
<menuitem
|
||||
action="act_open_sale_blanket_order_lines_view_tree"
|
||||
id="menu_sale_blanket_order_line"
|
||||
groups="sales_team.group_sale_salesman"
|
||||
sequence="21"
|
||||
parent="sale.sale_order_menu"
|
||||
/>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,382 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<!-- VIEWS -->
|
||||
|
||||
<record id="view_blanket_order_tree" model="ir.ui.view">
|
||||
<field name="name">sale.blanket.order.tree</field>
|
||||
<field name="model">sale.blanket.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree
|
||||
decoration-info="state == 'draft'"
|
||||
decoration-muted="state == 'expired'"
|
||||
>
|
||||
<field name="name" />
|
||||
<field name="user_id" />
|
||||
<field name="partner_id" />
|
||||
<field name="validity_date" />
|
||||
<field name="state" />
|
||||
<field
|
||||
name="company_id"
|
||||
groups="base.group_multi_company"
|
||||
optional="show"
|
||||
readonly="1"
|
||||
/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_blanket_order_form" model="ir.ui.view">
|
||||
<field name="name">sale.blanket.order.form</field>
|
||||
<field name="model">sale.blanket.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Blanket Order" class="o_sale_order">
|
||||
<header>
|
||||
<button
|
||||
name="%(action_create_sale_order)d"
|
||||
string="Create Sale Order"
|
||||
type="action"
|
||||
class="btn-primary"
|
||||
attrs="{'invisible': [('state', '!=', 'open')]}"
|
||||
/>
|
||||
<button
|
||||
name="set_to_draft"
|
||||
states="expired,done"
|
||||
string="To Draft"
|
||||
class="btn-secondary"
|
||||
type="object"
|
||||
/>
|
||||
<button
|
||||
name="action_cancel"
|
||||
states="open"
|
||||
string="Cancel"
|
||||
class="btn-secondary"
|
||||
type="object"
|
||||
/>
|
||||
<button
|
||||
name="action_confirm"
|
||||
states="draft"
|
||||
string="Confirm"
|
||||
class="btn-primary o_sale_confirm"
|
||||
type="object"
|
||||
/>
|
||||
<field
|
||||
name="state"
|
||||
widget="statusbar"
|
||||
statusbar_visible="draft,open,done,expired"
|
||||
/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button
|
||||
name="action_view_sale_orders"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-list-alt"
|
||||
>
|
||||
<field
|
||||
name="sale_count"
|
||||
widget="statinfo"
|
||||
string="Sale Orders"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="object"
|
||||
name="action_view_sale_blanket_order_line"
|
||||
class="oe_stat_button"
|
||||
attrs="{'invisible':[('line_count', '=', 0)]}"
|
||||
icon="fa-list"
|
||||
>
|
||||
<field name="line_count" widget="statinfo" string="Lines" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" readonly="1" />
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group name="group_left">
|
||||
<field
|
||||
name="partner_id"
|
||||
context="{'res_partner_search_mode': 'customer', 'show_address': 1}"
|
||||
attrs="{'required': [('state', '=', 'draft')]}"
|
||||
options="{"always_reload": True}"
|
||||
/>
|
||||
<field name="payment_term_id" />
|
||||
</group>
|
||||
<group name="group_right">
|
||||
<field
|
||||
name="currency_id"
|
||||
groups="base.group_multi_currency"
|
||||
options="{'no_create': True}"
|
||||
/>
|
||||
<field name="company_id" invisible="1" />
|
||||
<field
|
||||
name="validity_date"
|
||||
attrs="{'required': [('state', '=', 'draft')]}"
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Order Lines" name="order_lines">
|
||||
<field
|
||||
name="line_ids"
|
||||
attrs="{'readonly': [('state', 'in', ('open','done','expired'))]}"
|
||||
widget="section_and_note_one2many"
|
||||
>
|
||||
<tree editable="bottom">
|
||||
<control>
|
||||
<create
|
||||
name="add_product_control"
|
||||
string="Add a product"
|
||||
/>
|
||||
<create
|
||||
name="add_section_control"
|
||||
string="Add a section"
|
||||
context="{'default_display_type': 'line_section'}"
|
||||
/>
|
||||
<create
|
||||
name="add_note_control"
|
||||
string="Add a note"
|
||||
context="{'default_display_type': 'line_note'}"
|
||||
/>
|
||||
</control>
|
||||
<field name="display_type" invisible="1" />
|
||||
<field name="sequence" widget="handle" />
|
||||
<field
|
||||
name="product_id"
|
||||
attrs="{
|
||||
'required': [('display_type', '=', False)],
|
||||
'invisible': [('display_type', '=', True)],
|
||||
}"
|
||||
context="{
|
||||
'fld': 'product_id',
|
||||
'partner_id': parent.partner_id,
|
||||
'quantity': original_uom_qty,
|
||||
'company_id': parent.company_id
|
||||
}"
|
||||
/>
|
||||
<field
|
||||
name="name"
|
||||
widget="section_and_note_text"
|
||||
optional="show"
|
||||
/>
|
||||
<field
|
||||
name="analytic_distribution"
|
||||
widget="analytic_distribution"
|
||||
optional="hide"
|
||||
groups="analytic.group_analytic_accounting"
|
||||
options="{
|
||||
'product_field': 'product_id',
|
||||
'business_domain': 'sale_order'
|
||||
}"
|
||||
/>
|
||||
<field
|
||||
name="original_uom_qty"
|
||||
string="Original Qty"
|
||||
context="{
|
||||
'fld': 'original_uom_qty',
|
||||
'partner_id': parent.partner_id,
|
||||
'quantity': original_uom_qty,
|
||||
'company_id': parent.company_id
|
||||
}"
|
||||
attrs="{
|
||||
'required': [('display_type', '=', False)],
|
||||
'invisible': [('display_type', '=', True)],
|
||||
}"
|
||||
/>
|
||||
<field
|
||||
name="product_uom"
|
||||
invisible="1"
|
||||
groups="!uom.group_uom"
|
||||
/>
|
||||
<field
|
||||
name="product_uom"
|
||||
groups="uom.group_uom"
|
||||
optional="show"
|
||||
/>
|
||||
<field
|
||||
name="price_unit"
|
||||
attrs="{
|
||||
'required': [('display_type', '=', False)],
|
||||
'invisible': [('display_type', '=', True)],
|
||||
}"
|
||||
/>
|
||||
<field name="date_schedule" />
|
||||
<field name="ordered_uom_qty" optional="hide" />
|
||||
<field name="invoiced_uom_qty" optional="hide" />
|
||||
<field name="delivered_uom_qty" optional="hide" />
|
||||
<field name="remaining_uom_qty" optional="hide" />
|
||||
<field
|
||||
name="taxes_id"
|
||||
widget="many2many_tags"
|
||||
domain="[('type_tax_use','=','sale')]"
|
||||
context="{'default_type_tax_use': 'sale'}"
|
||||
options="{'no_create': True}"
|
||||
attrs="{
|
||||
'invisible': [('display_type', '=', True)],
|
||||
}"
|
||||
/>
|
||||
<field
|
||||
name="price_subtotal"
|
||||
widget="monetary"
|
||||
attrs="{
|
||||
'invisible': [('display_type', '=', True)],
|
||||
}"
|
||||
/>
|
||||
<field name="company_id" invisible="1" />
|
||||
</tree>
|
||||
</field>
|
||||
<group class="oe_subtotal_footer oe_right">
|
||||
<field
|
||||
name="amount_untaxed"
|
||||
widget="monetary"
|
||||
options="{'currency_field': 'currency_id'}"
|
||||
/>
|
||||
<field
|
||||
name="amount_tax"
|
||||
widget="monetary"
|
||||
options="{'currency_field': 'currency_id'}"
|
||||
/>
|
||||
<label for="amount_total" />
|
||||
<div class="oe_subtotal_footer_separator">
|
||||
<field
|
||||
name="amount_total"
|
||||
nolabel="1"
|
||||
class="oe_subtotal_footer_separator"
|
||||
widget="monetary"
|
||||
options="{'currency_field': 'currency_id'}"
|
||||
/>
|
||||
</div>
|
||||
</group>
|
||||
<group string="Terms and Conditions">
|
||||
<field
|
||||
name="note"
|
||||
nolabel="1"
|
||||
colspan="2"
|
||||
placeholder="Setup default terms and conditions in your company settings."
|
||||
/>
|
||||
</group>
|
||||
<div class="oe_clear" />
|
||||
</page>
|
||||
<page string="Other Information" name="other_information">
|
||||
<group>
|
||||
<group string="Sales Information" name="sales_person">
|
||||
<field
|
||||
name="pricelist_id"
|
||||
attrs="{'required': [('state', '=', 'draft')]}"
|
||||
/>
|
||||
<field name="user_id" />
|
||||
<field
|
||||
name="team_id"
|
||||
options="{'no_create': True}"
|
||||
/>
|
||||
<field name="client_order_ref" />
|
||||
<field
|
||||
name="company_id"
|
||||
options="{'no_create': True}"
|
||||
groups="base.group_multi_company"
|
||||
/>
|
||||
</group>
|
||||
<group name="invoicing" string="Invoicing">
|
||||
<t groups="base.group_multi_company">
|
||||
<field
|
||||
name="analytic_account_id"
|
||||
attrs="{'readonly': [('sale_count','!=',0),('state','!=','draft')]}"
|
||||
groups="analytic.group_analytic_accounting"
|
||||
force_save="1"
|
||||
/>
|
||||
</t>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" />
|
||||
<field name="activity_ids" />
|
||||
<field name="message_ids" />
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_blanket_order_search" model="ir.ui.view">
|
||||
<field name="name">sale.blanket.order.search</field>
|
||||
<field name="model">sale.blanket.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<filter
|
||||
name="state_open"
|
||||
string="Open"
|
||||
domain="[('state','=', 'open')]"
|
||||
/>
|
||||
<filter
|
||||
name="original_uom_qty"
|
||||
string="Original Qty"
|
||||
domain="[('original_uom_qty','>',0.0)]"
|
||||
/>
|
||||
<filter
|
||||
name="ordered_uom_qty"
|
||||
string="Ordered Qty"
|
||||
domain="[('ordered_uom_qty','>',0.0)]"
|
||||
/>
|
||||
<filter
|
||||
name="invoiced_uom_qty"
|
||||
string="Invoiced Qty"
|
||||
domain="[('invoiced_uom_qty','>',0.0)]"
|
||||
/>
|
||||
<filter
|
||||
name="received_uom_qty"
|
||||
string="Delivered Qty"
|
||||
domain="[('delivered_uom_qty','>',0.0)]"
|
||||
/>
|
||||
<filter
|
||||
name="remaining_uom_qty"
|
||||
string="Remaining Qty"
|
||||
domain="[('remaining_uom_qty','>',0.0)]"
|
||||
/>
|
||||
<group>
|
||||
<field name="name" />
|
||||
<field name="partner_id" />
|
||||
<field name="product_id" />
|
||||
<field name="state" />
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ACTIONS (SERVER) -->
|
||||
|
||||
<record model="ir.actions.act_window" id="act_open_blanket_order_view">
|
||||
<field name="name">Blanket Orders</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">sale.blanket.order</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="search_view_id" ref="view_blanket_order_search" />
|
||||
<field name="domain">[]</field>
|
||||
<field name="context">{}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
Click to create a blanket order that can be converted into a sale order.
|
||||
</p>
|
||||
<p>
|
||||
Use this menu to search within your blanket orders. For each blanket order,
|
||||
you can track the related discussion with the customer, control
|
||||
the products delivered and control the vendor bills.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- MENU'S -->
|
||||
|
||||
<menuitem
|
||||
id="menu_blanket_order_config"
|
||||
parent="sale.sale_order_menu"
|
||||
groups="sales_team.group_sale_manager"
|
||||
sequence="20"
|
||||
action="act_open_blanket_order_view"
|
||||
/>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!-- Copyright 2018 ACSONE SA/NV
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
<odoo>
|
||||
<record model="ir.ui.view" id="sale_config_settings_form_view">
|
||||
<field name="name">res.config.settings.form (in sale_blanket_order)</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="sale.res_config_settings_view_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath
|
||||
expr="//div[hasclass('row','mt16','o_settings_container')][position() = 3]"
|
||||
position="after"
|
||||
>
|
||||
<h2>Blanket Orders</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-xs-12 col-md-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="group_blanket_disable_adding_lines" />
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="group_blanket_disable_adding_lines" />
|
||||
<div class="text-muted" id="sale_config_website_quote">
|
||||
Disable adding more lines to SOs from Blanket Orders
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="view_order_form" model="ir.ui.view">
|
||||
<field name="name">sale.order.from.blanket.form</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="currency_id" position="after">
|
||||
<field name="blanket_order_id" invisible="1" />
|
||||
</field>
|
||||
<xpath
|
||||
expr="//field[@name='order_line']//tree/field[@name='product_id']"
|
||||
position="after"
|
||||
>
|
||||
<field
|
||||
name="blanket_order_line"
|
||||
context="{'from_sale_order': True}"
|
||||
attrs="{'column_invisible': [('parent.blanket_order_id', '=', False)]}"
|
||||
/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='order_line']" position="attributes">
|
||||
<attribute name="context">{'from_sale_order': True}</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_order_form_disable_adding_lines" model="ir.ui.view">
|
||||
<field name="name">sale.order.from.blanket.form - disable adding lines</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='order_line']" position="before">
|
||||
<field name="disable_adding_lines" invisible="1" />
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='order_line']//tree" position="attributes">
|
||||
<attribute name="create">not disable_adding_lines</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import create_sale_orders
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
# Copyright 2018 ACSONE SA/NV
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import float_is_zero
|
||||
|
||||
|
||||
class BlanketOrderWizard(models.TransientModel):
|
||||
_name = "sale.blanket.order.wizard"
|
||||
_description = "Blanket order wizard"
|
||||
|
||||
@api.model
|
||||
def _default_order(self):
|
||||
# in case the cron hasn't run
|
||||
self.env["sale.blanket.order"].expire_orders()
|
||||
if not self.env.context.get("active_id"):
|
||||
return False
|
||||
blanket_order = self.env["sale.blanket.order"].search(
|
||||
[("id", "=", self.env.context["active_id"])], limit=1
|
||||
)
|
||||
if blanket_order.state == "expired":
|
||||
raise UserError(
|
||||
_("You can't create a sale order from " "an expired blanket order!")
|
||||
)
|
||||
return blanket_order
|
||||
|
||||
@api.model
|
||||
def _check_valid_blanket_order_line(self, bo_lines):
|
||||
precision = self.env["decimal.precision"].precision_get(
|
||||
"Product Unit of Measure"
|
||||
)
|
||||
company_id = False
|
||||
|
||||
if all(
|
||||
float_is_zero(line.remaining_uom_qty, precision_digits=precision)
|
||||
for line in bo_lines
|
||||
):
|
||||
raise UserError(_("The sale has already been completed."))
|
||||
|
||||
for line in bo_lines:
|
||||
if line.order_id.state != "open":
|
||||
raise UserError(
|
||||
_("Sale Blanket Order %s is not open") % line.order_id.name
|
||||
)
|
||||
line_company_id = line.company_id and line.company_id.id or False
|
||||
if company_id is not False and line_company_id != company_id:
|
||||
raise UserError(_("You have to select lines " "from the same company."))
|
||||
else:
|
||||
company_id = line_company_id
|
||||
|
||||
@api.model
|
||||
def _default_lines(self):
|
||||
blanket_order_line_obj = self.env["sale.blanket.order.line"]
|
||||
blanket_order_line_ids = self.env.context.get("active_ids", False)
|
||||
active_model = self.env.context.get("active_model", False)
|
||||
|
||||
if active_model == "sale.blanket.order":
|
||||
bo_lines = self._default_order().line_ids
|
||||
else:
|
||||
bo_lines = blanket_order_line_obj.browse(blanket_order_line_ids)
|
||||
|
||||
self._check_valid_blanket_order_line(bo_lines)
|
||||
|
||||
lines = [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"blanket_line_id": bol.id,
|
||||
"product_id": bol.product_id.id,
|
||||
"date_schedule": bol.date_schedule,
|
||||
"remaining_uom_qty": bol.remaining_uom_qty,
|
||||
"price_unit": bol.price_unit,
|
||||
"product_uom": bol.product_uom,
|
||||
"qty": bol.remaining_uom_qty,
|
||||
"partner_id": bol.partner_id,
|
||||
},
|
||||
)
|
||||
for bol in bo_lines.filtered(
|
||||
lambda l: not l.display_type and l.remaining_uom_qty != 0.0
|
||||
)
|
||||
]
|
||||
return lines
|
||||
|
||||
blanket_order_id = fields.Many2one(
|
||||
comodel_name="sale.blanket.order",
|
||||
readonly=True,
|
||||
default=lambda self: self._default_order(),
|
||||
)
|
||||
sale_order_id = fields.Many2one(
|
||||
"sale.order", string="Purchase Order", domain=[("state", "=", "draft")]
|
||||
)
|
||||
line_ids = fields.One2many(
|
||||
"sale.blanket.order.wizard.line",
|
||||
"wizard_id",
|
||||
string="Lines",
|
||||
default=_default_lines,
|
||||
)
|
||||
|
||||
def _prepare_so_line_vals(self, line):
|
||||
return {
|
||||
"product_id": line.product_id.id,
|
||||
"name": line.product_id.name,
|
||||
"product_uom": line.product_uom.id,
|
||||
"sequence": line.blanket_line_id.sequence,
|
||||
"price_unit": line.blanket_line_id.price_unit,
|
||||
"blanket_order_line": line.blanket_line_id.id,
|
||||
"product_uom_qty": line.qty,
|
||||
"tax_id": [(6, 0, line.taxes_id.ids)],
|
||||
}
|
||||
|
||||
def _prepare_so_vals(
|
||||
self,
|
||||
customer,
|
||||
user_id,
|
||||
currency_id,
|
||||
pricelist_id,
|
||||
payment_term_id,
|
||||
order_lines_by_customer,
|
||||
):
|
||||
return {
|
||||
"partner_id": customer,
|
||||
"origin": self.blanket_order_id.name,
|
||||
"user_id": user_id,
|
||||
"currency_id": currency_id,
|
||||
"pricelist_id": pricelist_id,
|
||||
"payment_term_id": payment_term_id,
|
||||
"order_line": order_lines_by_customer[customer],
|
||||
"analytic_account_id": self.blanket_order_id.analytic_account_id.id,
|
||||
}
|
||||
|
||||
def create_sale_order(self):
|
||||
order_lines_by_customer = defaultdict(list)
|
||||
currency_id = 0
|
||||
pricelist_id = 0
|
||||
user_id = 0
|
||||
payment_term_id = 0
|
||||
for line in self.line_ids.filtered(lambda l: l.qty != 0.0):
|
||||
if line.qty > line.remaining_uom_qty:
|
||||
raise UserError(_("You can't order more than the remaining quantities"))
|
||||
vals = self._prepare_so_line_vals(line)
|
||||
order_lines_by_customer[line.partner_id.id].append((0, 0, vals))
|
||||
|
||||
if currency_id == 0:
|
||||
currency_id = line.blanket_line_id.order_id.currency_id.id
|
||||
elif currency_id != line.blanket_line_id.order_id.currency_id.id:
|
||||
currency_id = False
|
||||
|
||||
if pricelist_id == 0:
|
||||
pricelist_id = line.blanket_line_id.pricelist_id.id
|
||||
elif pricelist_id != line.blanket_line_id.pricelist_id.id:
|
||||
pricelist_id = False
|
||||
|
||||
if user_id == 0:
|
||||
user_id = line.blanket_line_id.user_id.id
|
||||
elif user_id != line.blanket_line_id.user_id.id:
|
||||
user_id = False
|
||||
|
||||
if payment_term_id == 0:
|
||||
payment_term_id = line.blanket_line_id.payment_term_id.id
|
||||
elif payment_term_id != line.blanket_line_id.payment_term_id.id:
|
||||
payment_term_id = False
|
||||
|
||||
if not order_lines_by_customer:
|
||||
raise UserError(_("An order can't be empty"))
|
||||
|
||||
if not currency_id:
|
||||
raise UserError(
|
||||
_(
|
||||
"Can not create Sale Order from Blanket "
|
||||
"Order lines with different currencies"
|
||||
)
|
||||
)
|
||||
|
||||
res = []
|
||||
for customer in order_lines_by_customer:
|
||||
order_vals = self._prepare_so_vals(
|
||||
customer,
|
||||
user_id,
|
||||
currency_id,
|
||||
pricelist_id,
|
||||
payment_term_id,
|
||||
order_lines_by_customer,
|
||||
)
|
||||
sale_order = self.env["sale.order"].create(order_vals)
|
||||
res.append(sale_order.id)
|
||||
return {
|
||||
"domain": [("id", "in", res)],
|
||||
"name": _("Sales Orders"),
|
||||
"view_type": "form",
|
||||
"view_mode": "tree,form",
|
||||
"res_model": "sale.order",
|
||||
"context": {"from_sale_order": True},
|
||||
"type": "ir.actions.act_window",
|
||||
}
|
||||
|
||||
|
||||
class BlanketOrderWizardLine(models.TransientModel):
|
||||
_name = "sale.blanket.order.wizard.line"
|
||||
_description = "Blanket order wizard line"
|
||||
|
||||
wizard_id = fields.Many2one("sale.blanket.order.wizard")
|
||||
blanket_line_id = fields.Many2one("sale.blanket.order.line")
|
||||
product_id = fields.Many2one(
|
||||
"product.product", related="blanket_line_id.product_id", string="Product"
|
||||
)
|
||||
product_uom = fields.Many2one(
|
||||
"uom.uom", related="blanket_line_id.product_uom", string="Unit of Measure"
|
||||
)
|
||||
date_schedule = fields.Date(string="Scheduled Date")
|
||||
remaining_uom_qty = fields.Float(related="blanket_line_id.remaining_uom_qty")
|
||||
qty = fields.Float(string="Quantity to Order", required=True)
|
||||
price_unit = fields.Float(related="blanket_line_id.price_unit")
|
||||
currency_id = fields.Many2one("res.currency", related="blanket_line_id.currency_id")
|
||||
partner_id = fields.Many2one(
|
||||
"res.partner", related="blanket_line_id.partner_id", string="Vendor"
|
||||
)
|
||||
taxes_id = fields.Many2many("account.tax", related="blanket_line_id.taxes_id")
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="view_create_sale_order" model="ir.ui.view">
|
||||
<field name="name">Create Sale Order</field>
|
||||
<field name="model">sale.blanket.order.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Create Sale Order">
|
||||
<group>
|
||||
<field name="line_ids" nolabel="1" colspan="2">
|
||||
<tree create="false" editable="bottom">
|
||||
<field
|
||||
name="blanket_line_id"
|
||||
force_save="1"
|
||||
invisible="1"
|
||||
/>
|
||||
<field name="product_id" />
|
||||
<field name="partner_id" />
|
||||
<field name="date_schedule" />
|
||||
<field name="price_unit" />
|
||||
<field name="remaining_uom_qty" />
|
||||
<field name="product_uom" groups="uom.group_uom" />
|
||||
<field name="qty" />
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
<footer>
|
||||
<button
|
||||
name="create_sale_order"
|
||||
string="Create and View Order"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
/>
|
||||
<button string="Cancel" class="btn-default" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_create_sale_order" model="ir.actions.act_window">
|
||||
<field name="name">Create Sale Order</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">sale.blanket.order.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field
|
||||
name="binding_model_id"
|
||||
ref="sale_blanket_order.model_sale_blanket_order_line"
|
||||
/>
|
||||
</record>
|
||||
</odoo>
|
||||