añadido Purchase Order Product Recommendation

This commit is contained in:
santiky 2021-10-20 21:34:48 +02:00
parent 50752d87e7
commit b65ba39360
Signed by: snt
GPG key ID: A9FD34930EADBE71
15 changed files with 1701 additions and 0 deletions

View file

@ -0,0 +1,109 @@
=====================================
Purchase Order Product Recommendation
=====================================
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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/licence-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%2Fpurchase--workflow-lightgray.png?logo=github
:target: https://github.com/OCA/purchase-workflow/tree/12.0/purchase_order_product_recommendation
:alt: OCA/purchase-workflow
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/purchase-workflow-12-0/purchase-workflow-12-0-purchase_order_product_recommendation
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/142/12.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5|
This module adds a recommended products wizard to current purchase order.
It is based on delivered products to customer locations in a given range of
dates, and allows the purchase manager to quickly know the most sold products
for the current supplier, which results in an easy to use hint to improve
the purchase workflow.
If you want a better mobile usability, the module is ready to use with the
'web_widget_numeric_step' module. Just install it and you will get a better
numeric input experience.
**Table of contents**
.. contents::
:local:
Usage
=====
To use this module, you need to:
#. Create a new purchase order.
#. Assign its supplier.
#. Press *Recommended Products* button.
#. Set or adjust hinted quantities to order.
#. Press *Accept*.
The wizard filters products those with supplier infos set for the current order
supplier. Then it computes how many times those products have been delivered to
customer locations, and finally it makes a simple estimation of how many
quantites would be necesary to order given the forcasted stock and the computed
demand.
If you want to constrain results to only some categories, you can also select
them in the wizard.
If you have multiple warehouses, you can also constrain the recommendations to
the deliveries of specific ones.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/purchase-workflow/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/purchase-workflow/issues/new?body=module:%20purchase_order_product_recommendation%0Aversion:%2012.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
~~~~~~~
* Tecnativa
Contributors
~~~~~~~~~~~~
* `Tecnativa <https://www.tecnativa.com>`_:
* David Vidal
* Ernesto Tejeda
* Pedro M. Baeza
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/purchase-workflow <https://github.com/OCA/purchase-workflow/tree/12.0/purchase_order_product_recommendation>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View file

@ -0,0 +1 @@
from . import wizards

View file

@ -0,0 +1,20 @@
# Copyright 2019 David Vidal <david.vidal@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "Purchase Order Product Recommendation",
"summary": "Recommend products to buy to supplier based on history",
"version": "12.0.1.2.3",
"category": "Purchases",
"website": "https://github.com/OCA/purchase-workflow",
"author": "Tecnativa, Odoo Community Association (OCA)",
"license": "AGPL-3",
"application": False,
"installable": True,
"depends": [
"purchase_stock",
],
"data": [
"wizards/purchase_order_recommendation_view.xml",
"views/purchase_order_view.xml",
],
}

View file

