From 3751379f1e9a4c215fb6eb898b4ccc67659b9ace Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Tue, 10 May 2022 21:51:50 +0700 Subject: initial commit 2 --- addons/product_expiry/__init__.py | 4 + addons/product_expiry/__manifest__.py | 32 ++ addons/product_expiry/data/product_expiry_data.xml | 15 + addons/product_expiry/i18n/am.po | 159 +++++++ addons/product_expiry/i18n/ar.po | 419 +++++++++++++++++ addons/product_expiry/i18n/az.po | 187 ++++++++ addons/product_expiry/i18n/bg.po | 413 +++++++++++++++++ addons/product_expiry/i18n/bn.po | 405 +++++++++++++++++ addons/product_expiry/i18n/bs.po | 197 ++++++++ addons/product_expiry/i18n/ca.po | 409 +++++++++++++++++ addons/product_expiry/i18n/ckb.po | 403 +++++++++++++++++ addons/product_expiry/i18n/cs.po | 432 ++++++++++++++++++ addons/product_expiry/i18n/da.po | 437 ++++++++++++++++++ addons/product_expiry/i18n/de.po | 418 +++++++++++++++++ addons/product_expiry/i18n/el.po | 408 +++++++++++++++++ addons/product_expiry/i18n/eo.po | 399 ++++++++++++++++ addons/product_expiry/i18n/es.po | 436 ++++++++++++++++++ addons/product_expiry/i18n/es_BO.po | 160 +++++++ addons/product_expiry/i18n/es_CO.po | 178 ++++++++ addons/product_expiry/i18n/es_CR.po | 160 +++++++ addons/product_expiry/i18n/es_DO.po | 168 +++++++ addons/product_expiry/i18n/es_EC.po | 177 ++++++++ addons/product_expiry/i18n/es_MX.po | 435 ++++++++++++++++++ addons/product_expiry/i18n/es_VE.po | 160 +++++++ addons/product_expiry/i18n/et.po | 422 +++++++++++++++++ addons/product_expiry/i18n/eu.po | 410 +++++++++++++++++ addons/product_expiry/i18n/fa.po | 409 +++++++++++++++++ addons/product_expiry/i18n/fi.po | 413 +++++++++++++++++ addons/product_expiry/i18n/fr.po | 444 ++++++++++++++++++ addons/product_expiry/i18n/gl.po | 159 +++++++ addons/product_expiry/i18n/gu.po | 190 ++++++++ addons/product_expiry/i18n/he.po | 418 +++++++++++++++++ addons/product_expiry/i18n/hi.po | 399 ++++++++++++++++ addons/product_expiry/i18n/hr.po | 414 +++++++++++++++++ addons/product_expiry/i18n/hu.po | 416 +++++++++++++++++ addons/product_expiry/i18n/id.po | 411 +++++++++++++++++ addons/product_expiry/i18n/is.po | 193 ++++++++ addons/product_expiry/i18n/it.po | 421 +++++++++++++++++ addons/product_expiry/i18n/ja.po | 408 +++++++++++++++++ addons/product_expiry/i18n/ka.po | 408 +++++++++++++++++ addons/product_expiry/i18n/kab.po | 163 +++++++ addons/product_expiry/i18n/km.po | 187 ++++++++ addons/product_expiry/i18n/ko.po | 408 +++++++++++++++++ addons/product_expiry/i18n/lb.po | 190 ++++++++ addons/product_expiry/i18n/lt.po | 414 +++++++++++++++++ addons/product_expiry/i18n/lv.po | 399 ++++++++++++++++ addons/product_expiry/i18n/mk.po | 176 ++++++++ addons/product_expiry/i18n/mn.po | 416 +++++++++++++++++ addons/product_expiry/i18n/nb.po | 407 +++++++++++++++++ addons/product_expiry/i18n/nl.po | 438 ++++++++++++++++++ addons/product_expiry/i18n/nl_BE.po | 160 +++++++ addons/product_expiry/i18n/pl.po | 419 +++++++++++++++++ addons/product_expiry/i18n/product_expiry.pot | 399 ++++++++++++++++ addons/product_expiry/i18n/pt.po | 413 +++++++++++++++++ addons/product_expiry/i18n/pt_BR.po | 438 ++++++++++++++++++ addons/product_expiry/i18n/ro.po | 433 ++++++++++++++++++ addons/product_expiry/i18n/ru.po | 415 +++++++++++++++++ addons/product_expiry/i18n/si.po | 399 ++++++++++++++++ addons/product_expiry/i18n/sk.po | 415 +++++++++++++++++ addons/product_expiry/i18n/sl.po | 408 +++++++++++++++++ addons/product_expiry/i18n/sr.po | 190 ++++++++ addons/product_expiry/i18n/sr@latin.po | 202 +++++++++ addons/product_expiry/i18n/sv.po | 404 +++++++++++++++++ addons/product_expiry/i18n/th.po | 193 ++++++++ addons/product_expiry/i18n/tr.po | 439 ++++++++++++++++++ addons/product_expiry/i18n/uk.po | 416 +++++++++++++++++ addons/product_expiry/i18n/ur.po | 399 ++++++++++++++++ addons/product_expiry/i18n/vi.po | 436 ++++++++++++++++++ addons/product_expiry/i18n/zh_CN.po | 412 +++++++++++++++++ addons/product_expiry/i18n/zh_TW.po | 405 +++++++++++++++++ addons/product_expiry/models/__init__.py | 10 + addons/product_expiry/models/product_product.py | 35 ++ addons/product_expiry/models/production_lot.py | 134 ++++++ .../product_expiry/models/res_config_settings.py | 21 + addons/product_expiry/models/stock_move.py | 22 + addons/product_expiry/models/stock_move_line.py | 37 ++ addons/product_expiry/models/stock_picking.py | 38 ++ addons/product_expiry/models/stock_quant.py | 33 ++ .../product_expiry/report/report_deliveryslip.xml | 22 + .../product_expiry/report/report_lot_barcode.xml | 62 +++ addons/product_expiry/security/ir.model.access.csv | 2 + addons/product_expiry/security/stock_security.xml | 9 + .../static/img/product_product_from-image.jpg | Bin 0 -> 2622 bytes .../static/img/product_product_jambon-image.jpg | Bin 0 -> 2224 bytes .../static/img/product_product_lait-image.jpg | Bin 0 -> 3224 bytes .../static/img/product_product_pain-image.jpg | Bin 0 -> 2712 bytes addons/product_expiry/tests/__init__.py | 4 + .../tests/test_stock_production_lot.py | 501 +++++++++++++++++++++ .../views/product_template_views.xml | 38 ++ .../product_expiry/views/production_lot_views.xml | 55 +++ .../views/res_config_settings_views.xml | 23 + addons/product_expiry/views/stock_move_views.xml | 44 ++ addons/product_expiry/views/stock_quant_views.xml | 55 +++ addons/product_expiry/wizard/__init__.py | 3 + addons/product_expiry/wizard/confirm_expiry.py | 51 +++ .../product_expiry/wizard/confirm_expiry_view.xml | 34 ++ 96 files changed, 24372 insertions(+) create mode 100644 addons/product_expiry/__init__.py create mode 100644 addons/product_expiry/__manifest__.py create mode 100644 addons/product_expiry/data/product_expiry_data.xml create mode 100644 addons/product_expiry/i18n/am.po create mode 100644 addons/product_expiry/i18n/ar.po create mode 100644 addons/product_expiry/i18n/az.po create mode 100644 addons/product_expiry/i18n/bg.po create mode 100644 addons/product_expiry/i18n/bn.po create mode 100644 addons/product_expiry/i18n/bs.po create mode 100644 addons/product_expiry/i18n/ca.po create mode 100644 addons/product_expiry/i18n/ckb.po create mode 100644 addons/product_expiry/i18n/cs.po create mode 100644 addons/product_expiry/i18n/da.po create mode 100644 addons/product_expiry/i18n/de.po create mode 100644 addons/product_expiry/i18n/el.po create mode 100644 addons/product_expiry/i18n/eo.po create mode 100644 addons/product_expiry/i18n/es.po create mode 100644 addons/product_expiry/i18n/es_BO.po create mode 100644 addons/product_expiry/i18n/es_CO.po create mode 100644 addons/product_expiry/i18n/es_CR.po create mode 100644 addons/product_expiry/i18n/es_DO.po create mode 100644 addons/product_expiry/i18n/es_EC.po create mode 100644 addons/product_expiry/i18n/es_MX.po create mode 100644 addons/product_expiry/i18n/es_VE.po create mode 100644 addons/product_expiry/i18n/et.po create mode 100644 addons/product_expiry/i18n/eu.po create mode 100644 addons/product_expiry/i18n/fa.po create mode 100644 addons/product_expiry/i18n/fi.po create mode 100644 addons/product_expiry/i18n/fr.po create mode 100644 addons/product_expiry/i18n/gl.po create mode 100644 addons/product_expiry/i18n/gu.po create mode 100644 addons/product_expiry/i18n/he.po create mode 100644 addons/product_expiry/i18n/hi.po create mode 100644 addons/product_expiry/i18n/hr.po create mode 100644 addons/product_expiry/i18n/hu.po create mode 100644 addons/product_expiry/i18n/id.po create mode 100644 addons/product_expiry/i18n/is.po create mode 100644 addons/product_expiry/i18n/it.po create mode 100644 addons/product_expiry/i18n/ja.po create mode 100644 addons/product_expiry/i18n/ka.po create mode 100644 addons/product_expiry/i18n/kab.po create mode 100644 addons/product_expiry/i18n/km.po create mode 100644 addons/product_expiry/i18n/ko.po create mode 100644 addons/product_expiry/i18n/lb.po create mode 100644 addons/product_expiry/i18n/lt.po create mode 100644 addons/product_expiry/i18n/lv.po create mode 100644 addons/product_expiry/i18n/mk.po create mode 100644 addons/product_expiry/i18n/mn.po create mode 100644 addons/product_expiry/i18n/nb.po create mode 100644 addons/product_expiry/i18n/nl.po create mode 100644 addons/product_expiry/i18n/nl_BE.po create mode 100644 addons/product_expiry/i18n/pl.po create mode 100644 addons/product_expiry/i18n/product_expiry.pot create mode 100644 addons/product_expiry/i18n/pt.po create mode 100644 addons/product_expiry/i18n/pt_BR.po create mode 100644 addons/product_expiry/i18n/ro.po create mode 100644 addons/product_expiry/i18n/ru.po create mode 100644 addons/product_expiry/i18n/si.po create mode 100644 addons/product_expiry/i18n/sk.po create mode 100644 addons/product_expiry/i18n/sl.po create mode 100644 addons/product_expiry/i18n/sr.po create mode 100644 addons/product_expiry/i18n/sr@latin.po create mode 100644 addons/product_expiry/i18n/sv.po create mode 100644 addons/product_expiry/i18n/th.po create mode 100644 addons/product_expiry/i18n/tr.po create mode 100644 addons/product_expiry/i18n/uk.po create mode 100644 addons/product_expiry/i18n/ur.po create mode 100644 addons/product_expiry/i18n/vi.po create mode 100644 addons/product_expiry/i18n/zh_CN.po create mode 100644 addons/product_expiry/i18n/zh_TW.po create mode 100644 addons/product_expiry/models/__init__.py create mode 100644 addons/product_expiry/models/product_product.py create mode 100644 addons/product_expiry/models/production_lot.py create mode 100644 addons/product_expiry/models/res_config_settings.py create mode 100644 addons/product_expiry/models/stock_move.py create mode 100644 addons/product_expiry/models/stock_move_line.py create mode 100644 addons/product_expiry/models/stock_picking.py create mode 100644 addons/product_expiry/models/stock_quant.py create mode 100644 addons/product_expiry/report/report_deliveryslip.xml create mode 100644 addons/product_expiry/report/report_lot_barcode.xml create mode 100644 addons/product_expiry/security/ir.model.access.csv create mode 100644 addons/product_expiry/security/stock_security.xml create mode 100644 addons/product_expiry/static/img/product_product_from-image.jpg create mode 100644 addons/product_expiry/static/img/product_product_jambon-image.jpg create mode 100644 addons/product_expiry/static/img/product_product_lait-image.jpg create mode 100644 addons/product_expiry/static/img/product_product_pain-image.jpg create mode 100644 addons/product_expiry/tests/__init__.py create mode 100644 addons/product_expiry/tests/test_stock_production_lot.py create mode 100644 addons/product_expiry/views/product_template_views.xml create mode 100644 addons/product_expiry/views/production_lot_views.xml create mode 100644 addons/product_expiry/views/res_config_settings_views.xml create mode 100644 addons/product_expiry/views/stock_move_views.xml create mode 100644 addons/product_expiry/views/stock_quant_views.xml create mode 100644 addons/product_expiry/wizard/__init__.py create mode 100644 addons/product_expiry/wizard/confirm_expiry.py create mode 100644 addons/product_expiry/wizard/confirm_expiry_view.xml (limited to 'addons/product_expiry') diff --git a/addons/product_expiry/__init__.py b/addons/product_expiry/__init__.py new file mode 100644 index 00000000..33bbab56 --- /dev/null +++ b/addons/product_expiry/__init__.py @@ -0,0 +1,4 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models +from . import wizard diff --git a/addons/product_expiry/__manifest__.py b/addons/product_expiry/__manifest__.py new file mode 100644 index 00000000..60f38223 --- /dev/null +++ b/addons/product_expiry/__manifest__.py @@ -0,0 +1,32 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. +{ + 'name': 'Products Expiration Date', + 'category': 'Inventory/Inventory', + 'depends': ['stock'], + 'demo': [], + 'description': """ +Track different dates on products and production lots. +====================================================== + +Following dates can be tracked: +------------------------------- + - end of life + - best before date + - removal date + - alert date + +Also implements the removal strategy First Expiry First Out (FEFO) widely used, for example, in food industries. +""", + 'data': ['security/ir.model.access.csv', + 'security/stock_security.xml', + 'views/production_lot_views.xml', + 'views/product_template_views.xml', + 'views/res_config_settings_views.xml', + 'views/stock_move_views.xml', + 'views/stock_quant_views.xml', + 'wizard/confirm_expiry_view.xml', + 'report/report_deliveryslip.xml', + 'report/report_lot_barcode.xml', + 'data/product_expiry_data.xml'], + 'license': 'LGPL-3', +} diff --git a/addons/product_expiry/data/product_expiry_data.xml b/addons/product_expiry/data/product_expiry_data.xml new file mode 100644 index 00000000..f0effeaa --- /dev/null +++ b/addons/product_expiry/data/product_expiry_data.xml @@ -0,0 +1,15 @@ + + + + First Expiry First Out (FEFO) + fefo + + + Alert Date Reached + default + + fa-tasks + 0 + + + diff --git a/addons/product_expiry/i18n/am.po b/addons/product_expiry/i18n/am.po new file mode 100644 index 00000000..cfc52e63 --- /dev/null +++ b/addons/product_expiry/i18n/am.po @@ -0,0 +1,159 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_expiry +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Odoo 9.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-08-18 14:07+0000\n" +"PO-Revision-Date: 2015-09-08 07:20+0000\n" +"Last-Translator: Martin Trigaux\n" +"Language-Team: Amharic (http://www.transifex.com/odoo/odoo-9/language/am/)\n" +"Language: am\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_stock_production_lot_alert_date +msgid "Alert Date" +msgstr "ማሳወቅያ ቀን" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_stock_production_lot_use_date +msgid "Best before Date" +msgstr "ያለፍ ጥሩ ቀናቶች" + +#. module: product_expiry +#: model:product.product,name:product_expiry.product_product_pain +#: model:product.template,name:product_expiry.product_product_pain_product_template +msgid "Bread" +msgstr "" + +#. module: product_expiry +#: model:product.product,name:product_expiry.product_product_lait +#: model:product.template,name:product_expiry.product_product_lait_product_template +msgid "Cow milk" +msgstr "" + +#. module: product_expiry +#: model_terms:ir.ui.view,arch_db:product_expiry.view_move_form_expiry +msgid "Dates" +msgstr "" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_stock_production_lot_life_date +msgid "End of Life Date" +msgstr "የመጨረሻው ቀን" + +#. module: product_expiry +#: model:product.product,name:product_expiry.product_product_jambon +#: model:product.template,name:product_expiry.product_product_jambon_product_template +msgid "French cheese Camembert" +msgstr "" + +#. module: product_expiry +#: model:product.product,name:product_expiry.product_product_from +#: model:product.template,name:product_expiry.product_product_from_product_template +msgid "Ham" +msgstr "" + +#. module: product_expiry +#: model:ir.model,name:product_expiry.model_stock_production_lot +msgid "Lot/Serial" +msgstr "" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_product_template_alert_time +msgid "Product Alert Time" +msgstr "እቃውን የማሳወቅያ ግዜ" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_product_template_life_time +msgid "Product Life Time" +msgstr "የእቃው የመቆያ ግዜ" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_product_template_removal_time +msgid "Product Removal Time" +msgstr "የእቃው የሚወገድበት ግዜ" + +#. module: product_expiry +#: model:ir.model,name:product_expiry.model_product_template +msgid "Product Template" +msgstr "የእቃው ማሳያ" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_product_template_use_time +msgid "Product Use Time" +msgstr "የእቃው መጠቀምያ ግዜ" + +#. module: product_expiry +#: model:ir.model,name:product_expiry.model_stock_quant +msgid "Quants" +msgstr "" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_stock_production_lot_removal_date +#: model:ir.model.fields,field_description:product_expiry.field_stock_quant_removal_date +msgid "Removal Date" +msgstr "የሚወገድበት ቀን" + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_stock_production_lot_alert_date +msgid "" +"This is the date on which an alert should be notified about the goods with " +"this Serial Number." +msgstr "" + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_stock_production_lot_life_date +msgid "" +"This is the date on which the goods with this Serial Number may become " +"dangerous and must not be consumed." +msgstr "" + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_stock_production_lot_removal_date +msgid "" +"This is the date on which the goods with this Serial Number should be " +"removed from the stock." +msgstr "" + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_stock_production_lot_use_date +msgid "" +"This is the date on which the goods with this Serial Number start " +"deteriorating, without being dangerous yet." +msgstr "" + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_product_template_alert_time +msgid "" +"When a new a Serial Number is issued, this is the number of days before an " +"alert should be notified." +msgstr "" + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_product_template_life_time +msgid "" +"When a new a Serial Number is issued, this is the number of days before the " +"goods may become dangerous and must not be consumed." +msgstr "" + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_product_template_removal_time +msgid "" +"When a new a Serial Number is issued, this is the number of days before the " +"goods should be removed from the stock." +msgstr "" + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_product_template_use_time +msgid "" +"When a new a Serial Number is issued, this is the number of days before the " +"goods starts deteriorating, without being dangerous yet." +msgstr "" diff --git a/addons/product_expiry/i18n/ar.po b/addons/product_expiry/i18n/ar.po new file mode 100644 index 00000000..da4d2e9a --- /dev/null +++ b/addons/product_expiry/i18n/ar.po @@ -0,0 +1,419 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_expiry +# +# Translators: +# Sherif Abd Ekmoniem , 2020 +# Mustafa Rawi , 2020 +# amrnegm , 2020 +# Martin Trigaux, 2020 +# Osoul , 2020 +# Mohammed Albasha , 2020 +# Mohammed Ibrahim , 2020 +# Ghaith Gammar , 2020 +# Osama Ahmaro , 2020 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server saas~13.5\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-09-01 07:29+0000\n" +"PO-Revision-Date: 2020-09-07 08:17+0000\n" +"Last-Translator: Osama Ahmaro , 2020\n" +"Language-Team: Arabic (https://www.transifex.com/odoo/teams/41243/ar/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: ar\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" + +#. module: product_expiry +#: model_terms:ir.ui.view,arch_db:product_expiry.view_move_form_expiry +msgid "" +", 2020 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server saas~13.5\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-09-01 07:29+0000\n" +"PO-Revision-Date: 2020-09-07 08:17+0000\n" +"Last-Translator: Haval Abdulkarim , 2020\n" +"Language-Team: Central Kurdish (https://www.transifex.com/odoo/teams/41243/ckb/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: ckb\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: product_expiry +#: model_terms:ir.ui.view,arch_db:product_expiry.view_move_form_expiry +msgid "" +"" + +#. module: product_expiry +#: model_terms:ir.ui.view,arch_db:product_expiry.view_product_form_expiry +msgid " days" +msgstr " jours" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_stock_production_lot__alert_date +msgid "Alert Date" +msgstr "Date d'alerte" + +#. module: product_expiry +#: model:mail.activity.type,name:product_expiry.mail_activity_type_alert_date_reached +msgid "Alert Date Reached" +msgstr "Date d'Alerte Atteinte" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_product_product__alert_time +#: model:ir.model.fields,field_description:product_expiry.field_product_template__alert_time +msgid "Alert Time" +msgstr "Délai avant Alerte" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_product_product__use_time +#: model:ir.model.fields,field_description:product_expiry.field_product_template__use_time +msgid "Best Before Time" +msgstr "Date Limite d'Utilisation Optimale" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_stock_production_lot__use_date +msgid "Best before Date" +msgstr "Date limite d'utilisation optimale" + +#. module: product_expiry +#: model_terms:ir.ui.view,arch_db:product_expiry.report_lot_label_expiry +msgid "Best before:" +msgstr "A utiliser avant:" + +#. module: product_expiry +#: model:ir.model,name:product_expiry.model_res_config_settings +msgid "Config Settings" +msgstr "Paramètres de config" + +#. module: product_expiry +#: model_terms:ir.ui.view,arch_db:product_expiry.confirm_expiry_view +msgid "Confirm" +msgstr "Confirmer" + +#. module: product_expiry +#: model:ir.model,name:product_expiry.model_expiry_picking_confirmation +msgid "Confirm Expiry" +msgstr "Confirmez expiration" + +#. module: product_expiry +#: code:addons/product_expiry/models/stock_picking.py:0 +#: model_terms:ir.ui.view,arch_db:product_expiry.confirm_expiry_view +#, python-format +msgid "Confirmation" +msgstr "Confirmation" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_expiry_picking_confirmation__create_uid +msgid "Created by" +msgstr "Créé par" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_expiry_picking_confirmation__create_date +msgid "Created on" +msgstr "Créé le" + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_stock_production_lot__alert_date +msgid "" +"Date to determine the expired lots and serial numbers using the filter " +"\"Expiration Alerts\"." +msgstr "" +"Date servant à déterminer les lots et numéros de série expirés à l'aide du " +"filtre « Alertes d'expiration »." + +#. module: product_expiry +#: model_terms:ir.ui.view,arch_db:product_expiry.view_move_form_expiry +#: model_terms:ir.ui.view,arch_db:product_expiry.view_product_form_expiry +msgid "Dates" +msgstr "Dates" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_expiry_picking_confirmation__description +msgid "Description" +msgstr "Description" + +#. module: product_expiry +#: model_terms:ir.ui.view,arch_db:product_expiry.confirm_expiry_view +msgid "Discard" +msgstr "Annuler" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_res_config_settings__group_expiry_date_on_delivery_slip +msgid "Display Expiration Dates on Delivery Slips" +msgstr "Afficher les dates d'expiration sur les bons de livraison" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_expiry_picking_confirmation__display_name +#: model:ir.model.fields,field_description:product_expiry.field_procurement_group__display_name +#: model:ir.model.fields,field_description:product_expiry.field_product_product__display_name +#: model:ir.model.fields,field_description:product_expiry.field_product_template__display_name +#: model:ir.model.fields,field_description:product_expiry.field_res_config_settings__display_name +#: model:ir.model.fields,field_description:product_expiry.field_stock_move__display_name +#: model:ir.model.fields,field_description:product_expiry.field_stock_move_line__display_name +#: model:ir.model.fields,field_description:product_expiry.field_stock_picking__display_name +#: model:ir.model.fields,field_description:product_expiry.field_stock_production_lot__display_name +#: model:ir.model.fields,field_description:product_expiry.field_stock_quant__display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: product_expiry +#: model_terms:ir.ui.view,arch_db:product_expiry.quant_search_view_inherit_product_expiry +#: model_terms:ir.ui.view,arch_db:product_expiry.search_product_lot_filter_inherit_product_expiry +msgid "Expiration Alerts" +msgstr "Alertes d'expiration" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_product_product__use_expiration_date +#: model:ir.model.fields,field_description:product_expiry.field_product_template__use_expiration_date +#: model:ir.model.fields,field_description:product_expiry.field_stock_move_line__expiration_date +#: model:ir.model.fields,field_description:product_expiry.field_stock_production_lot__expiration_date +#: model:ir.model.fields,field_description:product_expiry.field_stock_quant__use_expiration_date +#: model_terms:ir.ui.view,arch_db:product_expiry.stock_report_delivery_document_inherit_product_expiry +msgid "Expiration Date" +msgstr "Date de fin de validité" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_product_product__expiration_time +#: model:ir.model.fields,field_description:product_expiry.field_product_template__expiration_time +msgid "Expiration Time" +msgstr "Durée d'expiration" + +#. module: product_expiry +#: model_terms:ir.ui.view,arch_db:product_expiry.res_config_settings_view_form_stock +msgid "Expiration dates will appear on the delivery slip" +msgstr "Les dates d'expiration apparaîtront sur les bons de livraison" + +#. module: product_expiry +#: model_terms:ir.ui.view,arch_db:product_expiry.confirm_expiry_view +msgid "Expired Lot(s)" +msgstr "Lot(s) Expiré(s)" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_stock_production_lot__product_expiry_reminded +msgid "Expiry has been reminded" +msgstr "L'expiration a été rappelée" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_expiry_picking_confirmation__id +#: model:ir.model.fields,field_description:product_expiry.field_procurement_group__id +#: model:ir.model.fields,field_description:product_expiry.field_product_product__id +#: model:ir.model.fields,field_description:product_expiry.field_product_template__id +#: model:ir.model.fields,field_description:product_expiry.field_res_config_settings__id +#: model:ir.model.fields,field_description:product_expiry.field_stock_move__id +#: model:ir.model.fields,field_description:product_expiry.field_stock_move_line__id +#: model:ir.model.fields,field_description:product_expiry.field_stock_picking__id +#: model:ir.model.fields,field_description:product_expiry.field_stock_production_lot__id +#: model:ir.model.fields,field_description:product_expiry.field_stock_quant__id +msgid "ID" +msgstr "ID" + +#. module: product_expiry +#: model:res.groups,name:product_expiry.group_expiry_date_on_delivery_slip +msgid "Include expiration dates on delivery slip" +msgstr "Afficher les dates d'expiration sur les bons de livraison" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_expiry_picking_confirmation____last_update +#: model:ir.model.fields,field_description:product_expiry.field_procurement_group____last_update +#: model:ir.model.fields,field_description:product_expiry.field_product_product____last_update +#: model:ir.model.fields,field_description:product_expiry.field_product_template____last_update +#: model:ir.model.fields,field_description:product_expiry.field_res_config_settings____last_update +#: model:ir.model.fields,field_description:product_expiry.field_stock_move____last_update +#: model:ir.model.fields,field_description:product_expiry.field_stock_move_line____last_update +#: model:ir.model.fields,field_description:product_expiry.field_stock_picking____last_update +#: model:ir.model.fields,field_description:product_expiry.field_stock_production_lot____last_update +#: model:ir.model.fields,field_description:product_expiry.field_stock_quant____last_update +msgid "Last Modified on" +msgstr "Dernière modification le" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_expiry_picking_confirmation__write_uid +msgid "Last Updated by" +msgstr "Dernière mise à jour par" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_expiry_picking_confirmation__write_date +msgid "Last Updated on" +msgstr "Dernière mise à jour le" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_expiry_picking_confirmation__lot_ids +msgid "Lot" +msgstr "Lot" + +#. module: product_expiry +#: model:ir.model,name:product_expiry.model_stock_production_lot +msgid "Lot/Serial" +msgstr "Lot/N° série" + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_product_product__expiration_time +#: model:ir.model.fields,help:product_expiry.field_product_template__expiration_time +msgid "" +"Number of days after the receipt of the products (from the vendor or in " +"stock after production) after which the goods may become dangerous and must " +"not be consumed. It will be computed on the lot/serial number." +msgstr "" +"Nombre de jours après réception des produits (depuis un fournisseur ou " +"depuis la production) après lesquels les marchandises peuvent devenir " +"dangereuses et ne doivent plus être consommées. Calculé à partir du " +"lot/numéro de série." + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_product_product__alert_time +#: model:ir.model.fields,help:product_expiry.field_product_template__alert_time +msgid "" +"Number of days before the Expiration Date after which an alert should be " +"raised on the lot/serial number. It will be computed on the lot/serial " +"number." +msgstr "" +"Nombre de jours avant la Date d'Expiration pour lever une alerte sur le " +"lot/numéro de série. Calculé à partir du lot/numéro de série." + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_product_product__removal_time +#: model:ir.model.fields,help:product_expiry.field_product_template__removal_time +msgid "" +"Number of days before the Expiration Date after which the goods should be " +"removed from the stock. It will be computed on the lot/serial number." +msgstr "" +"Nombre de jours avant la Date d'Expiration pour retirer les marchandises du " +"stock. Calculé à partir du lot/numéro de série." + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_product_product__use_time +#: model:ir.model.fields,help:product_expiry.field_product_template__use_time +msgid "" +"Number of days before the Expiration Date after which the goods starts " +"deteriorating, without being dangerous yet. It will be computed on the " +"lot/serial number." +msgstr "" +"Nombre de jours avant la Date d'Expiration pour que les marchandises " +"commencent à se détériorer, sans pour autant être dangereuses. Calculé à " +"partir du lot/numéro de série." + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_expiry_picking_confirmation__picking_ids +msgid "Picking" +msgstr "Opération de manutention" + +#. module: product_expiry +#: model_terms:ir.ui.view,arch_db:product_expiry.confirm_expiry_view +msgid "Proceed except for expired one" +msgstr "Procéder sauf pour les expirés" + +#. module: product_expiry +#: model:ir.model,name:product_expiry.model_procurement_group +msgid "Procurement Group" +msgstr "Groupe d'approvisionnement" + +#. module: product_expiry +#: model:ir.model,name:product_expiry.model_product_product +msgid "Product" +msgstr "Article" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_stock_production_lot__product_expiry_alert +msgid "Product Expiry Alert" +msgstr "Alerte d'expiration de produit" + +#. module: product_expiry +#: model:ir.model,name:product_expiry.model_stock_move_line +msgid "Product Moves (Stock Move Line)" +msgstr "Mouvements d'article (Ligne de mouvement de stock)" + +#. module: product_expiry +#: model:ir.model,name:product_expiry.model_product_template +msgid "Product Template" +msgstr "Modèle d'article" + +#. module: product_expiry +#: model:ir.model,name:product_expiry.model_stock_quant +msgid "Quants" +msgstr "Quants" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_stock_production_lot__removal_date +#: model:ir.model.fields,field_description:product_expiry.field_stock_quant__removal_date +msgid "Removal Date" +msgstr "Date de retrait" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_product_product__removal_time +#: model:ir.model.fields,field_description:product_expiry.field_product_template__removal_time +msgid "Removal Time" +msgstr "Délai avant retrait" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_expiry_picking_confirmation__show_lots +msgid "Show Lots" +msgstr "Afficher les lots" + +#. module: product_expiry +#: model:ir.model,name:product_expiry.model_stock_move +msgid "Stock Move" +msgstr "Stock déplacer" + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_stock_production_lot__product_expiry_alert +msgid "The Expiration Date has been reached." +msgstr "La date d'expiration a été atteinte." + +#. module: product_expiry +#: code:addons/product_expiry/models/production_lot.py:0 +#, python-format +msgid "The alert date has been reached for this lot/serial number" +msgstr "La date d'alerte est atteinte pour ce numéro de lot/série" + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_stock_move_line__expiration_date +#: model:ir.model.fields,help:product_expiry.field_stock_production_lot__expiration_date +msgid "" +"This is the date on which the goods with this Serial Number may become " +"dangerous and must not be consumed." +msgstr "" +"Ceci est la date à laquelle les marchandises avec ce numéro de série " +"deviennent dangereux et ne doivent plus être consommées." + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_stock_production_lot__removal_date +#: model:ir.model.fields,help:product_expiry.field_stock_quant__removal_date +msgid "" +"This is the date on which the goods with this Serial Number should be " +"removed from the stock. This date will be used in FEFO removal strategy." +msgstr "" +"Il s'agit de la date à laquelle les marchandises portant ce numéro de série " +"doivent être retirées du stock. Cette date sera utilisée dans la stratégie " +"de suppression FEFO." + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_stock_production_lot__use_date +msgid "" +"This is the date on which the goods with this Serial Number start " +"deteriorating, without being dangerous yet." +msgstr "" +"Ceci est la date à laquelle les marchandises avec ce numéro de série " +"commencent à se détériorer, sans pour autant être dangereuses." + +#. module: product_expiry +#: model:ir.model,name:product_expiry.model_stock_picking +msgid "Transfer" +msgstr "Transfert" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_stock_move__use_expiration_date +#: model:ir.model.fields,field_description:product_expiry.field_stock_production_lot__use_expiration_date +msgid "Use Expiration Date" +msgstr "Utiliser la Date d'Expiration" + +#. module: product_expiry +#: model_terms:ir.ui.view,arch_db:product_expiry.report_lot_label_expiry +msgid "Use by:" +msgstr "Utilisé par :" + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_product_product__use_expiration_date +#: model:ir.model.fields,help:product_expiry.field_product_template__use_expiration_date +#: model:ir.model.fields,help:product_expiry.field_stock_move__use_expiration_date +#: model:ir.model.fields,help:product_expiry.field_stock_production_lot__use_expiration_date +#: model:ir.model.fields,help:product_expiry.field_stock_quant__use_expiration_date +msgid "" +"When this box is ticked, you have the possibility to specify dates to manage" +" product expiration, on the product and on the corresponding lot/serial " +"numbers" +msgstr "" +"Quand cette case est cochée, vous avez la possibilité de renseigner des " +"dates pour gérer l'expiration des articles, sur l'article et sur les " +"lots/numéros de série correspondants" + +#. module: product_expiry +#: code:addons/product_expiry/wizard/confirm_expiry.py:0 +#, python-format +msgid "" +"You are going to deliver some product expired lots.\n" +"Do you confirm you want to proceed ?" +msgstr "" +"Vous allez livrer certains lots d'articles expirés.\n" +"Êtes-vous sûr de vouloir continuer ?" + +#. module: product_expiry +#: code:addons/product_expiry/wizard/confirm_expiry.py:0 +#, python-format +msgid "" +"You are going to deliver the product %(product_name)s, %(lot_name)s which is expired.\n" +"Do you confirm you want to proceed ?" +msgstr "" +"Vous allez livrer l'article %(product_name)s, %(lot_name)s, dont la date d'expiration est dépassée.\n" +"Êtes-vous sûr de vouloir continuer ?" diff --git a/addons/product_expiry/i18n/gl.po b/addons/product_expiry/i18n/gl.po new file mode 100644 index 00000000..242801cb --- /dev/null +++ b/addons/product_expiry/i18n/gl.po @@ -0,0 +1,159 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_expiry +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Odoo 9.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-08-18 14:07+0000\n" +"PO-Revision-Date: 2015-09-08 07:20+0000\n" +"Last-Translator: Martin Trigaux\n" +"Language-Team: Galician (http://www.transifex.com/odoo/odoo-9/language/gl/)\n" +"Language: gl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_stock_production_lot_alert_date +msgid "Alert Date" +msgstr "Data de alerta" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_stock_production_lot_use_date +msgid "Best before Date" +msgstr "Data de caducidade" + +#. module: product_expiry +#: model:product.product,name:product_expiry.product_product_pain +#: model:product.template,name:product_expiry.product_product_pain_product_template +msgid "Bread" +msgstr "Pan" + +#. module: product_expiry +#: model:product.product,name:product_expiry.product_product_lait +#: model:product.template,name:product_expiry.product_product_lait_product_template +msgid "Cow milk" +msgstr "Leite de vaca" + +#. module: product_expiry +#: model_terms:ir.ui.view,arch_db:product_expiry.view_move_form_expiry +msgid "Dates" +msgstr "Datas" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_stock_production_lot_life_date +msgid "End of Life Date" +msgstr "Data de fin de vida" + +#. module: product_expiry +#: model:product.product,name:product_expiry.product_product_jambon +#: model:product.template,name:product_expiry.product_product_jambon_product_template +msgid "French cheese Camembert" +msgstr "" + +#. module: product_expiry +#: model:product.product,name:product_expiry.product_product_from +#: model:product.template,name:product_expiry.product_product_from_product_template +msgid "Ham" +msgstr "Xamón" + +#. module: product_expiry +#: model:ir.model,name:product_expiry.model_stock_production_lot +msgid "Lot/Serial" +msgstr "" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_product_template_alert_time +msgid "Product Alert Time" +msgstr "Tempo de alerta do produto" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_product_template_life_time +msgid "Product Life Time" +msgstr "Tempo de vida do produto" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_product_template_removal_time +msgid "Product Removal Time" +msgstr "Tempo de eliminación do produto" + +#. module: product_expiry +#: model:ir.model,name:product_expiry.model_product_template +msgid "Product Template" +msgstr "Modelo de Producto" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_product_template_use_time +msgid "Product Use Time" +msgstr "Tempo de uso do produto" + +#. module: product_expiry +#: model:ir.model,name:product_expiry.model_stock_quant +msgid "Quants" +msgstr "" + +#. module: product_expiry +#: model:ir.model.fields,field_description:product_expiry.field_stock_production_lot_removal_date +#: model:ir.model.fields,field_description:product_expiry.field_stock_quant_removal_date +msgid "Removal Date" +msgstr "Data de eliminación" + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_stock_production_lot_alert_date +msgid "" +"This is the date on which an alert should be notified about the goods with " +"this Serial Number." +msgstr "" + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_stock_production_lot_life_date +msgid "" +"This is the date on which the goods with this Serial Number may become " +"dangerous and must not be consumed." +msgstr "" + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_stock_production_lot_removal_date +msgid "" +"This is the date on which the goods with this Serial Number should be " +"removed from the stock." +msgstr "" + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_stock_production_lot_use_date +msgid "" +"This is the date on which the goods with this Serial Number start " +"deteriorating, without being dangerous yet." +msgstr "" + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_product_template_alert_time +msgid "" +"When a new a Serial Number is issued, this is the number of days before an " +"alert should be notified." +msgstr "" + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_product_template_life_time +msgid "" +"When a new a Serial Number is issued, this is the number of days before the " +"goods may become dangerous and must not be consumed." +msgstr "" + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_product_template_removal_time +msgid "" +"When a new a Serial Number is issued, this is the number of days before the " +"goods should be removed from the stock." +msgstr "" + +#. module: product_expiry +#: model:ir.model.fields,help:product_expiry.field_product_template_use_time +msgid "" +"When a new a Serial Number is issued, this is the number of days before the " +"goods starts deteriorating, without being dangerous yet." +msgstr "" diff --git a/addons/product_expiry/i18n/gu.po b/addons/product_expiry/i18n/gu.po new file mode 100644 index 00000000..4cfad569 --- /dev/null +++ b/addons/product_expiry/i18n/gu.po @@ -0,0 +1,190 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_expiry +# +# Translators: +# Martin Trigaux, 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server saas~11.5\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-09-21 13:17+0000\n" +"PO-Revision-Date: 2018-09-21 13:17+0000\n" +"Last-Translator: Martin Trigaux, 2018\n" +"Language-Team: Gujarati (https://www.transifex.com/odoo/teams/41243/gu/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: gu\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: product_expiry +#: model_terms:ir.ui.view,arch_db:product_expiry.view_move_form_expiry +#: model_terms:ir.ui.view,arch_db:product_expiry.view_move_form_expiry_simple +msgid "" +"', 0), + ('location_id.usage', '=', 'internal')]) + alert_lots = lot_stock_quants.mapped('lot_id') + + for lot in alert_lots: + lot.activity_schedule( + 'product_expiry.mail_activity_type_alert_date_reached', + user_id=lot.product_id.responsible_id.id or SUPERUSER_ID, + note=_("The alert date has been reached for this lot/serial number") + ) + alert_lots.write({ + 'product_expiry_reminded': True + }) + + def _update_date_values(self, new_date): + if new_date: + time_delta = new_date - (self.expiration_date or fields.Datetime.now()) + vals = self._get_date_values(time_delta) + vals['expiration_date'] = new_date + self.write(vals) + + def _get_date_values(self, time_delta): + ''' Return a dict with different date values updated depending of the + time_delta. Used in the onchange of `expiration_date` and when user + defines a date at the receipt. ''' + vals = {} + if self.use_date: + vals['use_date'] = self.use_date + time_delta + if self.removal_date: + vals['removal_date'] = self.removal_date + time_delta + if self.alert_date: + vals['alert_date'] = self.alert_date + time_delta + return vals + + +class ProcurementGroup(models.Model): + _inherit = 'procurement.group' + + @api.model + def _run_scheduler_tasks(self, use_new_cursor=False, company_id=False): + super(ProcurementGroup, self)._run_scheduler_tasks(use_new_cursor=use_new_cursor, company_id=company_id) + self.env['stock.production.lot']._alert_date_exceeded() + if use_new_cursor: + self.env.cr.commit() diff --git a/addons/product_expiry/models/res_config_settings.py b/addons/product_expiry/models/res_config_settings.py new file mode 100644 index 00000000..bb565c56 --- /dev/null +++ b/addons/product_expiry/models/res_config_settings.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + group_expiry_date_on_delivery_slip = fields.Boolean("Display Expiration Dates on Delivery Slips", + implied_group='product_expiry.group_expiry_date_on_delivery_slip') + + @api.onchange('group_lot_on_delivery_slip') + def _onchange_group_lot_on_delivery_slip(self): + if not self.group_lot_on_delivery_slip: + self.group_expiry_date_on_delivery_slip = False + + @api.onchange('module_product_expiry') + def _onchange_module_product_expiry(self): + if not self.module_product_expiry: + self.group_expiry_date_on_delivery_slip = False diff --git a/addons/product_expiry/models/stock_move.py b/addons/product_expiry/models/stock_move.py new file mode 100644 index 00000000..d0fc75be --- /dev/null +++ b/addons/product_expiry/models/stock_move.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import datetime + +from odoo import fields, models + + +class StockMove(models.Model): + _inherit = "stock.move" + use_expiration_date = fields.Boolean( + string='Use Expiration Date', related='product_id.use_expiration_date') + + def _generate_serial_move_line_commands(self, lot_names, origin_move_line=None): + """Override to add a default `expiration_date` into the move lines values.""" + move_lines_commands = super()._generate_serial_move_line_commands(lot_names, origin_move_line=origin_move_line) + if self.product_id.use_expiration_date: + date = fields.Datetime.today() + datetime.timedelta(days=self.product_id.expiration_time) + for move_line_command in move_lines_commands: + move_line_vals = move_line_command[2] + move_line_vals['expiration_date'] = date + return move_lines_commands diff --git a/addons/product_expiry/models/stock_move_line.py b/addons/product_expiry/models/stock_move_line.py new file mode 100644 index 00000000..b71993d8 --- /dev/null +++ b/addons/product_expiry/models/stock_move_line.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import datetime + +from odoo import api, fields, models + + +class StockMoveLine(models.Model): + _inherit = "stock.move.line" + + expiration_date = fields.Datetime(string='Expiration Date', + help='This is the date on which the goods with this Serial Number may' + ' become dangerous and must not be consumed.') + + @api.onchange('product_id', 'product_uom_id') + def _onchange_product_id(self): + res = super(StockMoveLine, self)._onchange_product_id() + if self.picking_type_use_create_lots: + if self.product_id.use_expiration_date: + self.expiration_date = fields.Datetime.today() + datetime.timedelta(days=self.product_id.expiration_time) + else: + self.expiration_date = False + return res + + @api.onchange('lot_id') + def _onchange_lot_id(self): + if not self.picking_type_use_existing_lots or not self.product_id.use_expiration_date: + return + if self.lot_id: + self.expiration_date = self.lot_id.expiration_date + else: + self.expiration_date = False + + def _assign_production_lot(self, lot): + super()._assign_production_lot(lot) + self.lot_id._update_date_values(self.expiration_date) diff --git a/addons/product_expiry/models/stock_picking.py b/addons/product_expiry/models/stock_picking.py new file mode 100644 index 00000000..35d275ba --- /dev/null +++ b/addons/product_expiry/models/stock_picking.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, _ + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + def _pre_action_done_hook(self): + res = super()._pre_action_done_hook() + # We use the 'skip_expired' context key to avoid to make the check when + # user did already confirmed the wizard about expired lots. + if res is True and not self.env.context.get('skip_expired'): + pickings_to_warn_expired = self._check_expired_lots() + if pickings_to_warn_expired: + return pickings_to_warn_expired._action_generate_expired_wizard() + return res + + def _check_expired_lots(self): + expired_pickings = self.move_line_ids.filtered(lambda ml: ml.lot_id.product_expiry_alert).picking_id + return expired_pickings + + def _action_generate_expired_wizard(self): + expired_lot_ids = self.move_line_ids.filtered(lambda ml: ml.lot_id.product_expiry_alert).lot_id.ids + context = dict(self.env.context) + context.update({ + 'default_picking_ids': [(6, 0, self.ids)], + 'default_lot_ids': [(6, 0, expired_lot_ids)], + }) + return { + 'name': _('Confirmation'), + 'type': 'ir.actions.act_window', + 'res_model': 'expiry.picking.confirmation', + 'view_mode': 'form', + 'target': 'new', + 'context': context, + } diff --git a/addons/product_expiry/models/stock_quant.py b/addons/product_expiry/models/stock_quant.py new file mode 100644 index 00000000..34e31b54 --- /dev/null +++ b/addons/product_expiry/models/stock_quant.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class StockQuant(models.Model): + _inherit = 'stock.quant' + + removal_date = fields.Datetime(related='lot_id.removal_date', store=True, readonly=False) + use_expiration_date = fields.Boolean(related='product_id.use_expiration_date', readonly=True) + + @api.model + def _get_inventory_fields_create(self): + """ Returns a list of fields user can edit when he want to create a quant in `inventory_mode`. + """ + res = super()._get_inventory_fields_create() + res += ['removal_date'] + return res + + @api.model + def _get_inventory_fields_write(self): + """ Returns a list of fields user can edit when he want to edit a quant in `inventory_mode`. + """ + res = super()._get_inventory_fields_write() + res += ['removal_date'] + return res + + @api.model + def _get_removal_strategy_order(self, removal_strategy): + if removal_strategy == 'fefo': + return 'removal_date, in_date, id' + return super(StockQuant, self)._get_removal_strategy_order(removal_strategy) diff --git a/addons/product_expiry/report/report_deliveryslip.xml b/addons/product_expiry/report/report_deliveryslip.xml new file mode 100644 index 00000000..1d390ee1 --- /dev/null +++ b/addons/product_expiry/report/report_deliveryslip.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/addons/product_expiry/report/report_lot_barcode.xml b/addons/product_expiry/report/report_lot_barcode.xml new file mode 100644 index 00000000..5e464dd9 --- /dev/null +++ b/addons/product_expiry/report/report_lot_barcode.xml @@ -0,0 +1,62 @@ + + + + + + diff --git a/addons/product_expiry/security/ir.model.access.csv b/addons/product_expiry/security/ir.model.access.csv new file mode 100644 index 00000000..3996979a --- /dev/null +++ b/addons/product_expiry/security/ir.model.access.csv @@ -0,0 +1,2 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"access_expiry_picking_confirmation","access.expiry.picking.confirmation","model_expiry_picking_confirmation","stock.group_stock_user",1,1,1,0 diff --git a/addons/product_expiry/security/stock_security.xml b/addons/product_expiry/security/stock_security.xml new file mode 100644 index 00000000..0757b8ac --- /dev/null +++ b/addons/product_expiry/security/stock_security.xml @@ -0,0 +1,9 @@ + + + + + Include expiration dates on delivery slip + + + + diff --git a/addons/product_expiry/static/img/product_product_from-image.jpg b/addons/product_expiry/static/img/product_product_from-image.jpg new file mode 100644 index 00000000..95758c75 Binary files /dev/null and b/addons/product_expiry/static/img/product_product_from-image.jpg differ diff --git a/addons/product_expiry/static/img/product_product_jambon-image.jpg b/addons/product_expiry/static/img/product_product_jambon-image.jpg new file mode 100644 index 00000000..ffcd026d Binary files /dev/null and b/addons/product_expiry/static/img/product_product_jambon-image.jpg differ diff --git a/addons/product_expiry/static/img/product_product_lait-image.jpg b/addons/product_expiry/static/img/product_product_lait-image.jpg new file mode 100644 index 00000000..ba698780 Binary files /dev/null and b/addons/product_expiry/static/img/product_product_lait-image.jpg differ diff --git a/addons/product_expiry/static/img/product_product_pain-image.jpg b/addons/product_expiry/static/img/product_product_pain-image.jpg new file mode 100644 index 00000000..d25c0220 Binary files /dev/null and b/addons/product_expiry/static/img/product_product_pain-image.jpg differ diff --git a/addons/product_expiry/tests/__init__.py b/addons/product_expiry/tests/__init__.py new file mode 100644 index 00000000..858d9530 --- /dev/null +++ b/addons/product_expiry/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_stock_production_lot diff --git a/addons/product_expiry/tests/test_stock_production_lot.py b/addons/product_expiry/tests/test_stock_production_lot.py new file mode 100644 index 00000000..170846f5 --- /dev/null +++ b/addons/product_expiry/tests/test_stock_production_lot.py @@ -0,0 +1,501 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta + +from odoo import fields +from odoo.addons.mail.tests.common import mail_new_test_user +from odoo.addons.stock.tests.common import TestStockCommon +from odoo.tests.common import Form + + +class TestStockProductionLot(TestStockCommon): + + @classmethod + def setUpClass(cls): + super(TestStockProductionLot, cls).setUpClass() + # Creates a tracked product with expiration dates. + cls.apple_product = cls.ProductObj.create({ + 'name': 'Apple', + 'type': 'product', + 'tracking': 'lot', + 'use_expiration_date': True, + 'expiration_time': 10, + 'use_time': 5, + 'removal_time': 8, + 'alert_time': 4, + }) + + def test_00_stock_production_lot(self): + """ Test Scheduled Task on lot with an alert_date in the past creates an activity """ + + # create product + self.productAAA = self.ProductObj.create({ + 'name': 'Product AAA', + 'type': 'product', + 'tracking':'lot', + 'company_id': self.env.company.id, + }) + + # create a new lot with with alert date in the past + self.lot1_productAAA = self.LotObj.create({ + 'name': 'Lot 1 ProductAAA', + 'product_id': self.productAAA.id, + 'alert_date': fields.Date.to_string(datetime.today() - relativedelta(days=15)), + 'company_id': self.env.company.id, + }) + + picking_in = self.PickingObj.create({ + 'picking_type_id': self.picking_type_in, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location + }) + + move_a = self.MoveObj.create({ + 'name': self.productAAA.name, + 'product_id': self.productAAA.id, + 'product_uom_qty': 33, + 'product_uom': self.productAAA.uom_id.id, + 'picking_id': picking_in.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location + }) + + self.assertEqual(picking_in.move_lines.state, 'draft', 'Wrong state of move line.') + picking_in.action_confirm() + self.assertEqual(picking_in.move_lines.state, 'assigned', 'Wrong state of move line.') + + # Replace pack operation of incoming shipments. + picking_in.action_assign() + move_a.move_line_ids.qty_done = 33 + move_a.move_line_ids.lot_id = self.lot1_productAAA.id + + # Transfer Incoming Shipment. + picking_in._action_done() + + # run scheduled tasks + self.env['stock.production.lot']._alert_date_exceeded() + + # check a new activity has been created + activity_id = self.env.ref('product_expiry.mail_activity_type_alert_date_reached').id + activity_count = self.env['mail.activity'].search_count([ + ('activity_type_id', '=', activity_id), + ('res_model_id', '=', self.env.ref('stock.model_stock_production_lot').id), + ('res_id', '=', self.lot1_productAAA.id) + ]) + self.assertEqual(activity_count, 1, 'No activity created while there should be one') + + # run the scheduler a second time + self.env['stock.production.lot']._alert_date_exceeded() + + # check there is still only one activity, no additional activity is created if there is already an existing activity + activity_count = self.env['mail.activity'].search_count([ + ('activity_type_id', '=', activity_id), + ('res_model_id', '=', self.env.ref('stock.model_stock_production_lot').id), + ('res_id', '=', self.lot1_productAAA.id) + ]) + self.assertEqual(activity_count, 1, 'There should be one and only one activity') + + # mark the activity as done + mail_activity = self.env['mail.activity'].search([ + ('activity_type_id', '=', activity_id), + ('res_model_id', '=', self.env.ref('stock.model_stock_production_lot').id), + ('res_id', '=', self.lot1_productAAA.id) + ]) + mail_activity.action_done() + + # check there is no more activity (because it is already done) + activity_count = self.env['mail.activity'].search_count([ + ('activity_type_id', '=', activity_id), + ('res_model_id', '=', self.env.ref('stock.model_stock_production_lot').id), + ('res_id', '=', self.lot1_productAAA.id) + ]) + self.assertEqual(activity_count, 0,"As activity is done, there shouldn't be any related activity") + + # run the scheduler a third time + self.env['stock.production.lot']._alert_date_exceeded() + + # check there is no activity created + activity_count = self.env['mail.activity'].search_count([ + ('activity_type_id', '=', activity_id), + ('res_model_id', '=', self.env.ref('stock.model_stock_production_lot').id), + ('res_id', '=',self.lot1_productAAA.id) + ]) + self.assertEqual(activity_count, 0, "As there is already an activity marked as done, there shouldn't be any related activity created for this lot") + + def test_01_stock_production_lot(self): + """ Test Scheduled Task on lot with an alert_date in future does not create an activity """ + + # create product + self.productBBB = self.ProductObj.create({ + 'name': 'Product BBB', + 'type': 'product', + 'tracking':'lot' + }) + + # create a new lot with with alert date in the past + self.lot1_productBBB = self.LotObj.create({ + 'name': 'Lot 1 ProductBBB', + 'product_id': self.productBBB.id, + 'alert_date': fields.Date.to_string(datetime.today() + relativedelta(days=15)), + 'company_id': self.env.company.id, + }) + + picking_in = self.PickingObj.create({ + 'picking_type_id': self.picking_type_in, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + + move_b = self.MoveObj.create({ + 'name': self.productBBB.name, + 'product_id': self.productBBB.id, + 'product_uom_qty': 44, + 'product_uom': self.productBBB.uom_id.id, + 'picking_id': picking_in.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + + self.assertEqual(picking_in.move_lines.state, 'draft', 'Wrong state of move line.') + picking_in.action_confirm() + self.assertEqual(picking_in.move_lines.state, 'assigned', 'Wrong state of move line.') + + # Replace pack operation of incoming shipments. + picking_in.action_assign() + move_b.move_line_ids.qty_done = 44 + move_b.move_line_ids.lot_id = self.lot1_productBBB.id + + # Transfer Incoming Shipment. + picking_in._action_done() + + # run scheduled tasks + self.env['stock.production.lot']._alert_date_exceeded() + + # check a new activity has not been created + activity_id = self.env.ref('product_expiry.mail_activity_type_alert_date_reached').id + activity_count = self.env['mail.activity'].search_count([ + ('activity_type_id', '=', activity_id), + ('res_model_id', '=', self.env.ref('stock.model_stock_production_lot').id), + ('res_id', '=', self.lot1_productBBB.id) + ]) + self.assertEqual(activity_count, 0, "An activity has been created while it shouldn't") + + def test_02_stock_production_lot(self): + """ Test Scheduled Task on lot without an alert_date does not create an activity """ + + # create product + self.productCCC = self.ProductObj.create({'name': 'Product CCC', 'type': 'product', 'tracking':'lot'}) + + # create a new lot with with alert date in the past + self.lot1_productCCC = self.LotObj.create({'name': 'Lot 1 ProductCCC', 'product_id': self.productCCC.id, 'company_id': self.env.company.id}) + + picking_in = self.PickingObj.create({ + 'picking_type_id': self.picking_type_in, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + + move_c = self.MoveObj.create({ + 'name': self.productCCC.name, + 'product_id': self.productCCC.id, + 'product_uom_qty': 44, + 'product_uom': self.productCCC.uom_id.id, + 'picking_id': picking_in.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + + self.assertEqual(picking_in.move_lines.state, 'draft', 'Wrong state of move line.') + picking_in.action_confirm() + self.assertEqual(picking_in.move_lines.state, 'assigned', 'Wrong state of move line.') + + # Replace pack operation of incoming shipments. + picking_in.action_assign() + move_c.move_line_ids.qty_done = 55 + move_c.move_line_ids.lot_id = self.lot1_productCCC.id + + # Transfer Incoming Shipment. + picking_in._action_done() + + # run scheduled tasks + self.env['stock.production.lot']._alert_date_exceeded() + + # check a new activity has not been created + activity_id = self.env.ref('product_expiry.mail_activity_type_alert_date_reached').id + activity_count = self.env['mail.activity'].search_count([ + ('activity_type_id', '=', activity_id), + ('res_model_id', '=', self.env.ref('stock.model_stock_production_lot').id), + ('res_id', '=', self.lot1_productCCC.id) + ]) + self.assertEqual(activity_count, 0, "An activity has been created while it shouldn't") + + def test_03_onchange_expiration_date(self): + """ Updates the `expiration_date` of the lot production and checks other date + fields are updated as well. """ + # Keeps track of the current datetime and set a delta for the compares. + today_date = datetime.today() + time_gap = timedelta(seconds=10) + # Creates a new lot number and saves it... + lot_form = Form(self.LotObj) + lot_form.name = 'Apple Box #1' + lot_form.product_id = self.apple_product + lot_form.company_id = self.env.company + apple_lot = lot_form.save() + # ...then checks date fields have the expected values. + self.assertAlmostEqual( + today_date + timedelta(days=self.apple_product.expiration_time), + apple_lot.expiration_date, delta=time_gap) + self.assertAlmostEqual( + today_date + timedelta(days=self.apple_product.use_time), + apple_lot.use_date, delta=time_gap) + self.assertAlmostEqual( + today_date + timedelta(days=self.apple_product.removal_time), + apple_lot.removal_date, delta=time_gap) + self.assertAlmostEqual( + today_date + timedelta(days=self.apple_product.alert_time), + apple_lot.alert_date, delta=time_gap) + + difference = timedelta(days=20) + new_date = apple_lot.expiration_date + difference + old_use_date = apple_lot.use_date + old_removal_date = apple_lot.removal_date + old_alert_date = apple_lot.alert_date + + # Modifies the lot `expiration_date`... + lot_form = Form(apple_lot) + lot_form.expiration_date = new_date + apple_lot = lot_form.save() + + # ...then checks all other date fields were correclty updated. + self.assertAlmostEqual( + apple_lot.use_date, old_use_date + difference, delta=time_gap) + self.assertAlmostEqual( + apple_lot.removal_date, old_removal_date + difference, delta=time_gap) + self.assertAlmostEqual( + apple_lot.alert_date, old_alert_date + difference, delta=time_gap) + + def test_04_expiration_date_on_receipt(self): + """ Test we can set an expiration date on receipt and all expiration + date will be correctly set. """ + partner = self.env['res.partner'].create({ + 'name': 'Apple\'s Joe', + 'company_id': self.env.ref('base.main_company').id, + }) + expiration_date = datetime.today() + timedelta(days=30) + time_gap = timedelta(seconds=10) + + # Receives a tracked production using expiration date. + picking_form = Form(self.env['stock.picking']) + picking_form.partner_id = partner + picking_form.picking_type_id = self.env.ref('stock.picking_type_in') + with picking_form.move_ids_without_package.new() as move: + move.product_id = self.apple_product + move.product_uom_qty = 4 + receipt = picking_form.save() + receipt.action_confirm() + + # Defines a date during the receipt. + move = receipt.move_ids_without_package[0] + line = move.move_line_ids[0] + self.assertEqual(move.use_expiration_date, True) + line.lot_name = 'Apple Box #2' + line.expiration_date = expiration_date + line.qty_done = 4 + + receipt._action_done() + # Get back the lot created when the picking was done... + apple_lot = self.env['stock.production.lot'].search( + [('product_id', '=', self.apple_product.id)], + limit=1, + ) + # ... and checks all date fields are correctly set. + self.assertAlmostEqual( + apple_lot.expiration_date, expiration_date, delta=time_gap) + self.assertAlmostEqual( + apple_lot.use_date, expiration_date - timedelta(days=5), delta=time_gap) + self.assertAlmostEqual( + apple_lot.removal_date, expiration_date - timedelta(days=2), delta=time_gap) + self.assertAlmostEqual( + apple_lot.alert_date, expiration_date - timedelta(days=6), delta=time_gap) + + def test_04_2_expiration_date_on_receipt(self): + """ Test we can set an expiration date on receipt even if all expiration + date related fields aren't set on product. """ + partner = self.env['res.partner'].create({ + 'name': 'Apple\'s Joe', + 'company_id': self.env.ref('base.main_company').id, + }) + # Unset some fields. + self.apple_product.expiration_time = False + self.apple_product.removal_time = False + + expiration_date = datetime.today() + timedelta(days=30) + time_gap = timedelta(seconds=10) + + # Receives a tracked production using expiration date. + picking_form = Form(self.env['stock.picking']) + picking_form.partner_id = partner + picking_form.picking_type_id = self.env.ref('stock.picking_type_in') + with picking_form.move_ids_without_package.new() as move: + move.product_id = self.apple_product + move.product_uom_qty = 4 + receipt = picking_form.save() + receipt.action_confirm() + + # Defines a date during the receipt. + move = receipt.move_ids_without_package[0] + line = move.move_line_ids[0] + self.assertEqual(move.use_expiration_date, True) + line.lot_name = 'Apple Box #3' + line.expiration_date = expiration_date + line.qty_done = 4 + + receipt._action_done() + # Get back the lot created when the picking was done... + apple_lot = self.env['stock.production.lot'].search( + [('product_id', '=', self.apple_product.id)], + limit=1, + ) + # ... and checks all date fields are correctly set. + self.assertAlmostEqual( + apple_lot.expiration_date, expiration_date, delta=time_gap, + msg="Must be define even if the product's `expiration_time` isn't set.") + self.assertAlmostEqual( + apple_lot.use_date, expiration_date + timedelta(days=5), delta=time_gap) + self.assertEqual( + apple_lot.removal_date, False, + "Must be false as the `removal_time` isn't set on product.") + self.assertAlmostEqual( + apple_lot.alert_date, expiration_date + timedelta(days=4), delta=time_gap) + + def test_05_confirmation_on_delivery(self): + """ Test when user tries to delivery expired lot, he/she gets a + confirmation wizard. """ + partner = self.env['res.partner'].create({ + 'name': 'Cider & Son', + 'company_id': self.env.ref('base.main_company').id, + }) + # Creates 3 lots (1 non-expired lot, 2 expired lots) + lot_form = Form(self.LotObj) # Creates the lot. + lot_form.name = 'good-apple-lot' + lot_form.product_id = self.apple_product + lot_form.company_id = self.env.company + good_lot = lot_form.save() + + lot_form = Form(self.LotObj) # Creates the lot. + lot_form.name = 'expired-apple-lot-01' + lot_form.product_id = self.apple_product + lot_form.company_id = self.env.company + expired_lot_1 = lot_form.save() + lot_form = Form(expired_lot_1) # Edits the lot to make it expired. + lot_form.expiration_date = datetime.today() - timedelta(days=10) + expired_lot_1 = lot_form.save() + + # Case #1: make a delivery with no expired lot. + picking_form = Form(self.env['stock.picking']) + picking_form.partner_id = partner + picking_form.picking_type_id = self.env.ref('stock.picking_type_out') + with picking_form.move_ids_without_package.new() as move: + move.product_id = self.apple_product + move.product_uom_qty = 4 + # Saves and confirms it... + delivery_1 = picking_form.save() + delivery_1.action_confirm() + # ... then create a move line with the non-expired lot and valids the picking. + delivery_1.move_line_ids_without_package = [(5, 0), (0, 0, { + 'company_id': self.env.company.id, + 'location_id': delivery_1.move_lines.location_id.id, + 'location_dest_id': delivery_1.move_lines.location_dest_id.id, + 'lot_id': good_lot.id, + 'product_id': self.apple_product.id, + 'product_uom_id': self.apple_product.uom_id.id, + 'qty_done': 4, + })] + res = delivery_1.button_validate() + # Validate a delivery for good products must not raise anything. + self.assertEqual(res, True) + + # Case #2: make a delivery with one non-expired lot and one expired lot. + picking_form = Form(self.env['stock.picking']) + picking_form.partner_id = partner + picking_form.picking_type_id = self.env.ref('stock.picking_type_out') + with picking_form.move_ids_without_package.new() as move: + move.product_id = self.apple_product + move.product_uom_qty = 8 + # Saves and confirms it... + delivery_2 = picking_form.save() + delivery_2.action_confirm() + # ... then create a move line for the non-expired lot and for an expired + # lot and valids the picking. + delivery_2.move_line_ids_without_package = [(5, 0), (0, 0, { + 'company_id': self.env.company.id, + 'location_id': delivery_2.move_lines.location_id.id, + 'location_dest_id': delivery_2.move_lines.location_dest_id.id, + 'lot_id': good_lot.id, + 'product_id': self.apple_product.id, + 'product_uom_id': self.apple_product.uom_id.id, + 'qty_done': 4, + }), (0, 0, { + 'company_id': self.env.company.id, + 'location_id': delivery_2.move_lines.location_id.id, + 'location_dest_id': delivery_2.move_lines.location_dest_id.id, + 'lot_id': expired_lot_1.id, + 'product_id': self.apple_product.id, + 'product_uom_id': self.apple_product.uom_id.id, + 'qty_done': 4, + })] + res = delivery_2.button_validate() + # Validate a delivery containing expired products must raise a confirmation wizard. + self.assertNotEqual(res, True) + self.assertEqual(res['res_model'], 'expiry.picking.confirmation') + + # Case #3: make a delivery with only on expired lot. + picking_form = Form(self.env['stock.picking']) + picking_form.partner_id = partner + picking_form.picking_type_id = self.env.ref('stock.picking_type_out') + with picking_form.move_ids_without_package.new() as move: + move.product_id = self.apple_product + move.product_uom_qty = 4 + # Saves and confirms it... + delivery_3 = picking_form.save() + delivery_3.action_confirm() + # ... then create two move lines with expired lot and valids the picking. + delivery_3.move_line_ids_without_package = [(5, 0), (0, 0, { + 'company_id': self.env.company.id, + 'location_id': delivery_3.move_lines.location_id.id, + 'location_dest_id': delivery_3.move_lines.location_dest_id.id, + 'lot_id': expired_lot_1.id, + 'product_id': self.apple_product.id, + 'product_uom_id': self.apple_product.uom_id.id, + 'qty_done': 4, + })] + res = delivery_3.button_validate() + # Validate a delivery containing expired products must raise a confirmation wizard. + self.assertNotEqual(res, True) + self.assertEqual(res['res_model'], 'expiry.picking.confirmation') + + def test_edit_removal_date_in_inventory_mode(self): + """ Try to edit removal_date with the inventory mode. + """ + user_group_stock_manager = self.env.ref('stock.group_stock_manager') + self.demo_user = mail_new_test_user( + self.env, + name='Demo user', + login='userdemo', + email='d.d@example.com', + groups='stock.group_stock_manager', + ) + lot_form = Form(self.LotObj) + lot_form.name = 'LOT001' + lot_form.product_id = self.apple_product + lot_form.company_id = self.env.company + apple_lot = lot_form.save() + + quant = self.StockQuantObj.with_context(inventory_mode=True).create({ + 'product_id': self.apple_product.id, + 'location_id': self.stock_location, + 'quantity': 10, + 'lot_id': apple_lot.id, + }) + # Try to write on quant with inventory mode + new_date = datetime.today() + timedelta(days=15) + quant.with_user(self.demo_user).with_context(inventory_mode=True).write({'removal_date': new_date}) + self.assertEqual(quant.removal_date, new_date) diff --git a/addons/product_expiry/views/product_template_views.xml b/addons/product_expiry/views/product_template_views.xml new file mode 100644 index 00000000..a6deea0d --- /dev/null +++ b/addons/product_expiry/views/product_template_views.xml @@ -0,0 +1,38 @@ + + + + product.template.inherit.form + product.template + + + + + + + + + + + + diff --git a/addons/product_expiry/views/production_lot_views.xml b/addons/product_expiry/views/production_lot_views.xml new file mode 100644 index 00000000..34a5a144 --- /dev/null +++ b/addons/product_expiry/views/production_lot_views.xml @@ -0,0 +1,55 @@ + + + + stock.production.lot.inherit.form + stock.production.lot + + + + + + + + + + + + + + + + + + + + Expiration Alert + + + + + + stock.production.lot.search.inherit + stock.production.lot + + + + + + + + + + stock.production.lot.tree.inherit.product.expiry + stock.production.lot + + + + + + + + + + + + diff --git a/addons/product_expiry/views/res_config_settings_views.xml b/addons/product_expiry/views/res_config_settings_views.xml new file mode 100644 index 00000000..1c636bed --- /dev/null +++ b/addons/product_expiry/views/res_config_settings_views.xml @@ -0,0 +1,23 @@ + + + + res.config.settings.view.form.inherit.product.expiry.stock + res.config.settings + + + +
+
+ +
+
+
+
+
+
+
+
diff --git a/addons/product_expiry/views/stock_move_views.xml b/addons/product_expiry/views/stock_move_views.xml new file mode 100644 index 00000000..98f62fe6 --- /dev/null +++ b/addons/product_expiry/views/stock_move_views.xml @@ -0,0 +1,44 @@ + + + + stock.move.operations.inherit.form + stock.move + + + + + + + + + + + stock.move.line.inherit.tree + stock.move.line + + + + + + + + + + + stock.move.line.operations.inherit.tree + stock.move.line + + + + + + + + + diff --git a/addons/product_expiry/views/stock_quant_views.xml b/addons/product_expiry/views/stock_quant_views.xml new file mode 100644 index 00000000..d15b1437 --- /dev/null +++ b/addons/product_expiry/views/stock_quant_views.xml @@ -0,0 +1,55 @@ + + + + stock.quant.inherit.form + stock.quant + + + + + + + + + + stock.quant.inherit.form + stock.quant + + + + + removal_date < current_date or quantity < 0 + + + + + + + + + + stock.quant.inherit.form + stock.quant + + + + + + + + + + + stock.quant.search.inherit + stock.quant + + + + + + + + + diff --git a/addons/product_expiry/wizard/__init__.py b/addons/product_expiry/wizard/__init__.py new file mode 100644 index 00000000..f96376b2 --- /dev/null +++ b/addons/product_expiry/wizard/__init__.py @@ -0,0 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import confirm_expiry diff --git a/addons/product_expiry/wizard/confirm_expiry.py b/addons/product_expiry/wizard/confirm_expiry.py new file mode 100644 index 00000000..d07eeacd --- /dev/null +++ b/addons/product_expiry/wizard/confirm_expiry.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ + + +class ConfirmExpiry(models.TransientModel): + _name = 'expiry.picking.confirmation' + _description = 'Confirm Expiry' + + lot_ids = fields.Many2many('stock.production.lot', readonly=True, required=True) + picking_ids = fields.Many2many('stock.picking', readonly=True) + description = fields.Char('Description', compute='_compute_descriptive_fields') + show_lots = fields.Boolean('Show Lots', compute='_compute_descriptive_fields') + + @api.depends('lot_ids') + def _compute_descriptive_fields(self): + # Shows expired lots only if we are more than one expired lot. + self.show_lots = len(self.lot_ids) > 1 + if self.show_lots: + # For multiple expired lots, they are listed in the wizard view. + self.description = _( + "You are going to deliver some product expired lots." + "\nDo you confirm you want to proceed ?" + ) + else: + # For one expired lot, its name is written in the wizard message. + self.description = _( + "You are going to deliver the product %(product_name)s, %(lot_name)s which is expired." + "\nDo you confirm you want to proceed ?", + product_name=self.lot_ids.product_id.display_name, + lot_name=self.lot_ids.name + ) + + def process(self): + picking_to_validate = self.env.context.get('button_validate_picking_ids') + if picking_to_validate: + picking_to_validate = self.env['stock.picking'].browse(picking_to_validate).with_context(skip_expired=True) + return picking_to_validate.button_validate() + return True + + def process_no_expired(self): + """ Don't process for concerned pickings (ones with expired lots), but + process for all other pickings (in case of multi). """ + # Remove `self.pick_ids` from `button_validate_picking_ids` and call + # `button_validate` with the subset (if any). + pickings_to_validate = self.env['stock.picking'].browse(self.env.context.get('button_validate_picking_ids')) + pickings_to_validate = pickings_to_validate - self.picking_ids + if pickings_to_validate: + return pickings_to_validate.with_context(skip_expired=True).button_validate() + return True diff --git a/addons/product_expiry/wizard/confirm_expiry_view.xml b/addons/product_expiry/wizard/confirm_expiry_view.xml new file mode 100644 index 00000000..d43c4255 --- /dev/null +++ b/addons/product_expiry/wizard/confirm_expiry_view.xml @@ -0,0 +1,34 @@ + + + + Confirm + expiry.picking.confirmation + +
+

+ +

+ + + + + + + + + +
+
+
-- cgit v1.2.3