From cc8782c3cdb64cc504584e587f829fdc9188097c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ram=C3=B3n=20V=C3=A1squez?= <ramon.vasquez@eynes.com.ar>
Date: Thu, 16 May 2024 17:44:25 -0300
Subject: [PATCH] [FIX][T4221] Purchase order manual importer; rejected files

---
 models/customer_purchase_order_importer.py    | 211 +++++-------------
 ...customer_purchase_order_importer_wizard.py |  46 ++--
 wizard/rejected_import_wizard.py              |   3 +-
 3 files changed, 84 insertions(+), 176 deletions(-)

diff --git a/models/customer_purchase_order_importer.py b/models/customer_purchase_order_importer.py
index efad48c..f187cde 100644
--- a/models/customer_purchase_order_importer.py
+++ b/models/customer_purchase_order_importer.py
@@ -6,10 +6,8 @@
 #
 ##############################################################################
 
-import base64
-import glob
+from base64 import b64decode, b64encode
 import logging
-import os
 import pytz
 from datetime import datetime, timedelta
 from ftplib import FTP
@@ -31,20 +29,6 @@ class CustomerPurchaseOrderImporter(models.Model):
     _name = 'customer.purchase.order.importer'
     _description = 'Customer Purchase Order Importer'
 
-    """The command to mount the remote folder where the import files are stored is:
-        sudo mount -t cifs //10.10.15.134/KrikosDMS/TEST_ORDERS/Documentos/Download/ORDERS \
-            /home/Downloads -o vers=3.0,uid=1000,gid=1000,forceuid,forcegid,username=odoo,domain=lario.net,password=Odoo123!
-
-        To get the filestore volume name, run:
-            docker volume ls
-
-        Within the folder <db_name>, create a folder named 'import' and within it, another one named
-        'purchase_orders'. This folder is the mount point.
-
-        IMPORTANT: The folder TEST_ORDERS is a copy of the original folder ORDERS. This folder is
-        used for testing purposes only. The original folder will be used in production.
-    """
-
     def get_lines(self, file_txt):
         lines = file_txt.split('\r\n')
 
@@ -421,14 +405,8 @@ class CustomerPurchaseOrderImporter(models.Model):
                 'modified': additional_line_expense,
             },
             'box_qty': {'original': box_qty, 'modified': box_qty},
-            'box_qty_ean_set': {
-                'original': box_qty_ean_set,
-                'modified': box_qty_ean_set,
-            },
-            'box_qty_pallets': {
-                'original': box_qty_pallets,
-                'modified': box_qty_pallets,
-            },
+            'box_qty_ean_set': {'original': box_qty_ean_set, 'modified': box_qty_ean_set},
+            'box_qty_pallets': {'original': box_qty_pallets, 'modified': box_qty_pallets},
             'comeback': {'original': comeback, 'modified': comeback},
             'discount1': {'original': discount1, 'modified': discount1},
             'discount2': {'original': discount2, 'modified': discount2},
@@ -438,63 +416,30 @@ class CustomerPurchaseOrderImporter(models.Model):
             'discount6': {'original': discount6, 'modified': discount6},
             'ean13_code': {'original': ean13_code, 'modified': ean13_code},
             'errors': [],
-            'gross_price_unit': {
-                'original': gross_price_unit,
-                'modified': gross_price_unit,
-            },
-            'internal_tax': {
-                'original': internal_tax,
-                'modified': internal_tax,
-            },
+            'gross_price_unit': {'original': gross_price_unit, 'modified': gross_price_unit},
+            'internal_tax': {'original': internal_tax, 'modified': internal_tax},
             'item_colour_description': {
                 'original': item_colour_description,
                 'modified': item_colour_description,
             },
-            'item_description1': {
-                'original': item_description1,
-                'modified': item_description1,
-            },
-            'item_description2': {
-                'original': item_description2,
-                'modified': item_description2,
-            },
-            'item_description3': {
-                'original': item_description3,
-                'modified': item_description3,
-            },
-            'item_description4': {
-                'original': item_description4,
-                'modified': item_description4,
-            },
+            'item_description1': {'original': item_description1, 'modified': item_description1},
+            'item_description2': {'original': item_description2, 'modified': item_description2},
+            'item_description3': {'original': item_description3, 'modified': item_description3},
+            'item_description4': {'original': item_description4, 'modified': item_description4},
             'item_short_description': {
                 'original': item_short_description,
                 'modified': item_short_description,
             },