@ -0,0 +1,299 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * purchase_order_product_recommendation
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 11.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-27 16:24+0000\n"
"PO-Revision-Date: 2019-11-27 16:24+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: purchase_order_product_recommendation
#: model_terms:ir.ui.view,arch_db:purchase_order_product_recommendation.purchase_order_recommendation_view_form
msgid "Accept"
msgstr "Aceptar"
#. module: purchase_order_product_recommendation
#: model_terms:ir.ui.view,arch_db:purchase_order_product_recommendation.purchase_order_recommendation_view_form
msgid "Cancel"
msgstr "Cancelar"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,help:purchase_order_product_recommendation.field_purchase_order_recommendation__warehouse_ids
msgid "Constrain search to an specific warehouse"
msgstr "Restringir búsqueda a un almacén concreto"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation__create_uid
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation_line__create_uid
msgid "Created by"
msgstr "Creado por"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation__create_date
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation_line__create_date
msgid "Created on"
msgstr "Creado el"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation_line__currency_id
msgid "Currency"
msgstr "Moneda"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation__date_begin
msgid "Date Begin"
msgstr "Fecha de inicio"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation__date_end
msgid "Date End"
msgstr "Fecha final"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation__display_name
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation_line__display_name
msgid "Display Name"
msgstr "Nombre a mostrar"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,help:purchase_order_product_recommendation.field_purchase_order_recommendation__product_category_ids
msgid "Filter by product internal category"
msgstr "Filtrar por categoría interna de producto"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,help:purchase_order_product_recommendation.field_purchase_order_recommendation__date_end
msgid "Final date to compute recommendations."
msgstr "Fecha has la que se calcularán las recomendaciones."
#. module: purchase_order_product_recommendation
#: model_terms:ir.ui.view,arch_db:purchase_order_product_recommendation.purchase_order_recommendation_view_form
msgid "Forecasted Qty"
msgstr "Ctd Prevista"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation__id
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation_line__id
msgid "ID"
msgstr "ID (identificación)"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,help:purchase_order_product_recommendation.field_purchase_order_recommendation__date_begin
msgid "Initial date to compute recommendations."
msgstr "Fecha desde la que se calcularán las recomendaciones."
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation_line__is_modified
msgid "Is Modified"
msgstr "Está modificado"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation____last_update
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation_line____last_update
msgid "Last Modified on"
msgstr "Última modificación en"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation__write_uid
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation_line__write_uid
msgid "Last Updated by"
msgstr "Última actualización por"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation__write_date
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation_line__write_date
msgid "Last Updated on"
msgstr "Última actualización el"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation__line_amount
msgid "Number of recommendations"
msgstr "Número de recomendaciones"
#. module: purchase_order_product_recommendation
#: model_terms:ir.ui.view,arch_db:purchase_order_product_recommendation.purchase_order_recommendation_view_form
msgid "Price"
msgstr "Precio"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation_line__price_unit
msgid "Price Unit"
msgstr "Precio unitario"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation_line__product_id
msgid "Product"
msgstr "Producto"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation__product_category_ids
msgid "Product Categories"
msgstr "Categorías de productos"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation__line_ids
msgid "Products"
msgstr "Productos"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation_line__purchase_line_id
msgid "Purchase Line"
msgstr "Línea de compra"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation__order_id
msgid "Purchase Order"
msgstr "Pedido de compra"
#. module: purchase_order_product_recommendation
#: model_terms:ir.ui.view,arch_db:purchase_order_product_recommendation.purchase_order_recommendation_view_form
msgid "Qty"
msgstr "Ctd"
#. module: purchase_order_product_recommendation
#: model_terms:ir.ui.view,arch_db:purchase_order_product_recommendation.purchase_order_recommendation_view_form
msgid "Qty Dlvd./day"
msgstr "Ctd entrg./día"
#. module: purchase_order_product_recommendation
#: model_terms:ir.ui.view,arch_db:purchase_order_product_recommendation.purchase_order_recommendation_view_form
msgid "Qty On Hand"
msgstr "Ctd a mano"
#. module: purchase_order_product_recommendation
#: model_terms:ir.ui.view,arch_db:purchase_order_product_recommendation.purchase_order_recommendation_view_form
msgid "Qty Received"
msgstr "Ctd recibida"
#. module: purchase_order_product_recommendation
#: model_terms:ir.ui.view,arch_db:purchase_order_product_recommendation.purchase_order_recommendation_view_form
msgid "Qty delivered"
msgstr "Ctd entregada"
#. module: purchase_order_product_recommendation
#: model_terms:ir.ui.view,arch_db:purchase_order_product_recommendation.purchase_order_form
msgid "Recommended Products"
msgstr "Productos recomendados"
#. module: purchase_order_product_recommendation
#: model:ir.actions.act_window,name:purchase_order_product_recommendation.purchase_order_recommendation_action
msgid "Recommended Products for this Customer"
msgstr "Productos recomendados para este proveedor"
#. module: purchase_order_product_recommendation
#: model:ir.model,name:purchase_order_product_recommendation.model_purchase_order_recommendation_line
msgid "Recommended product for current purchase order"
msgstr "Producto recomendado para el pedido de compra en curso"
#. module: purchase_order_product_recommendation
#: model:ir.model,name:purchase_order_product_recommendation.model_purchase_order_recommendation
msgid "Recommended products for current purchase order"
msgstr "Productos recomendados para el pedido de compra en curso"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,help:purchase_order_product_recommendation.field_purchase_order_recommendation__show_all_partner_products
msgid "Show all products with supplier infos for this supplier"
msgstr "Mostrar todos los productos con tarifas de compra para este proveedor"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation__show_all_products
msgid "Show all purchasable products"
msgstr "Mostrar todos los productos comprables"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation__show_all_partner_products
msgid "Show all supplier products"
msgstr "Mostrar todos los productos del proveedor"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,help:purchase_order_product_recommendation.field_purchase_order_recommendation__line_amount
msgid ""
"Stablish a limit on how many recommendations you want to get.Leave it as 0 "
"to set no limit"
msgstr ""
"Establezca un límite de cuántas recomendaciones desea obtener. Déjelo en 0 "
"si no desea límite"
#. module: purchase_order_product_recommendation
#: code:addons/purchase_order_product_recommendation/wizards/purchase_order_recommendation.py:73
#, python-format
msgid "This wizard is only valid for purchases"
msgstr "Este asistente solo es válido para compras"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation_line__times_delivered
msgid "Times Delivered"
msgstr "Veces entregado"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation_line__times_received
msgid "Times Received"
msgstr "Veces recibido"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation_line__units_available
msgid "Units Available"
msgstr "Cantidad a mano"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation_line__units_avg_delivered
msgid "Units Avg Delivered"
msgstr "Media de unidades entregadas"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation_line__units_delivered
msgid "Units Delivered"
msgstr "Unidades entregadas"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation_line__units_included
msgid "Units Included"
msgstr "Unidades incluídas"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation_line__units_received
msgid "Units Received"
msgstr "Unidades recibidas"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation_line__units_virtual_available
msgid "Units Virtual Available"
msgstr "Cantidad prevista"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,help:purchase_order_product_recommendation.field_purchase_order_recommendation__show_all_products
msgid "Useful if a product hasn't been selled by the partner yet"
msgstr "Resulta útil si un producto aún no ha sido vendido por el proveedor."
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation_line__partner_id
msgid "Vendor"
msgstr "Proveedor"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation__warehouse_ids
msgid "Warehouse"
msgstr "Almacén"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation__warehouse_count
msgid "Warehouse Count"
msgstr "Nº de Almacenes"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,field_description:purchase_order_product_recommendation.field_purchase_order_recommendation_line__wizard_id
msgid "Wizard"
msgstr "Asistente"
#. module: purchase_order_product_recommendation
#: model:ir.model.fields,help:purchase_order_product_recommendation.field_purchase_order_recommendation_line__partner_id
msgid "You can find a vendor by its Name, TIN, Email or Internal Reference."
msgstr ""

