diff --git a/edi_purchase_diapar_oca/README.rst b/edi_purchase_diapar_oca/README.rst new file mode 100644 index 000000000..7dd81cab7 --- /dev/null +++ b/edi_purchase_diapar_oca/README.rst @@ -0,0 +1,26 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +====================== +EDI Purchase DIAPAR +====================== + +Features +-------- + +This module contains all specific and necessary conditions and treatments to DIAPAR relationship. + + +Credits +======= + +Yassine TEIMI +Simon Mas + +Funders +------- + +The development of this module has been financially supported by: + +* La Louve (https://cooplalouve.fr/) diff --git a/edi_purchase_diapar_oca/__init__.py b/edi_purchase_diapar_oca/__init__.py new file mode 100644 index 000000000..3d5be67ea --- /dev/null +++ b/edi_purchase_diapar_oca/__init__.py @@ -0,0 +1,6 @@ +# Copyright (C) 2016-Today: Druidoo () +# @author: Druidoo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html + +from . import models +from . import components diff --git a/edi_purchase_diapar_oca/__manifest__.py b/edi_purchase_diapar_oca/__manifest__.py new file mode 100644 index 000000000..b1651e333 --- /dev/null +++ b/edi_purchase_diapar_oca/__manifest__.py @@ -0,0 +1,39 @@ +# Copyright (C) 2016-Today: Druidoo () +# @author: Druidoo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html + +{ + "name": "EDI Purchase DIAPAR", + "version": "18.0.1.0.0", + "category": "Custom", + "author": "Druidoo, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/edi-framework", + "license": "AGPL-3", + "depends": [ + "product", + "purchase_stock", + "edi_storage_oca", + "edi_purchase_oca", + "edi_exchange_template_oca", + ], + "data": [ + "security/ir.model.access.csv", + "views/supplier_price_list_views.xml", + "views/picking_update_views.xml", + "views/product_template_views.xml", + "views/res_partner_views.xml", + "views/exchange_type_views.xml", + "views/edi_exchange_template_output.xml", + "views/res_config_settings_views.xml", + "views/menus.xml", + "templates/exchange_template_output_diapar.xml", + ], + "demo": [ + "demo/fs_storage_demo.xml", + "demo/edi_backend_type_demo.xml", + "demo/edi_exchange_template_output_demo.xml", + "demo/edi_exchange_type_demo.xml", + "demo/edi_field_mapping_demo.xml", + "demo/edi_configuration_demo.xml", + ], +} diff --git a/edi_purchase_diapar_oca/components/__init__.py b/edi_purchase_diapar_oca/components/__init__.py new file mode 100644 index 000000000..b871c10f8 --- /dev/null +++ b/edi_purchase_diapar_oca/components/__init__.py @@ -0,0 +1,3 @@ +from . import input_process_ble +from . import input_process_ch +from . import generate_diapar_output diff --git a/edi_purchase_diapar_oca/components/generate_diapar_output.py b/edi_purchase_diapar_oca/components/generate_diapar_output.py new file mode 100644 index 000000000..6256a3489 --- /dev/null +++ b/edi_purchase_diapar_oca/components/generate_diapar_output.py @@ -0,0 +1,30 @@ +from odoo import models + +from odoo.addons.edi_core_oca.exceptions import EDINotImplementedError + + +class EdiOcaHandlerGenerate(models.AbstractModel): + _name = "edi.output.diapar.handler" + _inherit = [ + "edi.oca.handler.generate", + ] + _description = "EDI Handler Generate Output For Diapar" + + def generate(self, exchange_record): + exchange_record = self.env["edi.exchange.record"].browse(exchange_record.id) + tmpl = exchange_record.backend_id._get_output_template(exchange_record) + if tmpl: + exchange_record = exchange_record.with_context( + edi_framework_action="generate" + ) + tmpl = tmpl.with_context(edi_framework_action="generate") + if exchange_record.model == "purchase.order" and exchange_record.res_id: + order = self.env["purchase.order"].browse(exchange_record.res_id) + if order: + return order.generate_template_output_diapar( + tmpl, + exchange_record, + ) + raise EDINotImplementedError( + self.env._("Only purchase order with Diapar output template are supported.") + ) diff --git a/edi_purchase_diapar_oca/components/input_process_ble.py b/edi_purchase_diapar_oca/components/input_process_ble.py new file mode 100644 index 000000000..27d955678 --- /dev/null +++ b/edi_purchase_diapar_oca/components/input_process_ble.py @@ -0,0 +1,195 @@ +import logging + +from odoo import Command, models +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +class EDIInputProcessDiaparDespatchAdvice(models.AbstractModel): + """ + INPUT PROCESSOR BLE + """ + + _name = "input.process.diapar.despatch.advice" + _inherit = ["edi.oca.handler.process"] + _description = "Input process despatch advice from Diapar" + + def process(self, exchange_record): + _logger.info(">>>>>>>>>>>>>>>>>> Reading BLE file >>>>>>>>>>>>>>>>>>>>>") + picking_update, purchase_order = self._handle_create_picking_update( + exchange_record + ) + if picking_update: + picking_update.button_update_picking_order() + if not exchange_record.related_record_ids.filtered( + lambda r: r.model == "purchase.order" and r.res_id == purchase_order.id + ): + self.env["edi.exchange.related.record"].create( + { + "exchange_record_id": exchange_record.id, + "res_id": purchase_order.id, + "model": "purchase.order", + } + ) + return self.env._( + "Stock Picking %s updated. Related Order: %s", + picking_update.name.name, + purchase_order.name, + ) + return self.env._("No picking update created for this file.") + + def _parse_file_content_ble(self, exchange_record): + datas = exchange_record._get_file_content() + return datas.split("\n") + + def _get_mapping_field_position(self, exchange_type, field_name): + mapping = exchange_type.field_mapping_ids.filtered( + lambda rec: rec.mapping_field_id.name == field_name + ) + pos_from = mapping.sequence_start + pos_to = mapping.sequence_end + return pos_from, pos_to + + def _get_picking_order_and_supplier_info( + self, line, edi_exchange_type, picking_order, supplier_info, delivery_date + ): + pos_from, pos_to = self._get_mapping_field_position( + edi_exchange_type, "product_code" + ) + product_code = line[pos_from:pos_to] + purchase_order = self.env["purchase.order"] + supplier_info = self.env["product.supplierinfo"].search( + [("product_code", "=", product_code)], + limit=1, + ) + supplier_ids = supplier_info.mapped("partner_id").ids + # Look for corresponding purchase order, normally it + # should be just one even if there is more than one + # supplier associated to product + if supplier_ids: + sdate = f"{delivery_date} 00:00:00" + edate = f"{delivery_date} 23:59:59" + purchase_order = self.env["purchase.order"].search( + [ + ("partner_id", "in", supplier_ids), + ("state", "in", ["purchase", "done"]), + ("date_planned", ">=", sdate), + ("date_planned", "<=", edate), + ], + limit=1, + ) + # Assuming purchase order has only one picking order associated. + if purchase_order.exists(): + picking_order = purchase_order.picking_ids[0] + return picking_order, supplier_info, purchase_order + + def _get_updated_quantity_values( + self, line, edi_exchange_type, picking_order, supplier_info + ): + # Look for updated quantity + pos_from, pos_to = self._get_mapping_field_position( + edi_exchange_type, "product_qty" + ) + new_quantity = line[pos_from:pos_to] + # Look for ordered quantity + product_tmpl = self.env["product.template"] + if supplier_info: + product_tmpl = supplier_info[0].product_tmpl_id + ordered_product = self.env["product.product"].search( + [("product_tmpl_id", "=", product_tmpl.id)], + limit=1, + ) + ordered_operation = self.env["stock.move"].search( + [ + ("picking_id", "=", picking_order.id), + ("product_id", "=", ordered_product.id), + ] + ) + ordered_quantity = ordered_operation.quantity + # Construct one2many values + vals = dict() + if ordered_quantity != float(new_quantity): + vals.update( + { + "line_to_update_id": ordered_operation.id, + "product_id": ordered_product.id, + "ordered_quantity": ordered_quantity, + "product_qty": float(new_quantity), + } + ) + return vals + + def _reprepare_delivery_date(self, line, edi_exchange_type): + pos_from, pos_to = self._get_mapping_field_position( + edi_exchange_type, "date_planned" + ) + data = line[pos_from:pos_to] + return self.env["edi.configuration"].get_date_format_ble_yyyymmdd(data) + + def _prepare_picking_update_values(self, exchange_record): + lines = self._parse_file_content_ble(exchange_record) + edi_exchange_type = exchange_record.type_id + + if not lines: + raise ValidationError( + self.env._( + "Please configure fields mapping for BLE interface on your" + " EDI system!" + ) + ) + + delivery_date = delivery_sign = "" + picking_order = self.env["stock.picking"] + supplier_info = self.env["product.supplierinfo"] + purchase_order = self.env["purchase.order"] + proposition_vals = dict() + values_list = [] + for line in lines: + if not line or not isinstance(line, str): + continue + # This condition ensures that this job + # consider only one picking per EDI File + line = line.lstrip() + if line.startswith(edi_exchange_type.header_code) and not delivery_date: + # Header processing + delivery_date = self._reprepare_delivery_date(line, edi_exchange_type) + elif line.startswith(edi_exchange_type.lines_code): + # Look if it's a first delivery + delivery_sign = edi_exchange_type.delivery_sign + if delivery_sign == "-": + break + + # Look for picking_order, supplier_info + picking_order, supplier_info, purchase_order = ( + self._get_picking_order_and_supplier_info( + line, + edi_exchange_type, + picking_order, + supplier_info, + delivery_date, + ) + ) + # Look for updated quantity + updated_values = self._get_updated_quantity_values( + line, edi_exchange_type, picking_order, supplier_info + ) + if updated_values: + values_list += [Command.create(updated_values)] + if delivery_sign == "+": + proposition_vals.update( + { + "name": picking_order.id, + "values_proposed_ids": values_list, + } + ) + return proposition_vals, purchase_order + + def _handle_create_picking_update(self, exchange_record): + picking_update = self.env["picking.update"] + proposition_vals, purchase_order = self._prepare_picking_update_values( + exchange_record + ) + if proposition_vals: + picking_update = picking_update.create(proposition_vals) + return picking_update, purchase_order diff --git a/edi_purchase_diapar_oca/components/input_process_ch.py b/edi_purchase_diapar_oca/components/input_process_ch.py new file mode 100644 index 000000000..0593fc4a4 --- /dev/null +++ b/edi_purchase_diapar_oca/components/input_process_ch.py @@ -0,0 +1,126 @@ +import datetime +import logging + +from odoo import models +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +class EDIInputProcessDiaparPurchasePrice(models.AbstractModel): + """ + INPUT PROCESSOR CH + """ + + _name = "input.process.diapar.purchase.price" + _inherit = ["edi.oca.handler.process"] + _description = "Input process purchase price from Diapar" + + def process(self, exchange_record): + _logger.info(">>>>>>>>>>>>>>>>>> Reading CH file >>>>>>>>>>>>>>>>>>>>>") + SupplierPriceList = self.env["supplier.price.list"] + + self.create_supplier_price_list(exchange_record) + args = [ + ("product_tmpl_id", "!=", False), + ("price_updated", "=", False), + ("supplier_id", "!=", False), + ] + recs = self.env["supplier.price.list"].search(args) + products = recs.mapped("product_tmpl_id") + for product in products: + splists = recs.filtered( + lambda rec, product=product: rec.product_tmpl_id == product + ) + try: + SupplierPriceList.update_product_price_list(product, splists) + except Exception as e: + _logger.error( + "Could not update price list for the product %s: %s", + product, + str(e), + ) + + return self.env._("Process update purchase price from Diapar done!") + + def _parse_file_content_ch(self, exchange_record): + datas = exchange_record._get_file_content() + return datas.split("\n") + + def _get_mapping_field_position(self, exchange_type, field_name): + mapping = exchange_type.field_mapping_ids.filtered( + lambda rec: rec.mapping_field_id.name == field_name + ) + pos_from = mapping.sequence_start + pos_to = mapping.sequence_end + return pos_from, pos_to + + def _prepare_supplier_price_list_values(self, exchange_record): + lines = self._parse_file_content_ch(exchange_record) + if not lines: + raise ValidationError( + self.env._( + "Please configure fields mapping for prices interface on your \ + EDI Exchange Type!" + ) + ) + + edi_exchange_type = exchange_record.type_id + + EDIConfig = self.env["edi.configuration"] + ProductSupplierInfo = self.env["product.supplierinfo"] + + prices = [] + product_codes = [] + today = datetime.date.today() + for line in lines: + if not line or not isinstance(line, str): + continue + line = line.lstrip() + # check if this line is already imported. + pos_from, pos_to = self._get_mapping_field_position( + edi_exchange_type, "supplier_code" + ) + product_code = line[pos_from:pos_to] + if product_code in product_codes: + continue + else: + product_codes.append(product_code) + key = ["supplier_id", "import_date"] + value = [edi_exchange_type.partner_ids.ids[0], today] + for mapping in edi_exchange_type.field_mapping_ids: + slice_from = mapping.sequence_start + slice_to = mapping.sequence_end + # construct dictionary + key.append(mapping.mapping_field_id.name) + data = line[slice_from:slice_to] + # Product test + if mapping.mapping_field_id.name == "supplier_code": + # appending supplier_code data + value.append(data) + # appending product_id data + supp_info = ProductSupplierInfo.search( + [("product_code", "=", data)], limit=1 + ) + product_id = supp_info.product_tmpl_id.id + key.append("product_tmpl_id") + value.append(product_id) + # Date test + elif mapping.is_date: + # slice dates + apply_date = EDIConfig.get_date_format_yyyymmdd(data) + value.append(apply_date) + # numeric test + elif mapping.is_numeric: + decimal_precision = mapping.decimal_precision + price = EDIConfig.insert_separator(data, -decimal_precision, ".") + value.append(float(price)) + elif mapping.mapping_field_id.name == "product_name": + value.append(data) + prices_dict = {k: v for k, v in zip(key, value, strict=False)} + prices.append(prices_dict) + return prices + + def create_supplier_price_list(self, exchange_record): + prices = self._prepare_supplier_price_list_values(exchange_record) + return self.env["supplier.price.list"].create(prices) diff --git a/edi_purchase_diapar_oca/demo/edi_backend_type_demo.xml b/edi_purchase_diapar_oca/demo/edi_backend_type_demo.xml new file mode 100644 index 000000000..dcecad37a --- /dev/null +++ b/edi_purchase_diapar_oca/demo/edi_backend_type_demo.xml @@ -0,0 +1,22 @@ + + + + DIAPAR + diapar + + + + Diapar + + + IN/PENDING + IN/DONE + IN/ERROR + OUT/PENDING + OUT/DONE + OUT/ERROR + + diff --git a/edi_purchase_diapar_oca/demo/edi_configuration_demo.xml b/edi_purchase_diapar_oca/demo/edi_configuration_demo.xml new file mode 100644 index 000000000..c5f076c71 --- /dev/null +++ b/edi_purchase_diapar_oca/demo/edi_configuration_demo.xml @@ -0,0 +1,23 @@ + + + + Diapar Purchase Order + + + + + +if record.state == 'purchase' and not record._has_exchange_record(conf.type_id): + record._edi_create_exchange_record(conf.type_id) + + + diff --git a/edi_purchase_diapar_oca/demo/edi_exchange_template_output_demo.xml b/edi_purchase_diapar_oca/demo/edi_exchange_template_output_demo.xml new file mode 100644 index 000000000..ebb3dc919 --- /dev/null +++ b/edi_purchase_diapar_oca/demo/edi_exchange_template_output_demo.xml @@ -0,0 +1,28 @@ + + + + Diapar Output Exchange Template + + + diapar.output.exchange.template + text + qweb + + 33513 + 03 + HDIAPAR + *DIAPAR*DIAPAR + + diff --git a/edi_purchase_diapar_oca/demo/edi_exchange_type_demo.xml b/edi_purchase_diapar_oca/demo/edi_exchange_type_demo.xml new file mode 100644 index 000000000..d9bef0bc6 --- /dev/null +++ b/edi_purchase_diapar_oca/demo/edi_exchange_type_demo.xml @@ -0,0 +1,82 @@ + + + + diapar_out_order + diapar_out_order + + + output + LD{dt}H{time} + C99 + iso-8859-1 + + + + filename_pattern: + date_pattern: "%Y%m%d" + time_pattern: "%H%M%S" + + + + diapar_in_despatch_advice + diapar_in_despatch_advice + + + input + BLE.* + txt + iso-8859-1 + 1 + 2 + + + + + + + + diapar_in_purchase_price + diapar_in_purchase_price + + + input + CH* + txt + iso-8859-1 + + + + + diff --git a/edi_purchase_diapar_oca/demo/edi_field_mapping_demo.xml b/edi_purchase_diapar_oca/demo/edi_field_mapping_demo.xml new file mode 100644 index 000000000..335ca3ee7 --- /dev/null +++ b/edi_purchase_diapar_oca/demo/edi_field_mapping_demo.xml @@ -0,0 +1,107 @@ + + + + + Date Planned + 1 + 24 + 32 + True + + + + + Product Code + 2 + 16 + 22 + + + + + Product Quantity + 3 + 52 + 57 + + + + + + Product Code + 1 + 2 + 8 + + + + + Product Name + 2 + 13 + 38 + + + + + Price HT + 3 + 38 + 47 + True + + + 4 + + + Apply Date + 4 + 96 + 102 + True + + + + diff --git a/edi_purchase_diapar_oca/demo/fs_storage_demo.xml b/edi_purchase_diapar_oca/demo/fs_storage_demo.xml new file mode 100644 index 000000000..e1397c28c --- /dev/null +++ b/edi_purchase_diapar_oca/demo/fs_storage_demo.xml @@ -0,0 +1,14 @@ + + + + Diapar FTP Storage + diapar_ftp + ftp + { + "host": "127.0.0.1", + "username": "admin", + "password": "admin", + "port": 5000 + } + + diff --git a/edi_purchase_diapar_oca/i18n/edi_purchase_diapar.pot b/edi_purchase_diapar_oca/i18n/edi_purchase_diapar.pot new file mode 100644 index 000000000..8c7aa2156 --- /dev/null +++ b/edi_purchase_diapar_oca/i18n/edi_purchase_diapar.pot @@ -0,0 +1,205 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * edi_purchase_diapar +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: edi_purchase_diapar +#: model_terms:ir.ui.view,arch_db:edi_purchase_diapar.view_invoice_supplier_price_update +msgid "Cancel" +msgstr "" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/models/purchase.py:33 +#, python-format +msgid "Check price for lines with product %s!" +msgstr "" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/models/purchase.py:28 +#, python-format +msgid "Check taxes for lines with product %s!" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__constant_file_end +msgid "Constant File End" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__constant_file_start +msgid "Constant File Start" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__create_uid +msgid "Created by" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__create_date +msgid "Created on" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__customer_code +msgid "Customer Code" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__display_name +msgid "Display Name" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model,name:edi_purchase_diapar.model_edi_config_system +msgid "EDI Config System" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__edi_line_ids +msgid "Edi Line" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__ftp_login +msgid "FTP Login" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__ftp_password +msgid "FTP Password" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__ftp_host +msgid "FTP Server Host" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__id +msgid "ID" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_supplier_info_update_line__inv_supplier_price_id +msgid "Inv Supplier Price" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model,name:edi_purchase_diapar.model_account_invoice +msgid "Invoice" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model,name:edi_purchase_diapar.model_invoice_supplier_price_update +msgid "Invoice Supplier Price Update" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update____last_update +msgid "Last Modified on" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__write_date +msgid "Last Updated on" +msgstr "" + +#. module: edi_purchase_diapar +#: model_terms:ir.ui.view,arch_db:edi_purchase_diapar.view_invoice_supplier_price_update +msgid "Lines" +msgstr "" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/models/purchase.py:22 +#, python-format +msgid "No lines in this order %s!" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__partner_id +msgid "Partner" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_account_invoice__partner_is_edi +msgid "Partner (Is Edit)" +msgstr "" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/models/product.py:21 +#, python-format +msgid "Product code must be 6 digits for %s!" +msgstr "" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/models/product.py:17 +#, python-format +msgid "Product code must be numeric for %s!" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model,name:edi_purchase_diapar.model_purchase_order +msgid "Purchase Order" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__po_text_file_pattern +msgid "Purchase order File pattern" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__csv_relative_in_path +msgid "Relative path for IN interfaces" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__csv_relative_out_path +msgid "Relative path for OUT interfaces" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_invoice_supplier_price_update__show_discount +msgid "Show Discount" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model,name:edi_purchase_diapar.model_supplier_info_update_line +msgid "Supplier Information Update Line" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.actions.act_window,name:edi_purchase_diapar.invoice_supplier_price_update_act +msgid "Update Prices From EDI" +msgstr "" + +#. module: edi_purchase_diapar +#: model_terms:ir.ui.view,arch_db:edi_purchase_diapar.invoice_supplier_form_inherit_diapar +msgid "Update Prices from EDI" +msgstr "" + +#. module: edi_purchase_diapar +#: model:ir.model.fields,field_description:edi_purchase_diapar.field_edi_config_system__vrp_code +msgid "VRP Code" +msgstr "" + +#. module: edi_purchase_diapar +#: model_terms:ir.ui.view,arch_db:edi_purchase_diapar.view_invoice_supplier_price_update +msgid "Validate" +msgstr "" + diff --git a/edi_purchase_diapar_oca/i18n/fr.po b/edi_purchase_diapar_oca/i18n/fr.po new file mode 100644 index 000000000..15fa78b85 --- /dev/null +++ b/edi_purchase_diapar_oca/i18n/fr.po @@ -0,0 +1,84 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * edi_purchase_diapar +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 9.0c\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-12-26 10:56+0000\n" +"PO-Revision-Date: 2020-07-01 10:27+0000\n" +"Last-Translator: Simon Mas \n" +"Language-Team: French \n" +"Language: fr\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" +"X-Generator: Weblate 3.8\n" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/models/purchase.py:31 +#, python-format +msgid "Check price for lines with product %s!" +msgstr "Check price for lines with product %s!" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/models/purchase.py:29 +#, python-format +msgid "Check taxes for lines with product %s!" +msgstr "Merci de vérifier la taxe pour les lignes au produit: %s!" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/wizard/supplier_info_update.py:23 +#, python-format +msgid "No Config FTP for this supplier %s!" +msgstr "Aucune configuration trouvée pour le fournisseur: %s!" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/models/purchase.py:24 +#, python-format +msgid "No lines in this order %s!" +msgstr "Aucune ligne pour cette commande: %s!" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/wizard/supplier_info_update.py:72 +#, python-format +msgid "" +"No supplier code given for product: %s for supplier: %s!, please give a " +"supplier code to continue prices operation update" +msgstr "" +"Aucun code fournisseur renseigné pour ce produit : %s avec le fournisseur: " +"%s!, merci de mettre à jour le codde fournisseur pour continuer la mise à " +"jour des prix." + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/models/product.py:20 +#, python-format +msgid "Product code must be 6 digits for %s!" +msgstr "Le code produit doit être de 6 numériques pour : %s!" + +#. module: edi_purchase_diapar +#: code:addons/edi_purchase_diapar/models/product.py:18 +#, python-format +msgid "Product code must be numeric for %s!" +msgstr "Le code produit doit être numérique pour le produit : %s!" + +#. module: edi_purchase_diapar +#: model:ir.model,name:edi_purchase_diapar.model_purchase_order +msgid "Purchase Order" +msgstr "Bon de commande" + +#. module: edi_purchase_diapar +#: model:ir.model,name:edi_purchase_diapar.model_edi_config_system +msgid "edi.config.system" +msgstr "edi.config.system" + +#. module: edi_purchase_diapar +#: model:ir.model,name:edi_purchase_diapar.model_supplier_info_update +msgid "supplier.info.update" +msgstr "" + +#~ msgid "Information about a product vendor" +#~ msgstr "Information sur le vendeur de l'article" diff --git a/edi_purchase_diapar_oca/models/__init__.py b/edi_purchase_diapar_oca/models/__init__.py new file mode 100644 index 000000000..9c3912f3b --- /dev/null +++ b/edi_purchase_diapar_oca/models/__init__.py @@ -0,0 +1,18 @@ +# Copyright (C) 2016-Today: Druidoo () +# @author: Druidoo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html + +from . import account_move +from . import res_partner +from . import edi_configuration +from . import edi_field_mapping +from . import edi_exchange_type +from . import edi_exchange_template_output +from . import picking_update +from . import purchase_order +from . import product_product +from . import product_supplierinfo +from . import supplier_price_list +from . import res_company +from . import res_config_settings +from . import stock_move diff --git a/edi_purchase_diapar_oca/models/account_move.py b/edi_purchase_diapar_oca/models/account_move.py new file mode 100644 index 000000000..23188f3d0 --- /dev/null +++ b/edi_purchase_diapar_oca/models/account_move.py @@ -0,0 +1,15 @@ +# Copyright (C) 2016-Today: Druidoo () +# @author: Druidoo +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html + +from odoo import fields, models + + +class AccountMove(models.Model): + _inherit = "account.move" + + partner_is_edi = fields.Boolean( + related="partner_id.is_edi", + string="Partner (Is Edi)", + store=True, + ) diff --git a/edi_purchase_diapar_oca/models/edi_configuration.py b/edi_purchase_diapar_oca/models/edi_configuration.py new file mode 100644 index 000000000..a7fe93981 --- /dev/null +++ b/edi_purchase_diapar_oca/models/edi_configuration.py @@ -0,0 +1,59 @@ +import datetime +import time + +from odoo import api, models + + +class EDIConfiguration(models.Model): + _inherit = "edi.configuration" + + @api.model + def get_datenow_format_for_file(self): + now = time.strftime("%Y-%m-%d %H:%M:%S") + date = now.split(" ")[0].replace("-", "") + hour = now.split(" ")[1].replace(":", "") + return date, hour + + @api.model + def get_datetime_format_ddmmyyyy(self, do_date): + if not isinstance(do_date, datetime.datetime): + do_date = datetime.datetime.strptime(do_date, "%Y-%m-%d %H:%M:%S") + return "%02d%02d%s" % ( + do_date.day, + do_date.month, + str(do_date.year)[2:], + ) + + @api.model + def get_date_format_yyyymmdd(self, date): + """ + Transform a string date to datetime and format it to standard odoo + date format + """ + return datetime.datetime.strptime(date, "%y%m%d").strftime("%Y-%m-%d") + + @api.model + def get_date_format_ble_yyyymmdd(self, date): + """ + Transform a string date (specific to delivery order interface format) + to datetime object and format it to standard odoo date format + """ + return datetime.datetime.strptime(date, "%Y%m%d").strftime("%Y-%m-%d") + + @api.model + def insert_separator(self, string, index, separator): + """ + This method is to insert a separator inside string on a certain + position + """ + return string[:index] + separator + string[index:] + + def _fix_lenght(self, value, lenght, mode="float", replace="", position="before"): + value = str(value) + if mode == "float": + value = value.split(".")[0] + if position == "before": + value = "".join([replace for i in range(lenght - len(value))]) + value + else: + value += "".join([replace for i in range(lenght - len(value))]) + return value[0:lenght] diff --git a/edi_purchase_diapar_oca/models/edi_exchange_template_output.py b/edi_purchase_diapar_oca/models/edi_exchange_template_output.py new file mode 100644 index 000000000..511a5b515 --- /dev/null +++ b/edi_purchase_diapar_oca/models/edi_exchange_template_output.py @@ -0,0 +1,10 @@ +from odoo import fields, models + + +class EDIExchangeTemplateOutput(models.Model): + _inherit = "edi.exchange.template.output" + + customer_code = fields.Char() + constant_file_start = fields.Char() + constant_file_end = fields.Char() + vrp_code = fields.Char() diff --git a/edi_purchase_diapar_oca/models/edi_exchange_type.py b/edi_purchase_diapar_oca/models/edi_exchange_type.py new file mode 100644 index 000000000..4bfad0ad2 --- /dev/null +++ b/edi_purchase_diapar_oca/models/edi_exchange_type.py @@ -0,0 +1,54 @@ +from datetime import datetime + +from pytz import timezone, utc + +from odoo import fields, models + +DEFAULT_DATE_FMT = "%Y%m%d" +DEFAULT_TIME_FMT = "%H%M%S" + + +class EDIExchangeType(models.Model): + _inherit = "edi.exchange.type" + + field_mapping_ids = fields.One2many( + comodel_name="edi.field.mapping", + inverse_name="exchange_type_id", + string="Field Mappings", + ) + header_code = fields.Char() + lines_code = fields.Char() + delivery_sign = fields.Char() + + def _make_exchange_filename_time(self): + self.ensure_one() + pattern_settings = self.advanced_settings.get("filename_pattern", {}) + force_tz = pattern_settings.get("force_tz", self.env.user.tz) + time_pattern = pattern_settings.get("time_pattern", DEFAULT_TIME_FMT) + tz = timezone(force_tz) if force_tz else None + now = datetime.now(utc).astimezone(tz) + return self.env["ir.http"]._slugify(now.strftime(time_pattern)) + + def _make_exchange_filename(self, exchange_record): + pattern = self.exchange_filename_pattern + if "{time}" in pattern: + ext = self.exchange_file_ext + if ext: + pattern += ".{ext}" + dt = self._make_exchange_filename_datetime() + seq = self._make_exchange_filename_sequence() + record_name = self._get_record_name(exchange_record) + record = exchange_record + if exchange_record.model and exchange_record.res_id: + record = exchange_record.record + return pattern.format( + exchange_record=exchange_record, + record=record, + record_name=record_name, + type=self, + dt=dt, + seq=seq, + ext=ext, + time=self._make_exchange_filename_time(), + ) + return super()._make_exchange_filename(exchange_record) diff --git a/edi_purchase_diapar_oca/models/edi_field_mapping.py b/edi_purchase_diapar_oca/models/edi_field_mapping.py new file mode 100644 index 000000000..0ddd2ee06 --- /dev/null +++ b/edi_purchase_diapar_oca/models/edi_field_mapping.py @@ -0,0 +1,25 @@ +from odoo import fields, models + + +class EDIPriceMapping(models.Model): + _name = "edi.field.mapping" + _order = "position" + _description = "EDI Price Mapping" + + sequence = fields.Integer(default=1) + position = fields.Integer(required=True) + exchange_type_id = fields.Many2one( + comodel_name="edi.exchange.type", + string="Exchange type", + required=True, + ) + mapping_field_id = fields.Many2one( + comodel_name="ir.model.fields", + string="Prices mapping field", + ) + name = fields.Char(string="Zone description") + sequence_start = fields.Integer() + sequence_end = fields.Integer() + is_numeric = fields.Boolean(string="Is numeric ?") + is_date = fields.Boolean(string="Is a date?") + decimal_precision = fields.Integer() diff --git a/edi_purchase_diapar_oca/models/picking_update.py b/edi_purchase_diapar_oca/models/picking_update.py new file mode 100644 index 000000000..6f725c127 --- /dev/null +++ b/edi_purchase_diapar_oca/models/picking_update.py @@ -0,0 +1,51 @@ +# Copyright (C) 2016-Today: Druidoo () +# @author: Druidoo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html +from odoo import Command, fields, models + + +class PickingEdi(models.Model): + _name = "picking.edi" + _description = "Picking EDI" + + product_id = fields.Many2one("product.product") + ordered_quantity = fields.Float() + product_qty = fields.Float(string="EDI Quantity") + package_qty = fields.Float(string="Product package") + line_to_update_id = fields.Many2one("stock.move") + picking_update_id = fields.Many2one("picking.update") + + +class PickingUpdate(models.Model): + _name = "picking.update" + _inherit = ["mail.thread", "mail.activity.mixin"] + _description = "Picking Update" + + done = fields.Boolean(readonly=True) + name = fields.Many2one("stock.picking", string="Order picking", readonly=True) + values_proposed_ids = fields.One2many( + "picking.edi", + inverse_name="picking_update_id", + string="Quantities to update", + readonly=True, + ) + + def _prepare_values_proposed(self, proposition): + self.ensure_one() + return { + "quantity": proposition.product_qty, + } + + def button_update_picking_order(self): + self.ensure_one() + commands = [] + for proposition in self.values_proposed_ids: + commands += [ + Command.update( + proposition.line_to_update_id.id, + self._prepare_values_proposed(proposition), + ) + ] + self.done = True + self.name.write({"move_ids": commands}) + return True diff --git a/edi_purchase_diapar_oca/models/product_product.py b/edi_purchase_diapar_oca/models/product_product.py new file mode 100644 index 000000000..c7e393ade --- /dev/null +++ b/edi_purchase_diapar_oca/models/product_product.py @@ -0,0 +1,22 @@ +from odoo import models +from odoo.exceptions import ValidationError + + +class ProductProduct(models.Model): + _inherit = "product.product" + + def _get_supplier_code_or_ean(self, seller_id): + self.ensure_one() + code, origin_code = "", "" + seller_line = self.seller_ids.filtered( + lambda seller: seller.partner_id.id == seller_id and seller.product_code + ) + if seller_line and seller_line[0].product_code: + code = seller_line[0].product_code + origin_code = "supplier" + elif self.barcode: + code = self.barcode + origin_code = "barcode" + if not code: + raise ValidationError(self.env._("No code for this product %s!", self.name)) + return code, origin_code diff --git a/edi_purchase_diapar_oca/models/product_supplierinfo.py b/edi_purchase_diapar_oca/models/product_supplierinfo.py new file mode 100644 index 000000000..52ca22637 --- /dev/null +++ b/edi_purchase_diapar_oca/models/product_supplierinfo.py @@ -0,0 +1,80 @@ +from odoo import api, models +from odoo.exceptions import ValidationError + + +class SupplierInfo(models.Model): + _inherit = "product.supplierinfo" + + @api.model + def compute_edi_partner(self, partner): + """ + :param partner: purchase order/invoice supplier + :return: EDI supplier used + """ + edi_exchange_type_obj = self.env["edi.exchange.type"] + exchange_type = edi_exchange_type_obj.search( + [("partner_ids", "=", partner.id)], limit=1 + ) + if not exchange_type: + raise ValidationError( + self.env._("No EDI Exchange Type for this supplier %s!") % partner.name + ) + if partner.edi_purchase_supplier_id: + return partner.edi_purchase_supplier_id + else: + return partner + + def _get_price_field(self): + return "price" + + @api.model + def update_purchase_price(self, vals): + """ + Looks for most recent price on purchase table of prices, only for + EDI suppliers + :param vals: + :return: updated values with product price + """ + supplier_id = vals.get("partner_id", False) + supplier = self.env["res.partner"].browse(supplier_id) + if supplier.is_edi: + edi_supplier = self.compute_edi_partner(supplier) + supplier_code = vals.get("product_code", False) + if not supplier_code: + raise ValidationError( + self.env._("Please give a supplier code to create the product!") + ) + price = self.env["supplier.price.list"].search( + [ + ("supplier_id", "=", edi_supplier.id), + ("supplier_code", "=", supplier_code), + ], + order="apply_date DESC", + ) + if price: + price_field = self._get_price_field() + vals.update({price_field: price[0].price}) + price.sudo().write({"price_updated": True}) + return vals + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + vals = self.update_purchase_price(vals) + return super().create(vals_list) + + @api.constrains("product_code", "partner_id") + def _check_product_code(self): + if self.product_code and self.partner_id.is_edi: + if not self.product_code.isdigit(): + raise ValidationError( + self.env._( + "Product code must be numeric for %s!", self.partner_id.name + ) + ) from None + if len(self.product_code) != 6: + raise ValidationError( + self.env._( + "Product code must be 6 digits for %s!", self.partner_id.name + ) + ) from None diff --git a/edi_purchase_diapar_oca/models/purchase_order.py b/edi_purchase_diapar_oca/models/purchase_order.py new file mode 100644 index 000000000..076600b34 --- /dev/null +++ b/edi_purchase_diapar_oca/models/purchase_order.py @@ -0,0 +1,86 @@ +from odoo import models +from odoo.exceptions import ValidationError + + +class PurchaseOrder(models.Model): + _inherit = "purchase.order" + + def _edi_config_field_relation(self): + return self.partner_id.edi_purchase_conf_ids + + def _should_apply_receipt_qty_policy(self): + """Condition to apply the policy of using the quantity from + the EDI file instead of the one in the order line.""" + self.ensure_one() + exchange_types = self.env["edi.exchange.type"].search( + [ + ("partner_ids", "in", self.partner_id.ids), + ("direction", "=", "input"), + ] + ) + return ( + not self.edi_disable_auto + and self.partner_id.is_edi + and self.partner_id.edi_purchase_conf_ids + and exchange_types + ) + + def _consolidate_product_qty(self, order_line): + return order_line.product_qty + + def _consolidate_products(self): + self.ensure_one() + if not self.order_line: + raise ValidationError( + self.env._("No lines in this order %s!", self.name) + ) from None + lines = {} + for line in self.order_line: + quantity = self._consolidate_product_qty(line) + if line.product_id in lines: + if line.taxes_id != lines[line.product_id]["taxes_id"]: + raise ValidationError( + self.env._( + "Check taxes for lines with product %s!", + line.product_id.name, + ) + ) + if line.price_unit != lines[line.product_id]["price_unit"]: + raise ValidationError( + self.env._( + "Check price for lines with product %s!", + line.product_id.name, + ) + ) + lines[line.product_id]["quantity"] += quantity + else: + code, origin_code = line.product_id._get_supplier_code_or_ean( + line.partner_id.id + ) + values = { + "code": code, + "origin_code": origin_code, + "quantity": quantity, + "price_unit": line.price_unit, + "taxes_id": line.taxes_id, + } + lines.update({line.product_id: values}) + return lines + + def generate_template_output_diapar(self, output_template, exchange_record): + self.ensure_one() + return output_template.exchange_generate( + exchange_record, + order_lines=self._consolidate_products(), + env=self.env, + ) + + +class PurchaseOrderLine(models.Model): + _inherit = "purchase.order.line" + + def _create_stock_moves(self, picking): + if self.order_id._should_apply_receipt_qty_policy(): + if self.company_id.edi_receipt_qty_policy == "received_file": + self = self.with_context(qty_from_file_policy=True) + return super()._create_stock_moves(picking) diff --git a/edi_purchase_diapar_oca/models/res_company.py b/edi_purchase_diapar_oca/models/res_company.py new file mode 100644 index 000000000..6cc529364 --- /dev/null +++ b/edi_purchase_diapar_oca/models/res_company.py @@ -0,0 +1,16 @@ +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + edi_receipt_qty_policy = fields.Selection( + selection=[ + ("ordered", "From PO"), + ("received_file", "From EDI Receipt File"), + ], + string="Receipt Quantity Policy", + default="ordered", + help="Policy to determine the quantity to be used in " + "receipt when processing EDI file for purchase order.", + ) diff --git a/edi_purchase_diapar_oca/models/res_config_settings.py b/edi_purchase_diapar_oca/models/res_config_settings.py new file mode 100644 index 000000000..cd67ab420 --- /dev/null +++ b/edi_purchase_diapar_oca/models/res_config_settings.py @@ -0,0 +1,9 @@ +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + edi_receipt_qty_policy = fields.Selection( + related="company_id.edi_receipt_qty_policy", readonly=False + ) diff --git a/edi_purchase_diapar_oca/models/res_partner.py b/edi_purchase_diapar_oca/models/res_partner.py new file mode 100644 index 000000000..baea45a0a --- /dev/null +++ b/edi_purchase_diapar_oca/models/res_partner.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + edi_purchase_supplier_id = fields.Many2one( + "res.partner", + domain=[("supplier_rank", ">", 0), ("is_edi", "=", True)], + ) + is_edi = fields.Boolean(string="Is an EDI supplier") + show_discount = fields.Boolean() diff --git a/edi_purchase_diapar_oca/models/stock_move.py b/edi_purchase_diapar_oca/models/stock_move.py new file mode 100644 index 000000000..8ab945ae3 --- /dev/null +++ b/edi_purchase_diapar_oca/models/stock_move.py @@ -0,0 +1,12 @@ +from odoo import models + + +class StockMove(models.Model): + _inherit = "stock.move" + + def _action_assign(self, force_qty=False): + res = super()._action_assign(force_qty=force_qty) + if self.env.context.get("qty_from_file_policy"): + # Set the quantity to 0 and it will be updated later in the process. + self.quantity = 0 + return res diff --git a/edi_purchase_diapar_oca/models/supplier_price_list.py b/edi_purchase_diapar_oca/models/supplier_price_list.py new file mode 100644 index 000000000..a6b60b1fa --- /dev/null +++ b/edi_purchase_diapar_oca/models/supplier_price_list.py @@ -0,0 +1,114 @@ +# Copyright (C) 2016-Today: Druidoo () +# @author: Druidoo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html + +from odoo import api, fields, models + + +class SupplierPriceList(models.Model): + _name = "supplier.price.list" + _description = "Supplier Price List" + + import_date = fields.Date(readonly=True) + supplier_id = fields.Many2one( + comodel_name="res.partner", + string="EDI Supplier", + domain=[("is_edi", "=", True), ("supplier_rank", ">", 0)], + readonly=True, + required=True, + ) + product_tmpl_id = fields.Many2one( + comodel_name="product.template", string="Product", ondelete="set null" + ) + product_name = fields.Char(readonly=True, required=True) + supplier_code = fields.Char(readonly=True, required=True) + price = fields.Float( + digits="Product Price", + readonly=True, + required=True, + help="The price HT to purchase a product", + ) + apply_date = fields.Date(readonly=True, required=True) + barcode = fields.Char(string="Ean") + price_updated = fields.Boolean() + + def _get_price_field(self): + return "price" + + def _create_supplierinfo_with_price(self, vals): + price_field = self._get_price_field() + vals.update({price_field: self.price}) + return self.env["product.supplierinfo"].create(vals) + + def button_create_product(self): + self.ensure_one() + # create new product + product_tmpl_id = self.env["product.template"].create( + { + "name": self.product_name, + "sale_ok": True, + "purchase_ok": True, + "type": "consu", + "default_code": self.supplier_code, + "barcode": self.barcode, + } + ) + # link product with current supplier price list + self.sudo().product_tmpl_id = product_tmpl_id.id + # create product supplier info + self._create_supplierinfo_with_price( + { + "partner_id": self.supplier_id.id, + "product_code": self.supplier_code, + "product_tmpl_id": product_tmpl_id.id, + } + ) + self.sudo().price_updated = True + # find similar supplier price list + supplier_price_list_ids = self.search( + [ + ("supplier_id", "=", self.supplier_id.id), + ("product_name", "=", self.product_name), + ("supplier_code", "=", self.supplier_code), + ("product_tmpl_id", "=", False), + ] + ) + # link product to similar supplier price list + supplier_price_list_ids.sudo().write({"product_tmpl_id": product_tmpl_id.id}) + # create action to open newly created product form view + action = { + "name": self.env._("Product Form"), + "res_model": "product.template", + "type": "ir.actions.act_window", + "target": "current", + "res_id": product_tmpl_id.id, + } + return action + + @api.model + def update_product_price_list(self, product, splists): + splists = splists.sorted("apply_date") + splist = splists[-1] + price = splist.price + args = [ + ("product_code", "=", splist.supplier_code), + ("product_tmpl_id", "=", product.id), + ] + supplierinfos = self.env["product.supplierinfo"].search(args, limit=1) + if supplierinfos: + price_field = self._get_price_field() + if getattr(supplierinfos, price_field) != price: + supplierinfos.write( + { + price_field: price, + } + ) + else: + self._create_supplierinfo_with_price( + { + "partner_id": splist.supplier_id.id, + "product_code": splist.supplier_code, + "product_tmpl_id": product.id, + } + ) + splists.sudo().write({"price_updated": True}) diff --git a/edi_purchase_diapar_oca/pyproject.toml b/edi_purchase_diapar_oca/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/edi_purchase_diapar_oca/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/edi_purchase_diapar_oca/readme/CONTRIBUTORS.md b/edi_purchase_diapar_oca/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..68accf590 --- /dev/null +++ b/edi_purchase_diapar_oca/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- Druidoo +- [Trobz](https://www.trobz.com) + - Phan Hong Phuc \<\> diff --git a/edi_purchase_diapar_oca/security/ir.model.access.csv b/edi_purchase_diapar_oca/security/ir.model.access.csv new file mode 100644 index 000000000..9138190da --- /dev/null +++ b/edi_purchase_diapar_oca/security/ir.model.access.csv @@ -0,0 +1,8 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_model_edi_field_mapping_all,access_model_edi_field_mapping_all,model_edi_field_mapping,base.group_user,1,0,0,0 +access_model_edi_field_mapping_manager,access_model_edi_field_mapping_manager,model_edi_field_mapping,base_edi.group_edi_manager,1,1,1,1 +access_model_picking_edi_all,access_model_picking_edi_all,model_picking_edi,base.group_user,1,0,0,0 +access_model_picking_edi_manager,access_model_picking_edi_manager,model_picking_edi,base_edi.group_edi_manager,1,1,0,0 +access_model_picking_update_all,access_model_picking_update_all,model_picking_update,base.group_user,1,0,0,0 +access_model_picking_update_manager,access_model_picking_update_manager,model_picking_update,base_edi.group_edi_manager,1,1,0,0 +access_model_supplier_price_list_all,access_model_supplier_price_list_all,model_supplier_price_list,base.group_user,1,0,0,0 diff --git a/edi_purchase_diapar_oca/static/description/icon.png b/edi_purchase_diapar_oca/static/description/icon.png new file mode 100644 index 000000000..fd60220fb Binary files /dev/null and b/edi_purchase_diapar_oca/static/description/icon.png differ diff --git a/edi_purchase_diapar_oca/templates/exchange_template_output_diapar.xml b/edi_purchase_diapar_oca/templates/exchange_template_output_diapar.xml new file mode 100644 index 000000000..b2a5b5827 --- /dev/null +++ b/edi_purchase_diapar_oca/templates/exchange_template_output_diapar.xml @@ -0,0 +1,10 @@ + + + + diff --git a/edi_purchase_diapar_oca/tests/__init__.py b/edi_purchase_diapar_oca/tests/__init__.py new file mode 100644 index 000000000..bd5bea96f --- /dev/null +++ b/edi_purchase_diapar_oca/tests/__init__.py @@ -0,0 +1 @@ +from . import test_edi_purchase_diapar_oca diff --git a/edi_purchase_diapar_oca/tests/test_edi_purchase_diapar_oca.py b/edi_purchase_diapar_oca/tests/test_edi_purchase_diapar_oca.py new file mode 100644 index 000000000..ad0366d01 --- /dev/null +++ b/edi_purchase_diapar_oca/tests/test_edi_purchase_diapar_oca.py @@ -0,0 +1,238 @@ +from odoo import Command +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestEdiPurchaseDiaparOCA(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.edi_config_diapar = cls.env.ref( + "edi_purchase_diapar_oca.demo_edi_config_diapar" + ) + cls.backend = cls.env.ref("edi_purchase_diapar_oca.demo_edi_backend_diapar") + cls.edi_template_output_diapar = cls.env.ref( + "edi_purchase_diapar_oca.demo_edi_exchange_template_output_diapar" + ) + cls.exchange_type_ble = cls.env.ref( + "edi_purchase_diapar_oca.demo_exchange_diapar_in_despatch_advice" + ) + cls.exchange_type_ch = cls.env.ref( + "edi_purchase_diapar_oca.demo_exchange_diapar_in_purchase_price" + ) + cls.edi_supplier = cls.env.ref("base.res_partner_12") + cls.edi_supplier.write( + { + "is_edi": True, + "edi_purchase_conf_ids": [Command.set(cls.edi_config_diapar.ids)], + } + ) + cls.exchange_type_ble.partner_ids = [Command.set(cls.edi_supplier.ids)] + cls.exchange_type_ch.partner_ids = [Command.set(cls.edi_supplier.ids)] + cls.normal_supplier = cls.env.ref("base.res_partner_4") + cls.product_4 = cls.env.ref("product.product_product_4") + cls.product_5 = cls.env.ref("product.product_product_5") + cls.supplier_info_5 = cls.env["product.supplierinfo"].create( + { + "product_tmpl_id": cls.product_5.product_tmpl_id.id, + "partner_id": cls.edi_supplier.id, + "product_code": "987654", + "min_qty": 1, + "price": 10, + } + ) + + def _create_purchase_order(self, partner, product): + return self.env["purchase.order"].create( + { + "partner_id": partner.id, + "order_line": [ + Command.create( + { + "product_id": product.id, + "product_qty": 10, + "price_unit": 10, + } + ) + ], + } + ) + + def test_purchase_order_confirmation_without_product_code(self): + order = self._create_purchase_order(self.edi_supplier, self.product_4) + with self.assertRaises(ValidationError, msg="Please give a supplier code to"): + order.button_confirm() + + def test_purchase_order_confirmation_with_edi_supplier(self): + order = self._create_purchase_order(self.edi_supplier, self.product_5) + order.button_confirm() + self.assertEqual(order.state, "purchase") + self.assertEqual(order.exchange_record_count, 1) + + def test_purchase_order_confirmation_with_normal_supplier(self): + order = self._create_purchase_order(self.normal_supplier, self.product_5) + order.button_confirm() + self.assertEqual(order.state, "purchase") + self.assertEqual(order.exchange_record_count, 0) + + def test_edi_ouput_process_diapar_purchase_order(self): + order = self._create_purchase_order(self.edi_supplier, self.product_5) + order.button_confirm() + exchange_record = order.exchange_record_ids[0] + exchange_record.backend_id.exchange_generate(exchange_record) + file_content = exchange_record._get_file_content() + self.assertTrue(file_content, "The generated file content should not be empty.") + # Test template output diapar, this shoule be removed? + constant_file_start = self.edi_template_output_diapar.constant_file_start + constant_file_end = self.edi_template_output_diapar.constant_file_end + customer_code = self.edi_template_output_diapar.customer_code + vrp_code = self.edi_template_output_diapar.vrp_code + mapping_line_1 = "C" + self.edi_config_diapar.get_datetime_format_ddmmyyyy( + order.date_planned + ) + mapping_line_2 = "".join( + [ + ("D" if vals["origin_code"] == "supplier" else "E") + + self.edi_config_diapar._fix_lenght( + vals["code"], 6, mode="string", replace="0", position="after" + ) + + self.edi_config_diapar._fix_lenght( + vals["quantity"], 3, mode="float", replace="0", position="before" + ) + for _product, vals in order._consolidate_products().items() + ] + ) + self.assertEqual( + str(file_content).strip(), + f"{constant_file_start} A{vrp_code}B{customer_code}" + f"{mapping_line_1}{mapping_line_2}{constant_file_end}", + ) + + def _generate_fake_input_ble_content(self, order, new_quantity): + """ + Example: + 1AAAAABBBBBCCDDD2026030420260305EEEE + 2AAAAABBBBBCCDDD987654UUUUUUUUUUUUUUUUUUUUUUUUUVVVVV00007-GG + (where: + - 1 is the header code, 2 is the lines code + - 20260305 is the planned date + - 987654 is the product code + - 000007 is the new quantity) + """ + partner_id = order.partner_id.id + date_planned = order.date_planned.strftime("%Y%m%d") + header = f"1AAAAABBBBBCCDDD20260304{date_planned}EEEEEE" + lines = [] + for order_line in order.order_line: + product = order_line.product_id + code, _origin_code = product._get_supplier_code_or_ean(partner_id) + line = ( + f"2AAAAABBBBBCCDDD{code}UUUUUUUUUUUUUUUUUUUUUUUUU" + + f"VVVVV{str(new_quantity).zfill(5)}-GG" + ) + lines.append(line) + return "\n".join([header] + lines) + + def test_edi_input_process_ble(self): + new_quantity = 7 + order = self._create_purchase_order(self.edi_supplier, self.product_5) + order.write({"date_planned": "2026-03-05"}) + order.button_confirm() + exchange_record_out = order.exchange_record_ids[0] + exchange_record_out.backend_id.exchange_generate(exchange_record_out) + + exchange_record_ble = self.env["edi.exchange.record"].create( + { + "backend_id": self.backend.id, + "type_id": self.exchange_type_ble.id, + "model": "purchase.order", + "res_id": order.id, + } + ) + self.assertEqual(order.exchange_record_count, 2) + + fake_content = self._generate_fake_input_ble_content(order, new_quantity) + exchange_record_ble._set_file_content(fake_content) + exchange_record_ble.write({"edi_exchange_state": "input_received"}) + exchange_record_ble.backend_id.exchange_process(exchange_record_ble) + self.assertEqual( + exchange_record_ble.edi_exchange_state, + "input_processed", + "The EDI exchange record should be processed successfully.", + ) + order_picking = order.picking_ids[0] + picking_update = self.env["picking.update"].search( + [("name", "=", order_picking.id)] + ) + self.assertTrue(picking_update, "A picking update record should be created.") + self.assertTrue( + picking_update.done, "The picking update should be marked as done." + ) + moves = order_picking.move_ids + self.assertEqual( + len(moves), 1, "There should be one stock move in the picking." + ) + self.assertEqual( + moves[0].quantity, + new_quantity, + "The stock move quantity should be updated to the new quantity.", + ) + + def _generate_fake_input_ch_content(self): + product_code = self.supplier_info_5.product_code + product_name = "BISCOTTE 6CEREALE.300HEUD" + price_ht = "000015500" + apply_date = "20060305" + return ( + f"07{product_code}00015{product_name}{price_ht}" + + f"0000214100024000033924604808270011013700000030000{apply_date}0001403" + ) + + def test_edi_input_process_ch(self): + order = self._create_purchase_order(self.edi_supplier, self.product_5) + order.button_confirm() + exchange_record_ch = self.env["edi.exchange.record"].create( + { + "backend_id": self.backend.id, + "type_id": self.exchange_type_ch.id, + "model": "purchase.order", + "res_id": order.id, + } + ) + fake_content = self._generate_fake_input_ch_content().strip() + exchange_record_ch._set_file_content(fake_content) + exchange_record_ch.write({"edi_exchange_state": "input_received"}) + exchange_record_ch.backend_id.exchange_process(exchange_record_ch) + self.assertEqual( + exchange_record_ch.edi_exchange_state, + "input_processed", + "The EDI exchange record should be processed successfully.", + ) + + supplier_price_list = self.env["supplier.price.list"].search( + [ + ("supplier_id", "=", self.edi_supplier.id), + ("product_tmpl_id", "=", self.product_5.product_tmpl_id.id), + ] + ) + self.assertTrue( + supplier_price_list, "A supplier price list record should be created." + ) + self.assertEqual( + supplier_price_list.price, + 1.55, + "The price in the supplier price list should be updated to the new price.", + ) + supllier_info = self.env["product.supplierinfo"].search( + [ + ("partner_id", "=", self.edi_supplier.id), + ("product_code", "=", self.supplier_info_5.product_code), + ] + ) + self.assertTrue(supllier_info, "The supplier info record should exist.") + self.assertEqual( + supllier_info.price, + 1.55, + "The base price in the supplier info should be updated to the new price.", + ) diff --git a/edi_purchase_diapar_oca/views/edi_exchange_template_output.xml b/edi_purchase_diapar_oca/views/edi_exchange_template_output.xml new file mode 100644 index 000000000..2dd1b03ae --- /dev/null +++ b/edi_purchase_diapar_oca/views/edi_exchange_template_output.xml @@ -0,0 +1,18 @@ + + + + edi.exchange.template.output + + + + + + + + + + + diff --git a/edi_purchase_diapar_oca/views/exchange_type_views.xml b/edi_purchase_diapar_oca/views/exchange_type_views.xml new file mode 100644 index 000000000..280da8e30 --- /dev/null +++ b/edi_purchase_diapar_oca/views/exchange_type_views.xml @@ -0,0 +1,35 @@ + + + + edi.exchange.type.form.inherit + edi.exchange.type + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/edi_purchase_diapar_oca/views/menus.xml b/edi_purchase_diapar_oca/views/menus.xml new file mode 100644 index 000000000..ca0a422a9 --- /dev/null +++ b/edi_purchase_diapar_oca/views/menus.xml @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/edi_purchase_diapar_oca/views/picking_update_views.xml b/edi_purchase_diapar_oca/views/picking_update_views.xml new file mode 100644 index 000000000..43837f8cc --- /dev/null +++ b/edi_purchase_diapar_oca/views/picking_update_views.xml @@ -0,0 +1,57 @@ + + + + picking.update.tree + picking.update + + + + + + + + + + + picking.update.form + picking.update + +
+
+
+ + + + + + + + + + + + + + + + +
+
+
+ + + Delivery Order Update + ir.actions.act_window + picking.update + list,form + +
diff --git a/edi_purchase_diapar_oca/views/product_template_views.xml b/edi_purchase_diapar_oca/views/product_template_views.xml new file mode 100644 index 000000000..f38dd4289 --- /dev/null +++ b/edi_purchase_diapar_oca/views/product_template_views.xml @@ -0,0 +1,19 @@ + + + + product.template + + 20 + +
+
+
+
+
diff --git a/edi_purchase_diapar_oca/views/res_config_settings_views.xml b/edi_purchase_diapar_oca/views/res_config_settings_views.xml new file mode 100644 index 000000000..c1e4399a1 --- /dev/null +++ b/edi_purchase_diapar_oca/views/res_config_settings_views.xml @@ -0,0 +1,23 @@ + + + + res.config.settings.view.form.inherit.purchase + res.config.settings + + + + + + + + + + + + diff --git a/edi_purchase_diapar_oca/views/res_partner_views.xml b/edi_purchase_diapar_oca/views/res_partner_views.xml new file mode 100644 index 000000000..01df2b7fd --- /dev/null +++ b/edi_purchase_diapar_oca/views/res_partner_views.xml @@ -0,0 +1,17 @@ + + + + view.partner.form.inherit + res.partner + + + + + + + + + diff --git a/edi_purchase_diapar_oca/views/supplier_price_list_views.xml b/edi_purchase_diapar_oca/views/supplier_price_list_views.xml new file mode 100644 index 000000000..3feb0856b --- /dev/null +++ b/edi_purchase_diapar_oca/views/supplier_price_list_views.xml @@ -0,0 +1,81 @@ + + + + product.price.history.search + supplier.price.list + + + + + + + + + + + product.price.history.tree + supplier.price.list + + + + + + + + + + + + + + product.price.history.form + supplier.price.list + +
+
+
+ + + + + + + + + + + + + + + + +
+
+
+ + + Price History + ir.actions.act_window + supplier.price.list + list,form + + + + Product Price History + ir.actions.act_window + supplier.price.list + { + 'search_default_product_tmpl_id': [active_id], + 'default_product_tmpl_id': active_id + } + + +
diff --git a/test-requirements.txt b/test-requirements.txt index a90c99ba3..3d68b5b20 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,4 @@ odoo-test-helper xmlunittest responses +odoo-addon-edi_purchase_oca @ git+https://github.com/OCA/edi-framework.git@refs/pull/180/head#subdirectory=edi_purchase_oca