-            'item_size_description': {
-                'original': item_size_description,
-                'modified': item_size_description,
-            },
-            'line_number': {
-                'original': line_number,
-                'modified': line_number,
-            },
-            'line_total_amount': {
-                'original': line_total_amount,
-                'modified': line_total_amount,
-            },
-            'net_price_unit': {
-                'original': net_price_unit,
-                'modified': net_price_unit,
-            },
+            'item_size_description': {'original': item_size_description, 'modified': item_size_description},
+            'line_number': {'original': line_number, 'modified': line_number},
+            'line_total_amount': {'original': line_total_amount, 'modified': line_total_amount},
+            'net_price_unit': {'original': net_price_unit, 'modified': net_price_unit},
             'ordered_qty': {'original': ordered_qty, 'modified': ordered_qty},
             'supplier_product_internal_code': {
                 'original': supplier_product_internal_code,
                 'modified': supplier_product_internal_code,
             },
-            'units_per_box': {
-                'original': units_per_box,
-                'modified': units_per_box,
-            },
+            'units_per_box': {'original': units_per_box, 'modified': units_per_box},
             'uom': {'original': uom, 'modified': uom},
             'use_unit': {'original': use_unit, 'modified': use_unit},
             'vat_tax': {'original': vat_tax, 'modified': vat_tax},
@@ -503,22 +448,16 @@ class CustomerPurchaseOrderImporter(models.Model):
     def get_partner(self, ean_code):
         ResPartner = self.env['res.partner']
 
-        partner = ResPartner.search(
-            [
-                ('partner_ean_code', '=', ean_code),
-            ]
-        )
+        partner = ResPartner.search([('partner_ean_code', '=', ean_code)])
 
         return partner
 
     def get_branch(self, ean_code, product_type):
         ResPartner = self.env['res.partner']
 
-        branch = ResPartner.search(
-            [
-                ('partner_ean_code', '=', ean_code),
-            ]
-        ).filtered(lambda x: x.zone_accounts_line_ids.rp_za_product_type_id == product_type)
+        branch = ResPartner.search([('partner_ean_code', '=', ean_code)]).filtered(
+            lambda x: x.zone_accounts_line_ids.rp_za_product_type_id == product_type
+        )
 
         return branch
 
@@ -526,10 +465,7 @@ class CustomerPurchaseOrderImporter(models.Model):
         PreSaleOrder = self.env['pre.sale.order']
 
         purchase_order = PreSaleOrder.search_count(
-            [
-                ('name', '=', 'ORD' + po_num),
-                ('partner_shipping_id', '=', branch.id),
-            ]
+            [('name', '=', 'ORD' + po_num), ('partner_shipping_id', '=', branch.id)]
         )
 
         return purchase_order
@@ -590,10 +526,7 @@ class CustomerPurchaseOrderImporter(models.Model):
         if _due_date and not due_date:
             errors.append(_('~ {} is not valid.\n').format(_('Due date')))
 
-        start_date_is_earlier = self.start_date_is_earlier(
-            date_order,
-            due_date,
-        )
+        start_date_is_earlier = self.start_date_is_earlier(date_order, due_date)
         if not start_date_is_earlier:
             errors.append(_('~ {} cannot be earlier than {}.\n').format(_('Due date'), _('order date')))
 
@@ -660,23 +593,18 @@ class CustomerPurchaseOrderImporter(models.Model):
             if len(product_types) > 1:
                 errors.append(_('~ Some products have different product types.\n'))
             else:
-                product_type = ProductType.browse(
-                    list(product_types)[0],
-                )
+                product_type = ProductType.browse(list(product_types)[0])
 
         if not product_type:
             errors.append(
                 _(
-                    '~ Product type not found. This is because no product has been found in the '
-                    'database with its corresponding code. Without a defined product type, the '
-                    "branch will not be found, even when the branch's EAN code was correct.\n"
+                    '~ Product type not found. This is because no product has been found in the database '
+                    'with its corresponding code. Without a defined product type, the branch will not be '
+                    "found, even when the branch's EAN code was correct.\n"
                 )
             )
 
-        return {
-            'product_type': product_type,
-            'errors': errors,
-        }
+        return {'product_type': product_type, 'errors': errors}
 
     def get_product(self, config, code, is_ean_code=False, partner=None, description=''):
         CustomerPurchaseOrderConfigLine = self.env['customer.purchase.order.config.line']
@@ -754,16 +682,11 @@ class CustomerPurchaseOrderImporter(models.Model):
                                 'product_id': product.id,
                                 'product_uom': product.uom_id.id,
                                 'pricelist_price': (
-                                    PreSaleOrder._get_pricelist_price(
-                                        product,
-                                        pricelist,
-                                    )
+                                    PreSaleOrder._get_pricelist_price(product, pricelist)
                                     if pricelist
                                     else 0.0
                                 ),
-                                'tax_id': [
-                                    (6, 0, product.taxes_id.ids),
-                                ],
+                                'tax_id': [(6, 0, product.taxes_id.ids)],
                             }
                         )
 