View file

@ -0,0 +1,5 @@
* `Tecnativa <https://www.tecnativa.com>`_:
* David Vidal
* Ernesto Tejeda
* Pedro M. Baeza

View file

@ -0,0 +1,10 @@
This module adds a recommended products wizard to current purchase order.
It is based on delivered products to customer locations in a given range of
dates, and allows the purchase manager to quickly know the most sold products
for the current supplier, which results in an easy to use hint to improve
the purchase workflow.
If you want a better mobile usability, the module is ready to use with the
'web_widget_numeric_step' module. Just install it and you will get a better
numeric input experience.

View file

@ -0,0 +1,19 @@
To use this module, you need to:
#. Create a new purchase order.
#. Assign its supplier.
#. Press *Recommended Products* button.
#. Set or adjust hinted quantities to order.
#. Press *Accept*.
The wizard filters products those with supplier infos set for the current order
supplier. Then it computes how many times those products have been delivered to
customer locations, and finally it makes a simple estimation of how many
quantites would be necesary to order given the forcasted stock and the computed
demand.
If you want to constrain results to only some categories, you can also select
them in the wizard.
If you have multiple warehouses, you can also constrain the recommendations to
the deliveries of specific ones.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View file

@ -0,0 +1,452 @@
<?xml version="1.0" encoding="utf-8" ?>
<!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 0.15.1: http://docutils.sourceforge.net/" />
<title>Purchase Order Product Recommendation</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/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: grey; } /* 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 {
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" id="purchase-order-product-recommendation">
<h1 class="title">Purchase Order Product Recommendation</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" 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" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/purchase-workflow/tree/12.0/purchase_order_product_recommendation"><img alt="OCA/purchase-workflow" src="https://img.shields.io/badge/github-OCA%2Fpurchase--workflow-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/purchase-workflow-12-0/purchase-workflow-12-0-purchase_order_product_recommendation"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/142/12.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>This module adds a recommended products wizard to current purchase order.</p>
<p>It is based on delivered products to customer locations in a given range of
dates, and allows the purchase manager to quickly know the most sold products
for the current supplier, which results in an easy to use hint to improve
the purchase workflow.</p>
<p>If you want a better mobile usability, the module is ready to use with the
web_widget_numeric_step module. Just install it and you will get a better
numeric input experience.</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="id1">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id2">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id3">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id4">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id5">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id6">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#id1">Usage</a></h1>
<p>To use this module, you need to:</p>
<ol class="arabic simple">
<li>Create a new purchase order.</li>
<li>Assign its supplier.</li>
<li>Press <em>Recommended Products</em> button.</li>
<li>Set or adjust hinted quantities to order.</li>
<li>Press <em>Accept</em>.</li>
</ol>
<p>The wizard filters products those with supplier infos set for the current order
supplier. Then it computes how many times those products have been delivered to
customer locations, and finally it makes a simple estimation of how many
quantites would be necesary to order given the forcasted stock and the computed
demand.</p>
<p>If you want to constrain results to only some categories, you can also select
them in the wizard.</p>
<p>If you have multiple warehouses, you can also constrain the recommendations to
the deliveries of specific ones.</p>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id2">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/purchase-workflow/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/purchase-workflow/issues/new?body=module:%20purchase_order_product_recommendation%0Aversion:%2012.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">
<h1><a class="toc-backref" href="#id3">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id4">Authors</a></h2>
<ul class="simple">
<li>Tecnativa</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id5">Contributors</a></h2>
<ul class="simple">
<li><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:<ul>
<li>David Vidal</li>
<li>Ernesto Tejeda</li>
<li>Pedro M. Baeza</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id6">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/purchase-workflow/tree/12.0/purchase_order_product_recommendation">OCA/purchase-workflow</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1 @@
from . import test_recommendation

View file

@ -0,0 +1,299 @@
# Copyright 2019 David Vidal <david.vidal@tecnativa.com>
# Copyright 2020 Tecnativa - Pedro M. Baeza
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from datetime import date, datetime
from odoo.tests.common import SavepointCase
class RecommendationCase(SavepointCase):
@classmethod
def setUpClass(cls):
super(RecommendationCase, cls).setUpClass()
cls.partner = cls.env['res.partner'].create({
'name': 'Mr. Odoo',
})
cls.category_obj = cls.env['product.category']
cls.categ1 = cls.category_obj.create({
'name': 'Test Cat 1',
})
cls.categ2 = cls.category_obj.create({
'name': 'Test Cat 2',
})
cls.product_obj = cls.env['product.product']
cls.prod_1 = cls.product_obj.create({
'default_code': 'product-1',
'name': 'Test Product 1',
'categ_id': cls.categ1.id,
'type': 'product',
'seller_ids': [(0, 0, {'name': cls.partner.id, 'price': 5})],
})
cls.prod_2 = cls.prod_1.copy({
'default_code': 'product-2',
'name': 'Test Product 2',
'categ_id': cls.categ2.id,
'seller_ids': [(0, 0, {'name': cls.partner.id, 'price': 10})],
})
cls.prod_3 = cls.prod_1.copy({
'default_code': 'product-3',
'name': 'Test Product 3',
'categ_id': cls.categ2.id,
'seller_ids': [(0, 0, {'name': cls.partner.id, 'price': 7})],
})
# Warehouses
cls.wh1 = cls.env['stock.warehouse'].create({
'name': 'TEST WH1',
'code': 'TST1',
})
cls.wh2 = cls.env['stock.warehouse'].create({
'name': 'TEST WH2',
'code': 'TST2',
})
# Locations
location_obj = cls.env['stock.location']
cls.supplier_loc = location_obj.create({
'name': 'Test supplier location',
'usage': 'supplier',
})
cls.customer_loc = location_obj.create({
'name': 'Test customer location',
'usage': 'customer',
})
# Create deliveries and receipts orders to have a history
cls.picking_obj = cls.env['stock.picking']
cls.picking_1 = cls.picking_obj.create({
'location_id': cls.wh1.lot_stock_id.id,
'location_dest_id': cls.customer_loc.id,
'partner_id': cls.partner.id,
'picking_type_id': cls.wh1.out_type_id.id,
})
cls.picking_2 = cls.picking_obj.create({
'location_id': cls.wh2.lot_stock_id.id,
'location_dest_id': cls.customer_loc.id,
'partner_id': cls.partner.id,
'picking_type_id': cls.wh2.out_type_id.id,
})
cls.picking_3 = cls.picking_obj.create({
'location_id': cls.supplier_loc.id,
'location_dest_id': cls.wh1.lot_stock_id.id,
'partner_id': cls.partner.id,
'picking_type_id': cls.wh1.in_type_id.id,
})
cls.move_line = cls.env['stock.move.line']
cls.move_line |= cls.move_line.create({
'product_id': cls.prod_1.id,
'product_uom_id': cls.prod_1.uom_id.id,
'qty_done': 1,
'date': datetime(2018, 1, 11, 15, 5),
'location_id': cls.wh1.lot_stock_id.id,
'location_dest_id': cls.customer_loc.id,
'picking_id': cls.picking_1.id,
})
cls.move_line |= cls.move_line.create({
'product_id': cls.prod_2.id,
'product_uom_id': cls.prod_2.uom_id.id,
'qty_done': 38,
'date': datetime(2019, 2, 1, 0, 5),
'location_id': cls.wh1.lot_stock_id.id,
'location_dest_id': cls.customer_loc.id,
'picking_id': cls.picking_1.id,
})
cls.move_line |= cls.move_line.create({
'product_id': cls.prod_2.id,
'product_uom_id': cls.prod_2.uom_id.id,
'qty_done': 4,
'date': datetime(2019, 2, 1, 0, 5),
'location_id': cls.wh2.lot_stock_id.id,
'location_dest_id': cls.customer_loc.id,
'picking_id': cls.picking_2.id,
})
cls.move_line |= cls.move_line.create({
'product_id': cls.prod_3.id,
'product_uom_id': cls.prod_3.uom_id.id,
'qty_done': 13,
'date': datetime(2019, 2, 1, 0, 6),
'location_id': cls.wh2.lot_stock_id.id,
'location_dest_id': cls.customer_loc.id,
'picking_id': cls.picking_2.id,
})
cls.move_line |= cls.move_line.create({
'product_id': cls.prod_3.id,
'product_uom_id': cls.prod_3.uom_id.id,
'qty_done': 7,
'date': datetime(2019, 2, 1, 0, 0),
'location_id': cls.supplier_loc.id,
'location_dest_id': cls.wh1.lot_stock_id.id,
'picking_id': cls.picking_3.id,
})
cls.move_line.write({
'state': 'done',
})
# Total stock available for prod3 is 5 units split in two warehouses
quant_obj = cls.env['stock.quant']
quant_obj.create({
'product_id': cls.prod_3.id,
'location_id': cls.wh1.lot_stock_id.id,
'quantity': 2.0,
})
quant_obj.create({
'product_id': cls.prod_3.id,
'location_id': cls.wh2.lot_stock_id.id,
'quantity': 3.0,
})
# Create a purchase order for the same customer
cls.new_po = cls.env["purchase.order"].create({
"partner_id": cls.partner.id,
})
def wizard(self):
"""Get a wizard."""
wizard = self.env["purchase.order.recommendation"].with_context(
active_id=self.new_po.id, active_model='purchase.order'
).create({})
wizard._generate_recommendations()
return wizard
class RecommendationCaseTests(RecommendationCase):
def test_recommendations(self):
"""Recommendations are OK."""
wizard = self.wizard()
# Order came in from context
self.assertEqual(wizard.order_id, self.new_po)
# All our moves are in the past
self.assertFalse(wizard.line_ids)
wizard.date_begin = wizard.date_end = date(2019, 2, 1)
wizard._generate_recommendations()
self.assertEqual(wizard.line_ids[0].times_delivered, 2)
self.assertEqual(wizard.line_ids[0].units_delivered, 42)
self.assertEqual(wizard.line_ids[0].units_included, 42)
self.assertEqual(wizard.line_ids[0].product_id, self.prod_2)
self.assertEqual(wizard.line_ids[1].times_delivered, 1)
self.assertEqual(wizard.line_ids[1].units_delivered, 13)
self.assertEqual(wizard.line_ids[1].units_included, 8)
self.assertEqual(wizard.line_ids[1].product_id, self.prod_3)
self.assertEqual(wizard.line_ids[1].units_available, 5)
self.assertEqual(wizard.line_ids[1].units_virtual_available, 5)
# Only 1 product if limited as such
wizard.line_amount = 1
wizard._generate_recommendations()
self.assertEqual(len(wizard.line_ids), 1)
def test_recommendations_by_warehouse(self):
"""We can split recommendations by delivery warehouse"""
wizard = self.wizard()
wizard.date_begin = wizard.date_end = date(2019, 2, 1)
# Just delivered to WH2
wizard.warehouse_ids = self.wh2
wizard._generate_recommendations()
self.assertEqual(wizard.line_ids[0].times_delivered, 1)
self.assertEqual(wizard.line_ids[0].units_delivered, 4)
self.assertEqual(wizard.line_ids[0].units_included, 4)
self.assertEqual(wizard.line_ids[0].product_id, self.prod_2)
self.assertEqual(wizard.line_ids[1].times_delivered, 1)
self.assertEqual(wizard.line_ids[1].units_delivered, 13)
self.assertEqual(wizard.line_ids[1].units_included, 10)
self.assertEqual(wizard.line_ids[1].product_id, self.prod_3)
self.assertEqual(wizard.line_ids[1].units_available, 3)
self.assertEqual(wizard.line_ids[1].units_virtual_available, 3)
# Just delivered to WH1
wizard.warehouse_ids = self.wh1
wizard._generate_recommendations()
self.assertEqual(wizard.line_ids[0].times_delivered, 1)
self.assertEqual(wizard.line_ids[0].units_delivered, 38)
self.assertEqual(wizard.line_ids[0].units_included, 38)
self.assertEqual(wizard.line_ids[0].product_id, self.prod_2)
self.assertEqual(wizard.line_ids[1].times_delivered, 0)
self.assertEqual(wizard.line_ids[1].units_delivered, 0)
self.assertEqual(wizard.line_ids[1].units_received, 7)
self.assertEqual(wizard.line_ids[1].units_included, 0)
self.assertEqual(wizard.line_ids[1].product_id, self.prod_3)
self.assertEqual(len(wizard.line_ids), 2)
self.assertEqual(wizard.line_ids[1].units_available, 2)
self.assertEqual(wizard.line_ids[1].units_virtual_available, 2)
# Delivered to both warehouses
wizard.warehouse_ids |= self.wh2
wizard._generate_recommendations()
self.assertEqual(wizard.line_ids[0].times_delivered, 2)
self.assertEqual(wizard.line_ids[0].units_delivered, 42)
self.assertEqual(wizard.line_ids[0].units_included, 42)
self.assertEqual(wizard.line_ids[0].product_id, self.prod_2)
self.assertEqual(wizard.line_ids[1].times_delivered, 1)
self.assertEqual(wizard.line_ids[1].units_delivered, 13)
self.assertEqual(wizard.line_ids[1].units_included, 8)
self.assertEqual(wizard.line_ids[1].product_id, self.prod_3)
self.assertEqual(wizard.line_ids[1].units_available, 5)
self.assertEqual(wizard.line_ids[1].units_virtual_available, 5)
def test_action_accept(self):
"""Open wizard when there are PO Lines and click on Accept"""
po_line = self.env['purchase.order.line'].new({
'sequence': 1,
'order_id': self.new_po.id,
'product_id': self.prod_2.id,
})
po_line.onchange_product_id()
po_line.product_qty = 10
po_line._onchange_quantity()
self.new_po.order_line = po_line
# Create wizard and set dates
wizard = self.wizard()
wizard.date_begin = wizard.date_end = date(2019, 2, 1)
wizard._generate_recommendations()
# After change dates, in the recommendation line corresponding to the
# self.prod_2 Units Included must be 10
self.assertEqual(wizard.line_ids[0].units_included, 10)
self.assertEqual(wizard.line_ids[1].units_included, 8)
# Change Units Included amount to 20 and accept, then the product_qty
# of the PO Line corresponding to the self.prod_2 must change to 20
wizard.line_ids[0].units_included = 20
wizard.action_accept()
self.assertEqual(len(self.new_po.order_line), 2)
self.assertEqual(self.new_po.order_line[0].product_id, self.prod_2)
self.assertEqual(self.new_po.order_line[0].product_qty, 20)
self.assertEqual(self.new_po.order_line[1].product_id, self.prod_3)
self.assertEqual(self.new_po.order_line[1].product_qty, 8)
def test_recommendations_by_category(self):
"""We can split recommendations by delivery warehouse"""
wizard = self.wizard()
wizard.date_begin = wizard.date_end = '2019-02-01'
# Just delivered from category 1
wizard.product_category_ids = self.categ1
wizard.show_all_partner_products = True
wizard._generate_recommendations()
# Just one line with products from category 1
self.assertEqual(wizard.line_ids.product_id, self.prod_1)
# Just delivered from category 2
wizard.product_category_ids = self.categ2
wizard._generate_recommendations()
self.assertEqual(len(wizard.line_ids), 2)
# All categorys
wizard.product_category_ids += self.categ1
wizard._generate_recommendations()
self.assertEqual(len(wizard.line_ids), 3)
# No category set
wizard.product_category_ids = False
wizard._generate_recommendations()
self.assertEqual(len(wizard.line_ids), 3)
# All products
wizard.show_all_products = True
wizard.line_amount = 0
wizard._generate_recommendations()
purchase_products_number = self.product_obj.search_count([
('purchase_ok', '!=', False),
])
self.assertEqual(len(wizard.line_ids), purchase_products_number)
def test_recommendations_inactive_product(self):
"""Recommendations are OK."""
self.prod_2.active = False
wizard = self.wizard()
wizard.date_begin = wizard.date_end = date(2019, 2, 1)
wizard._generate_recommendations()
# The first recommendation line is the prod_3, as prod_2 is archived
self.assertEqual(wizard.line_ids[0].product_id, self.prod_3)
self.prod_3.purchase_ok = False
wizard._generate_recommendations()
# No recommendations as both elegible products are excluded
self.assertFalse(wizard.line_ids)

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="purchase_order_form" model="ir.ui.view">
<field name="name">Recommended products button</field>
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.purchase_order_form"/>
<field name="arch" type="xml">
<header>
<button
name="%(purchase_order_recommendation_action)d"
states="draft,sent,sale"
string="Recommended Products"
type="action"/>
</header>
</field>
</record>
</odoo>

View file

@ -0,0 +1 @@
from . import purchase_order_recommendation

View file

@ -0,0 +1,393 @@
# Copyright 2019 David Vidal <david.vidal@tecnativa.com>
# Copyright 2020 Tecnativa - Pedro M. Baeza
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from datetime import datetime
from odoo import _, api, fields, models
from odoo.addons import decimal_precision as dp
from odoo.exceptions import UserError
from datetime import timedelta
class PurchaseOrderRecommendation(models.TransientModel):
_name = 'purchase.order.recommendation'
_description = 'Recommended products for current purchase order'
order_id = fields.Many2one(
comodel_name='purchase.order',
string='Purchase Order',
default=lambda self: self._default_order_id(),
required=True,
readonly=True,
ondelete='cascade',
)
date_begin = fields.Date(
default=fields.Date.context_today,
required=True,
help='Initial date to compute recommendations.',
)
date_end = fields.Date(
default=fields.Date.context_today,
required=True,
help='Final date to compute recommendations.',
)
line_ids = fields.One2many(
comodel_name='purchase.order.recommendation.line',
inverse_name='wizard_id',
string='Products',
)
line_amount = fields.Integer(
string='Number of recommendations',
default=15,
required=True,
help='Stablish a limit on how many recommendations you want to get.'
'Leave it as 0 to set no limit',
)
show_all_partner_products = fields.Boolean(
string='Show all supplier products',
default=False,
help='Show all products with supplier infos for this supplier',
)
show_all_products = fields.Boolean(
string='Show all purchasable products',
default=False,
help="Useful if a product hasn't been selled by the partner yet",
)
product_category_ids = fields.Many2many(
comodel_name='product.category',
string="Product Categories",
help='Filter by product internal category',
)
warehouse_ids = fields.Many2many(
comodel_name='stock.warehouse',
string='Warehouse',
help='Constrain search to an specific warehouse',
)
warehouse_count = fields.Integer(
default=lambda self: len(self.env['stock.warehouse'].search([])),
)
@api.model
def _default_order_id(self):
if self.env.context.get('active_model', False) != 'purchase.order':
raise UserError(_('This wizard is only valid for purchases'))
return self.env.context.get('active_id', False)
@api.multi
def _get_total_days(self):
"""Compute days between the initial and the end date"""
day = (self.date_end + timedelta(days=1) - self.date_begin).days
return day
def _get_supplier_products(self):
"""Common method to be used for field domain filters"""
supplierinfo_obj = self.env['product.supplierinfo'].with_context(
prefetch_fields=False)
partner = self.order_id.partner_id.commercial_partner_id
supplierinfos = supplierinfo_obj.search([('name', '=', partner.id)])
product_tmpls = supplierinfos.mapped('product_tmpl_id').filtered(
lambda x: x.active and x.purchase_ok
)
products = supplierinfos.mapped('product_id').filtered(
lambda x: x.active and x.purchase_ok
)
products += product_tmpls.mapped('product_variant_ids')
return products
def _get_products(self):
"""Override to filter products show_all_partner_products is set"""
products = self._get_supplier_products()
# Filter products by category if set.
# It will apply to show_all_partner_products as well
if self.product_category_ids:
products = products.filtered(
lambda x: x.categ_id in self.product_category_ids)
return products
def _get_move_line_domain(self, products, src, dst):
"""Allows to easily extend the domain by third modules"""
combine = datetime.combine
# We can receive a context to be able to get different dates with
# the same wizard attributes, for example comparing periods
date_begin = self.env.context.get("period_date_begin", self.date_begin)
date_end = self.env.context.get("period_date_end", self.date_end)
domain = [
('product_id', 'in', products.ids),
('date', '>=', combine(date_begin, datetime.min.time())),
('date', '<=', combine(date_end, datetime.max.time())),
('location_id.usage', '=', src),
('location_dest_id.usage', '=', dst),
('state', '=', 'done'),
]
if self.warehouse_ids:
domain += [('picking_id.picking_type_id.warehouse_id', 'in',
self.warehouse_ids.ids)]
return domain
def _get_all_products_domain(self):
"""Override to add more product filters if show_all_products is set"""
domain = [
('purchase_ok', '=', True),
]
if self.product_category_ids:
domain += [('categ_id', 'in', self.product_category_ids.ids)]
return domain
@api.multi
def _find_move_line(self, src='internal', dst='customer'):
""""Returns a dictionary from the move lines in a range of dates
from and to given location types"""
products = self._get_products()
domain = self._get_move_line_domain(products, src, dst)
found_lines = self.env['stock.move.line'].read_group(
domain, ['product_id', 'qty_done'], ['product_id'])
# Manual ordering that circumvents ORM limitations
found_lines = sorted(
found_lines,
key=lambda res: (
res['product_id_count'],
res['qty_done'],
),
reverse=True,
)
product_dict = {p.id: p for p in products}
found_lines = [{
'id': x['product_id'][0],
'product_id': product_dict[x['product_id'][0]],
'product_id_count': x['product_id_count'],
'qty_done': x['qty_done']
} for x in found_lines]
found_lines = {l['id']: l for l in found_lines}
# Show every purchaseable product
if self.show_all_products:
products += self.env['product.product'].search(
self._get_all_products_domain())
# Show all products with supplier infos belonging to a partner
if self.show_all_partner_products or self.show_all_products:
for product in products.filtered(
lambda p: p.id not in found_lines.keys()):
found_lines.update({
product.id: {'product_id': product},
})
return found_lines
def _prepare_wizard_line_from_seller(self, vals, seller):
"""Allow to add values coming from the selected seller, which will have
more priority than existing prepared values.
:param vals: Existing wizard line dictionary vals.
:param seller: Selected seller for this line.
"""
self.ensure_one()
return {
"price_unit": seller.price,
}
def _prepare_wizard_line(self, vals, order_line=False):
"""Used to create the wizard line"""
self.ensure_one()
product_id = order_line and order_line.product_id or vals['product_id']
if self.warehouse_ids:
units_available = sum([
product_id.with_context(warehouse=wh).qty_available
for wh in self.warehouse_ids.ids
])
units_virtual_available = sum([
product_id.with_context(warehouse=wh).virtual_available
for wh in self.warehouse_ids.ids
])
else:
units_available = product_id.qty_available
units_virtual_available = product_id.virtual_available
qty_to_order = abs(
min(0, units_virtual_available - vals.get('qty_delivered', 0)))
vals['is_modified'] = bool(qty_to_order)
units_included = order_line and order_line.product_qty or qty_to_order
seller = product_id._select_seller(
partner_id=self.order_id.partner_id,
date=fields.Date.today(),
quantity=units_included,
uom_id=product_id.uom_po_id,
)
res = {
'purchase_line_id': order_line and order_line.id,
'product_id': product_id.id,
'times_delivered': vals.get('times_delivered', 0),
'times_received': vals.get('times_received', 0),
'units_received': vals.get('qty_received', 0),
'units_available': units_available,
'units_virtual_available': units_virtual_available,
'units_avg_delivered': (vals.get('qty_delivered', 0) /
self._get_total_days()),
'units_delivered': vals.get('qty_delivered', 0),
'units_included': units_included,
'is_modified': vals.get('is_modified', False),
}
res.update(self._prepare_wizard_line_from_seller(res, seller))
return res
@api.multi
@api.onchange('order_id', 'date_begin', 'date_end', 'line_amount',
'show_all_partner_products', 'show_all_products',
'product_category_ids', 'warehouse_ids')
def _generate_recommendations(self):
"""Generate lines according to received and delivered items"""
self.line_ids = False
# Get quantities received from suppliers
found_dict = self._find_move_line(src='supplier', dst='internal')
for product, line in found_dict.items():
found_dict[product]['qty_received'] = line.get('qty_done', 0)
found_dict[product]['times_received'] = line.get(
'product_id_count', 0)
# Get quantities delivered to customers
found_delivered_dict = self._find_move_line(
src='internal', dst='customer')
# Merge the two dicts
for product, line in found_delivered_dict.items():
if not found_dict.get(product):
found_dict[product] = line
found_dict[product]['qty_delivered'] = line.get('qty_done', 0)
found_dict[product]['times_delivered'] = line.get(
'product_id_count', 0)
found_dict[product].update(
{
k: v for k, v in line.items()
if k not in found_dict[product].keys()
}
)
RecomendationLine = self.env['purchase.order.recommendation.line']
existing_product_ids = []
# Add products from purchase order lines
for order_line in self.order_id.order_line:
found_line = found_dict.get(order_line.product_id.id, {})
new_line = RecomendationLine.new(
self._prepare_wizard_line(found_line, order_line))
self.line_ids += new_line
existing_product_ids.append(order_line.product_id.id)
# Add those recommendations too
i = 0
for product, line in found_dict.items():
if product in existing_product_ids:
continue
new_line = RecomendationLine.new(
self._prepare_wizard_line(line)
)
self.line_ids += new_line
# Limit number of results. It has to be done here, as we need to
# populate all results first, for being able to select best matches
i += 1
if i == self.line_amount:
break
self.line_ids = self.line_ids.sorted(key=lambda x: x.product_id.name)
@api.multi
def action_accept(self):
"""Propagate recommendations to purchase order."""
po_lines = self.env['purchase.order.line']
sequence = max(self.order_id.mapped('order_line.sequence') or [0])
for wiz_line in self.line_ids.filtered('is_modified'):
# Use preexisting line if any
if wiz_line.purchase_line_id:
if wiz_line.units_included:
wiz_line.purchase_line_id.update(
wiz_line._prepare_update_po_line()
)
wiz_line.purchase_line_id._onchange_quantity()
else:
wiz_line.purchase_line_id.unlink()
continue
sequence += 1
# Use a new in-memory line otherwise
po_line = po_lines.new(
wiz_line._prepare_new_po_line(sequence)
)
po_line.onchange_product_id()
po_line.product_qty = wiz_line.units_included
po_line._onchange_quantity()
po_lines |= po_line
self.order_id.order_line |= po_lines
class PurchaseOrderRecommendationLine(models.TransientModel):
_name = 'purchase.order.recommendation.line'
_description = 'Recommended product for current purchase order'
_order = 'id'
currency_id = fields.Many2one(
related='product_id.currency_id',
readonly=True,
)
partner_id = fields.Many2one(
related='wizard_id.order_id.partner_id',
readonly=True,
)
product_id = fields.Many2one(
comodel_name='product.product',
string='Product',
)
price_unit = fields.Monetary(
readonly=True,
)
times_delivered = fields.Integer(
readonly=True,
)
times_received = fields.Integer(
readonly=True,
)
units_received = fields.Float(
readonly=True,
)
units_delivered = fields.Float(
readonly=True,
)
units_avg_delivered = fields.Float(
digits=dp.get_precision('Product Unit of Measure'),
readonly=True,
)
units_available = fields.Float(
readonly=True,
)
units_virtual_available = fields.Float(
readonly=True,
)
units_included = fields.Float()
wizard_id = fields.Many2one(
comodel_name='purchase.order.recommendation',
string='Wizard',
ondelete='cascade',
required=True,
readonly=True,
)
purchase_line_id = fields.Many2one(
comodel_name='purchase.order.line',
)
is_modified = fields.Boolean()
@api.onchange('units_included')
def _onchange_units_included(self):
self.is_modified = bool(self.purchase_line_id or self.units_included)
self.price_unit = self.product_id._select_seller(
partner_id=self.partner_id,
date=fields.Date.today(),
quantity=self.units_included,
uom_id=self.product_id.uom_po_id,
).price
@api.multi
def _prepare_update_po_line(self):
"""So we can extend PO update"""
return {
'product_qty': self.units_included,
}
@api.multi
def _prepare_new_po_line(self, sequence):
"""So we can extend PO create"""
return {
'order_id': self.wizard_id.order_id.id,
'product_id': self.product_id.id,
# set this related field manually, as there's a glitch
# in ORM that doesn't set its initial value
'partner_id': self.partner_id.id,
'sequence': sequence,
}

View file

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="purchase_order_recommendation_view_form"
model="ir.ui.view">
<field name="name">Recommended Products for this Customer</field>
<field name="model">purchase.order.recommendation</field>
<field name="arch" type="xml">
<form>
<sheet>
<group name="group_sup">
<group name="col_left">
<field name="order_id" invisible="1"/>
<field name="warehouse_count" invisible="1"/>
<field name="date_begin"/>
<field name="date_end"/>
<field name="product_category_ids" widget="many2many_tags" options="{'no_create': True}"/>
<field name="warehouse_ids"
widget="many2many_tags"
options="{'no_open': True, 'no_create': True}"
attrs="{'invisible': [('warehouse_count', '&lt;', 2)]}"/>
<field name="line_amount" widget="numeric_step"/>
</group>
<group name="col_right">
<field name="show_all_partner_products" attrs="{'invisible': [('show_all_products', '!=', False)]}"/>
<field name="show_all_products"/>
</group>
</group>
<group>
<field name="line_ids" nolabel="1">
<tree decoration-info="purchase_line_id != False"
decoration-success="units_included &gt; 0 and not purchase_line_id"
decoration-danger="units_virtual_available &lt; 0 and units_included &lt; abs(units_virtual_available)"
decoration-muted="units_received == 0 and units_delivered == 0"
create="0" delete="0" editable="top">
<field name="currency_id" invisible="1"/>
<field name="purchase_line_id" invisible="1"/>
<field name="is_modified" invisible="1"/>
<field name="product_id" readonly="1" force_save="1"/>
<field name="price_unit" string="Price"/>
<field name="units_available" string="Qty On Hand"/>
<field name="units_virtual_available" string="Forecasted Qty"/>
<field name="units_received" string="Qty Received"/>
<field name="units_delivered" string="Qty delivered" class="oe_bold"/>
<field name="units_avg_delivered" string="Qty Dlvd./day"/>
<field name="units_included" string="Qty" widget="numeric_step"/>
</tree>
</field>
</group>
</sheet>
<footer>
<button name="action_accept"
type="object"
string="Accept"
class="oe_highlight"/>
<button special="cancel"
string="Cancel"/>
</footer>
</form>
</field>
</record>
<record id="purchase_order_recommendation_action"
model="ir.actions.act_window">
<field name="name">Recommended Products for this Customer</field>
<field name="res_model">purchase.order.recommendation</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>