@@ -782,21 +705,11 @@ class CustomerPurchaseOrderImporter(models.Model):
         BranchEANCode = self.env['branch.ean.code']
 
         branch_ean_code = BranchEANCode.search(
-            [
-                ('branch_id', '=', branch.id),
-                ('ean13_code', '=', ean13_code),
-                ('product_id', '=', product.id),
-            ]
+            [('branch_id', '=', branch.id), ('ean13_code', '=', ean13_code), ('product_id', '=', product.id)]
         )
 
         if not branch_ean_code:
-            BranchEANCode.create(
-                {
-                    'branch_id': branch.id,
-                    'ean13_code': ean13_code,
-                    'product_id': product.id,
-                }
-            )
+            BranchEANCode.create({'branch_id': branch.id, 'ean13_code': ean13_code, 'product_id': product.id})
 
     def get_config(self, partner):
         CustomerPurchaseOrderConfig = self.env['customer.purchase.order.config']
@@ -1046,10 +959,8 @@ class CustomerPurchaseOrderImporter(models.Model):
         processed_path = paths['processed']
         rejected_path = paths['rejected']
 
-        # if not manual_import:
-        #     _logger.info('Importing PO files from ' + import_path)
-
-        #     files = glob.glob('{}*.*'.format(import_path))
+        if not manual_import:
+            _logger.info('Importing PO files from ' + import_path)
 
         ftp = connect_ftp()
 
@@ -1076,21 +987,20 @@ class CustomerPurchaseOrderImporter(models.Model):
                 file_vals = self.get_full_file_data(lines)
 
                 if isinstance(file_vals, list):
-                    partner_name = file_vals.pop(-1)
+                    rejected_file = ''
 
+                    partner_name = file_vals.pop(-1).replace(' ', '_')
                     if partner_name not in _file:
-                        rejected_file = _file.replace('ORD_', 'ORD_{}_'.format(partner_name))
-
-                        ftp.rename(import_path + _file, import_path + rejected_file)
-
-                        _file = rejected_file
-
-                    msg_list.insert(0, _('FILE: {}\n').format(_file))
+                        rejected_file = _file.replace('ORD_', 'ORD_%s_' % partner_name)
 
+                    msg_list.insert(0, _('FILE: %s\n') % rejected_file)
                     msg_list.extend(file_vals)
                     not_imported.append(''.join(msg_list) + '\n')
 
-                    ftp.rename(import_path + _file, rejected_path + _file)
+                    if rejected_file in ftp.nlst(rejected_path):
+                        ftp.delete(rejected_path + rejected_file)
+
+                    ftp.rename(import_path + _file, rejected_path + rejected_file)
 
                     continue
                 else:
@@ -1115,10 +1025,10 @@ class CustomerPurchaseOrderImporter(models.Model):
                                 msg_list.insert(0, _('FILE: {}\n').format(_file))
                                 msg_list.append(
                                     _(
-                                        '~ No method has been defined for {}. Please contact '
-                                        'the developer to fix this issue.\n'
-                                    ).format(_('configuration type {}').format(config_type_name))
-                                    + '\n'
+                                        '~ No method has been defined for %s. Please contact the developer '
+                                        'to fix this issue.\n'
+                                    )
+                                    % (_('configuration type %s\n') % config_type_name)
                                 )
                                 not_imported.append(''.join(msg_list))
                                 continue
@@ -1131,17 +1041,17 @@ class CustomerPurchaseOrderImporter(models.Model):
 
                     ftp.rename(import_path + _file, processed_path + _file)
 
+        if len(imported) > 0:
+            msg += _('The following files have been imported successfully:\n') + ', '.join(sorted(imported))
+
         if len(not_imported) > 0:
-            msg += (
-                _('The following files have errors that need to be corrected before importing ' 'them:\n\n')
-                if manual_import
-                else ''
-            ) + ''.join(sorted(not_imported))
+            not_imported_msg = ''.join(sorted(not_imported))
 
-            # self._create_error_file(rejected_path, msg, ftp)
+            self._create_error_file(rejected_path, not_imported_msg, ftp)
 
-        if len(imported) > 0:
-            msg += _('The following files have been imported successfully:\n') + ', '.join(sorted(imported))
+            msg += (
+                _('The following files have errors that need to be corrected before importing them:\n\n')
+            ) + not_imported_msg
 
         self.env.cr.commit()
 
@@ -1150,31 +1060,28 @@ class CustomerPurchaseOrderImporter(models.Model):
         if manual_import:
             raise UserError(msg)
 
-    def _create_error_file(self, path, datas, ftp):
+    def _create_error_file(self, path, msg, ftp):
         IrAttachment = self.env['ir.attachment']
 
+        datas = bytes(msg, 'utf-8')
+
         today = str(datetime.today().date())
 
         name = _('[{}] Rejected Purchase Order Files').format(today)
         attachment_id = IrAttachment.search([('name', '=', name)])
         if attachment_id:
-            datas += base64.b64decode(attachment_id.datas)
+            datas += b64decode(attachment_id.datas)
 
-            attachment_id.write({'datas': base64.b64encode(datas.encode('utf-8')), 'name': name})
+            attachment_id.write({'datas': b64encode(datas), 'name': name})
 
         file_name = '_not_imported_{}.txt'.format(today.replace('-', ''))
-        file = BytesIO(bytes(datas, 'utf-8'))
+
+        file = BytesIO(datas)
         ftp.storbinary('STOR ' + file_name, file)
-        file.seek(0)
 
         if not attachment_id:
             IrAttachment.create(
-                {
-                    'datas_fname': file_name,
-                    'datas': base64.b64encode(file.read()),
-                    'name': name,
-                    'type': 'binary',
-                }
+                {'datas_fname': file_name, 'datas': b64encode(datas), 'name': name, 'type': 'binary'}
             )
 
         file.close()
diff --git a/wizard/customer_purchase_order_importer_wizard.py b/wizard/customer_purchase_order_importer_wizard.py
index 3b3ace3..5f41b1b 100644
--- a/wizard/customer_purchase_order_importer_wizard.py
+++ b/wizard/customer_purchase_order_importer_wizard.py
@@ -1,45 +1,45 @@
 import logging
 from base64 import b64decode
+from io import BytesIO
 from odoo import _, fields, models
+from odoo.addons.customer_purchase_order.models.customer_purchase_order_importer import connect_ftp
 from odoo.exceptions import UserError
-from shutil import move
-from tempfile import mkstemp
 
 _logger = logging.getLogger(__name__)
 
 
 class CustomerPurchaseOrderImporterWizard(models.TransientModel):
-    _name = "customer.purchase.order.importer.wizard"
-    _description = "Customer Purchase Order Importer Wizard"
+    _name = 'customer.purchase.order.importer.wizard'
+    _description = 'Customer Purchase Order Importer Wizard'
 
     name = fields.Char()
     files = fields.Many2many(
-        "ir.attachment",
-        "class_ir_attachments_rel",
-        "class_id",
-        "attachment_id",
-        string="Files to Import",
+        'ir.attachment', 'class_ir_attachments_rel', 'class_id', 'attachment_id', string='Files to Import'
     )
 
+    def prevent_dups(self, files):
+        no_dups = []
+
+        for file in files:
+            if file.name not in [f.name for f in no_dups]:
+                no_dups.append(file)
+
+        return no_dups
+
     def create_tmp_and_move_file(self, file, new_name, path='/tmp/'):
         if file:
-            tmp_file = mkstemp()[1]
-            with open(tmp_file, "wb+") as tmp:
-                tmp.write(b64decode(file.datas))
-                tmp.close()
+            ftp = connect_ftp()
+            ftp.cwd(path)
 
-            move(tmp_file, new_name)
+            tmp_file = BytesIO(b64decode(file.datas))
 
-    def prevent_dups(self, files):
-        no_dups = []
-        for file in files:
-            if file.name not in [f[1] for f in no_dups]:
-                no_dups.append((file, file.name))
+            ftp.storbinary('STOR %s' % file.name, tmp_file)
 
-        return [f[0] for f in no_dups]
+            tmp_file.seek(0)
+            tmp_file.close()
 
     def do_import(self):
-        CustomerPurchaseOrderImporter = self.env["customer.purchase.order.importer"]
+        CustomerPurchaseOrderImporter = self.env['customer.purchase.order.importer']
 
         file_list = []
 
@@ -48,12 +48,12 @@ class CustomerPurchaseOrderImporterWizard(models.TransientModel):
         files = self.prevent_dups(self.files)
         for file in files:
             try:
-                filename = '{}{}'.format(import_path, file.name)
+                filename = '%s%s' % (import_path, file.name)
 
                 self.create_tmp_and_move_file(file, filename, import_path)
 
                 file_list.append(filename)
             except UserError:
-                raise UserError(_("Error creating file %s") % file.name)
+                raise UserError(_('Error creating file %s') % file.name)
 
         CustomerPurchaseOrderImporter.do_import(files=file_list, manual_import=True)
diff --git a/wizard/rejected_import_wizard.py b/wizard/rejected_import_wizard.py
index 3136506..9562c0a 100644
--- a/wizard/rejected_import_wizard.py
+++ b/wizard/rejected_import_wizard.py
@@ -23,7 +23,8 @@ class RejectedImportWizard(models.Model):
         'ir.attachment',
         string='File',
         default=lambda self: self._default_file_id(),
-        domain="[('datas_fname', 'ilike', '_not_imported_')]")
+        domain="[('datas_fname', 'ilike', '_not_imported_')]",
+    )
 
     def _default_file_id(self):
         attachment = self.env['ir.attachment'].search(
-- 
GitLab