From 6b088cb64de7ec4050cc50d21c1f72f23441530d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20V=C3=A1squez?= <ramon.vasquez@eynes.com.ar> Date: Fri, 17 May 2024 16:35:40 -0300 Subject: [PATCH] [FIX][T4278] Cron & manual importers simultaneity --- models/customer_purchase_order_importer.py | 1549 +++++++++-------- models/pre_sale_order.py | 46 +- ...customer_purchase_order_importer_wizard.py | 43 +- 3 files changed, 826 insertions(+), 812 deletions(-) diff --git a/models/customer_purchase_order_importer.py b/models/customer_purchase_order_importer.py index f187cde..bbe624a 100644 --- a/models/customer_purchase_order_importer.py +++ b/models/customer_purchase_order_importer.py @@ -6,16 +6,15 @@ # ############################################################################## -from base64 import b64decode, b64encode import logging import pytz +from base64 import b64decode, b64encode from datetime import datetime, timedelta from ftplib import FTP from io import BytesIO from odoo import _, models from odoo.exceptions import UserError - _logger = logging.getLogger(__name__) @@ -29,462 +28,352 @@ class CustomerPurchaseOrderImporter(models.Model): _name = 'customer.purchase.order.importer' _description = 'Customer Purchase Order Importer' - def get_lines(self, file_txt): - lines = file_txt.split('\r\n') + def do_import(self, files=None, manual_import=False): # noqa + CustomerPurchaseOrderConfig = self.env['customer.purchase.order.config'] + PreSaleOrder = self.env['pre.sale.order'] - if lines[-1] == '': - lines.pop() + msg = '' + imported = [] + not_imported = [] + pre_sale_orders = [] - return lines + paths = self._get_paths() + import_path = paths['import'] + processed_path = paths['processed'] + rejected_path = paths['rejected'] - def get_full_file_data(self, lines): - msg_list = [] - address = None - partner = None - branch = None - product_type = None - pricelist = None - header_data = self.get_header_data(lines) + try: + ftp = connect_ftp() + except Exception as e: + raise UserError(_('~ Error connecting to FTP server.\n') + str(e)) - # PO number - po_number = header_data['purchase_order_number'] - if not po_number: - msg_list.append(_('~ Purchase order number not found.\n')) + _logger.info('Importing purchase order files from ' + import_path) - # Partner - partner_ean_code = header_data['partner_ean_code'] - if not partner_ean_code: - msg_list.append(_('~ Partner EAN code not found.\n')) - else: - partner = self.get_partner(partner_ean_code) - if not partner: - msg_list.append(_('~ Partner not found for EAN code {}.\n').format(partner_ean_code)) - else: - if len(partner) > 1: - partner = None - msg_list.append( - _('~ More than one partner found with EAN code {}.\n').format(partner_ean_code) - ) - else: - # Product type - product_codes = self.get_product_codes(lines[1:]) - _product_type = self.get_product_type(product_codes, partner) - product_type = _product_type['product_type'] + ftp.cwd(import_path) - if _product_type['errors']: - msg_list.extend(_product_type['errors']) + if manual_import: + self._create_tmp_files(ftp, files, path=import_path) - address = partner.address_get(['invoice']) + import_path += 'tmp\\' - # Branch - branch_ean_code = header_data['branch_ean_code'] - if not branch_ean_code: - msg_list.append(_('~ Branch EAN code not found.\n')) - else: - branch = self.get_branch(branch_ean_code, product_type) - if not branch: - msg_list.append(_('~ Branch not found for EAN code {}.\n').format(branch_ean_code)) - else: - if len(branch) > 1: - branch = None - msg_list.append( - _('~ More than one branch found with EAN code {}.\n').format(branch_ean_code) - ) - else: - # Pricelist - pricelist = branch.zone_accounts_line_ids.rp_za_pricelist - po_exists = self.purchase_order_exists_for_branch(po_number, branch) - if po_exists: - msg_list.append( - _('~ Purchase order ORD{} already exists for branch {}.\n').format( - po_number, branch.name - ) - ) + files = self._standarize_file_names(import_path, ftp) + for _file in files: + if not _file: + continue - if partner and branch: - if not self.vat_matches(partner, branch): - msg_list.append( - _('~ Partner VAT code ({}) does not match branch VAT code ({}).\n').format( - partner.vat, branch.vat - ) - ) + po_vals = {} - # Dates - dates = self.get_dates(header_data) - date_order = dates['date_order'] - delivery_date = dates['delivery_date'] - due_date = dates['due_date'] + file = BytesIO() + ftp.retrbinary('RETR ' + _file, file.write) + msg_list = [] - if dates['errors']: - msg_list.extend(dates['errors']) + try: + file_txt = file.getvalue().decode('utf-8') - # Product lines - po_lines = self.get_product_lines(lines, partner, product_type, pricelist, branch) + lines = self.get_lines(file_txt) - if po_lines['errors']: - msg_list.extend(po_lines['errors']) + file.close() - if msg_list: - msg_list += [partner.name.upper() if partner else 'PARTNER_NOT_FOUND_'] - return msg_list - else: - po_data = { - 'name': 'ORD' + po_number, - 'partner': partner.id, - 'partner_invoice_id': address['invoice'], - 'partner_shipping_id': branch.id, - 'price_type': 'gross', - 'date_order': date_order, - 'delivery_date': delivery_date, - 'due_date': due_date, - 'manual_create': False, - 'summary_code': po_lines['summary_code'], - 'summary_qty': po_lines['summary_qty'], - 'pre_order_line_ids': po_lines['po_lines'], - 'product_type': product_type.id, - } + file_vals = self.get_full_file_data(lines) - return po_data + if isinstance(file_vals, list): + rejected_file = '' - """All data is retrieved from the file with these two methods. Not all of it is - required nor used to create a purchase order, though it may be used in future - developments. - """ + partner_name = file_vals.pop(-1).replace(' ', '_') + if partner_name not in _file: + rejected_file = _file.replace('ORD_', 'ORD_%s_' % partner_name) - def get_header_data(self, lines): - partner_ean_code = lines[0][4:17].split() - partner_ean_code = partner_ean_code.pop() if partner_ean_code else None + msg_list.insert(0, _('FILE: %s\n') % rejected_file) + msg_list.extend(file_vals) + not_imported.append(''.join(msg_list) + '\n') - supplier_ean_code = lines[0][17:30].split() - supplier_ean_code = supplier_ean_code.pop() if supplier_ean_code else None + if rejected_file in ftp.nlst(rejected_path): + ftp.delete(rejected_path + rejected_file) - branch_ean_code = lines[0][30:43].split() - branch_ean_code = branch_ean_code.pop() if branch_ean_code else None + ftp.rename(import_path + _file, rejected_path + rejected_file) - emission_point_code = lines[0][44:47].split() - emission_point_code = emission_point_code.pop() if emission_point_code else None + continue + else: + partner = file_vals['partner'] + config = self.get_config(partner) + config_type = 'basic' - supplier_code = lines[0][57:67].split() - supplier_code = supplier_code.pop() if supplier_code else None + if config: + for c in config: + if c.config_type: + if c.config_type != 'internal_code': + config_type = c.config_type - supplier_description = lines[0][67:102].split() - supplier_description = supplier_description.pop() if supplier_description else None + try: + po_vals.update( + getattr(self, 'get_config_{}_vals'.format(config_type))(file_vals) + ) + except AttributeError: + _type = CustomerPurchaseOrderConfig._fields['config_type'].selection + config_type_name = dict(_type).get(config_type) - supplier_address = lines[0][102:137].split() - supplier_address = supplier_address.pop() if supplier_address else None + msg_list.insert(0, _('FILE: {}\n').format(_file)) + msg_list.append( + _( + '~ 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)) - supplier_phone = lines[0][137:177].split() - supplier_phone = supplier_phone.pop() if supplier_phone else None + continue - supplier_fax = lines[0][177:217].split() - supplier_fax = supplier_fax.pop() if supplier_fax else None + imported.append(_file) - supplier_city = lines[0][217:252].split() - supplier_city = supplier_city.pop() if supplier_city else None + po_vals.update(self._update_file_vals(file_vals)) + pre_sale_orders.append(self._refactor_product_vals_before_import(po_vals)) - supplier_zip = lines[0][252:257].split() - supplier_zip = supplier_zip.pop() if supplier_zip else None + if _file in ftp.nlst(processed_path): + ftp.delete(processed_path + _file) - date_order = lines[0][257:265].split() - date_order = date_order.pop() if date_order else None + ftp.rename(import_path + _file, processed_path + _file) + except Exception as e: + _logger.error(e) - delivery_date = lines[0][267:275].split() - delivery_date = delivery_date.pop() if delivery_date else None + not_imported.append(_('~ Error importing file %s.\n') % _file) - due_date = lines[0][277:285].split() - due_date = due_date.pop() if due_date else None + continue - delivery_shift_date = lines[0][464:476].split() - delivery_shift_date = delivery_shift_date.pop() if delivery_shift_date else None + if len(imported) > 0: + msg += _('The following files have been imported successfully:\n') + ', '.join(sorted(imported)) - payment_method = lines[0][287:322].split() - payment_method = payment_method.pop() if payment_method else None + if len(not_imported) > 0: + not_imported_msg = ''.join(sorted(not_imported)) - total_lines = lines[0][322:327].split() - total_lines = int(total_lines.pop()) if total_lines else None + self._create_error_file(ftp, import_path, not_imported_msg) - total_amount = lines[0][327:342].split() - total_amount = float(total_amount.pop()) if total_amount else 0.0 + msg += ( + _('The following files have errors that need to be corrected before importing them:\n\n') + ) + not_imported_msg - sending_date = lines[0][342:348].split() - sending_date = sending_date.pop() if sending_date else None + PreSaleOrder.create(pre_sale_orders) - sending_time = lines[0][348:352].split() - sending_time = sending_time.pop() if sending_time else None + _logger.info(_('Purchase order files import finished!')) - payment_method_description1 = lines[0][352:387].split() - payment_method_description1 = ( - payment_method_description1.pop() if payment_method_description1 else None - ) + if manual_import: + raise UserError(msg) - payment_method_description2 = lines[0][387:422].split() - payment_method_description2 = ( - payment_method_description2.pop() if payment_method_description2 else None - ) + def get_branch(self, ean_code, product_type): + ResPartner = self.env['res.partner'] - purchase_order_payment_days = lines[0][422:425].split() - purchase_order_payment_days = ( - purchase_order_payment_days.pop() if purchase_order_payment_days else None + branch = ResPartner.search([('partner_ean_code', '=', ean_code)]).filtered( + lambda x: x.zone_accounts_line_ids.rp_za_product_type_id == product_type ) - invoice_payment_days = lines[0][425:428].split() - invoice_payment_days = invoice_payment_days.pop() if invoice_payment_days else None + return branch - vat_percentage = lines[0][428:431].split() - vat_percentage = float(vat_percentage.pop()) if vat_percentage else 0.0 + def get_config(self, partner): + CustomerPurchaseOrderConfig = self.env['customer.purchase.order.config'] - tax_amount = lines[0][431:446].split() - tax_amount = float(tax_amount.pop()) if tax_amount else 0.0 + if partner: + config = CustomerPurchaseOrderConfig.search([('partner', '=', partner)]) - purchase_order_gross_amount = lines[0][446:461].split() - purchase_order_gross_amount = ( - float(purchase_order_gross_amount.pop()) if purchase_order_gross_amount else 0.0 - ) + return config - basic_percentage_per_unit = lines[0][461:464].split() - basic_percentage_per_unit = ( - float(basic_percentage_per_unit.pop()) if basic_percentage_per_unit else 0.0 - ) + return False - purchase_order_number = lines[0][476:496].split() - purchase_order_number = purchase_order_number.pop() if purchase_order_number else None + """Config methods receive a dict with all the values of each file --some of which will + probably be empty. The product lines key ('pre_order_line_ids') is a list of tuples + with the format (0, 0, {...product data...}). The dict has the following structure: - purchase_order_down_index = lines[0][496:496].split() - purchase_order_down_index = purchase_order_down_index.pop() if purchase_order_down_index else None + { + 'some_value': { + 'original': 'some_value', + 'modified': 'some_value', + }, + } - pier = lines[0][497:507].split() - pier = pier.pop() if pier else None + If the values of the product lines are to be modified, you can access them this way: - purchase_order_type = lines[0][507:542].split() - purchase_order_type = purchase_order_type.pop() if purchase_order_type else None + for line in vals['pre_order_line_ids']: + variable = line[2]['key']['original'] + ...some logic... + line[2]['key']['modified'] = variable - shipping_destination_ean_code = lines[0][542:555].split() - shipping_destination_ean_code = ( - shipping_destination_ean_code.pop() if shipping_destination_ean_code else None - ) + Return the modified dict (the only method that should return the dict untouched is + get_config_basic_vals). - return { - 'basic_percentage_per_unit': basic_percentage_per_unit, - 'branch_ean_code': branch_ean_code, - 'date_order': date_order, - 'delivery_date': delivery_date, - 'delivery_shift_date': delivery_shift_date, - 'due_date': due_date, - 'emission_point_code': emission_point_code, - 'invoice_payment_days': invoice_payment_days, - 'partner_ean_code': partner_ean_code, - 'payment_method': payment_method, - 'payment_method_description1': payment_method_description1, - 'payment_method_description2': payment_method_description2, - 'pier': pier, - 'purchase_order_down_index': purchase_order_down_index, - 'purchase_order_gross_amount': purchase_order_gross_amount, - 'purchase_order_number': purchase_order_number, - 'purchase_order_payment_days': purchase_order_payment_days, - 'purchase_order_type': purchase_order_type, - 'sending_date': sending_date, - 'sending_time': sending_time, - 'shipping_destination_ean_code': shipping_destination_ean_code, - 'supplier_address': supplier_address, - 'supplier_city': supplier_city, - 'supplier_code': supplier_code, - 'supplier_description': supplier_description, - 'supplier_ean_code': supplier_ean_code, - 'supplier_fax': supplier_fax, - 'supplier_phone': supplier_phone, - 'supplier_zip': supplier_zip, - 'tax_amount': tax_amount, - 'total_amount': total_amount, - 'total_lines': total_lines, - 'vat_percentage': vat_percentage, - } + -------------------------------------------------------------------------------------- - """This method retrieves all product line data and returns a dictionary with lists as - values. Each list wil have two items, both are the same. In spite of its redundance, - this will help updating the file data through the config type methods without - overwriting the original data. This is useful in the cases where more than one config - type is used in the same file. + The names of these methods must be defined using the following format: + + get_config_{config_type}_vals + + Where {config_type} is the value of the config_type selection field of model + 'customer.purchase.order.config' (see the file customer_purchase_order_config.py). + This is required for the methods to be called based on the config_type value set in + the configuration found for the imported purchase order. + + -------------------------------------------------------------------------------------- + + You should be aware that some keys of the dict are added by the method + get_product_lines (see above). These keys are: 'box_kg', 'content', 'is_maxipiece', + 'name', 'pricelist_price', 'product_id', 'product_uom', 'tax_id'. The values of these + keys cannot be accessed by the key 'original', and they mustn't be modified. """ - def get_product_data(self, line): - line_number = line[4:10].split() - line_number = line_number.pop() if line_number else None + def get_config_basic_vals(self, vals): + return vals - ean13_code = line[10:23].split().pop() - ean13_code = ean13_code if ean13_code else None + def get_config_boxes_based_on_box_kg_vals(self, vals): + ProductProduct = self.env['product.product'] - item_size_description = line[24:34].split() - item_size_description = item_size_description.pop() if item_size_description else None + for line in vals['pre_order_line_ids']: + ordered_qty = line[2]['ordered_qty']['original'] + box_kg = line[2]['box_kg'] - item_colour_description = line[34:44].split() - item_colour_description = item_colour_description.pop() if item_colour_description else None + if ordered_qty and box_kg and box_kg > 0: + line[2]['box_qty']['modified'] = int(ordered_qty / box_kg) + else: + product_id = line[2]['product_id'] + product = ProductProduct.browse(product_id) - item_short_description = line[44:79] - item_short_description = item_short_description if item_short_description else None + if product: + line[2]['errors'].append( + _('~ Box Kilograms not set for product {}.\n').format(product.name) + ) - item_description1 = line[79:114].split() - item_description1 = item_description1.pop() if item_description1 else None + return vals - item_description2 = line[114:149].split() - item_description2 = item_description2.pop() if item_description2 else None + def get_config_boxes_based_on_units_vals(self, vals): + for line in vals['pre_order_line_ids']: + ordered_qty = line[2]['ordered_qty']['original'] + units_per_box = line[2]['units_per_box']['original'] - item_description3 = line[149:184].split() - item_description3 = item_description3.pop() if item_description3 else None + if ordered_qty and units_per_box and units_per_box > 0: + line[2]['box_qty']['modified'] = int(ordered_qty / units_per_box) - item_description4 = line[184:219].split() - item_description4 = item_description4.pop() if item_description4 else None + return vals - supplier_product_internal_code = line[219:233].split() - supplier_product_internal_code = ( - supplier_product_internal_code.pop() if supplier_product_internal_code else None - ) + def get_config_custom_uom_vals(self, vals): + ProductProduct = self.env['product.product'] - uom = line[233:236].split() - uom = uom.pop() if uom else None + for line in vals['pre_order_line_ids']: + box_qty = line[2]['box_qty']['original'] + content = line[2]['content'] + uom = line[2]['uom']['original'] - use_unit = line[236:240].split() - use_unit = use_unit.pop() if use_unit else None + if box_qty and content and content > 0: + if uom and uom.upper() == 'PZA': + line[2]['box_qty']['modified'] = int(box_qty / content) + else: + product_id = line[2]['product_id'] + product = ProductProduct.browse(product_id) - box_qty = line[240:247].split() - box_qty = float(box_qty.pop()) if box_qty else 0.0 + line[2]['errors'].append(_('~ Content not set for product {}.\n').format(product.name)) - box_qty_ean_set = line[247:254].split() - box_qty_ean_set = float(box_qty_ean_set.pop()) if box_qty_ean_set else 0.0 + return vals - box_qty_pallets = line[254:261].split() - box_qty_pallets = float(box_qty_pallets.pop()) if box_qty_pallets else 0.0 + def get_config_internal_code(self, partner): + _config = self.get_config(partner) + config = None - ordered_qty = line[261:272].split() - ordered_qty = float(ordered_qty.pop()) if ordered_qty else 0.0 + if _config: + for c in _config: + if c.config_type == 'internal_code': + config = c + break - units_per_box = line[272:279].split() - units_per_box = float(units_per_box.pop()) if units_per_box else 0.0 + return config - net_price_unit = line[279:294].split() - net_price_unit = float(net_price_unit.pop()) if net_price_unit else 0.0 + def get_config_item_gross_price_vals(self, vals): + ProductProduct = self.env['product.product'] - gross_price_unit = line[294:309].split() - gross_price_unit = float(gross_price_unit.pop()) if gross_price_unit else 0.0 + for line in vals['pre_order_line_ids']: + box_qty = line[2]['box_qty']['original'] - line_total_amount = line[309:324].split() - line_total_amount = float(line_total_amount.pop()) if line_total_amount else 0.0 + if box_qty and box_qty > 0: + line[2]['gross_price_unit']['modified'] /= box_qty + else: + product_id = line[2]['product_id'] + product = ProductProduct.browse(product_id) - additional_line_expense = line[324:339].split() - additional_line_expense = float(additional_line_expense.pop()) if additional_line_expense else 0.0 + line[2]['errors'].append(_('~ Box Quantity not set for product {}.\n').format(product.name)) - vat_tax = line[339:347].split() - vat_tax = float(vat_tax.pop()) if vat_tax else 0.0 + return vals - internal_tax = line[347:355].split() - internal_tax = float(internal_tax.pop()) if internal_tax else 0.0 + def get_config_maxipiece_vals(self, vals): + ProductProduct = self.env['product.product'] - discount1 = line[355:365].split() - discount1 = float(discount1.pop()) if discount1 else 0.0 + for line in vals['pre_order_line_ids']: + box_kg = line[2]['box_kg'] + ordered_qty = line[2]['ordered_qty']['original'] - discount2 = line[365:375].split() - discount2 = float(discount2.pop()) if discount2 else 0.0 + if box_kg and box_kg > 0 and ordered_qty: + is_maxipiece = line[2]['is_maxipiece'] + if is_maxipiece: + line[2]['box_qty']['modified'] = ordered_qty / box_kg + else: + product_id = line[2]['product_id'] + product = ProductProduct.browse(product_id) - discount3 = line[375:385].split() - discount3 = float(discount3.pop()) if discount3 else 0.0 + line[2]['errors'].append(_('~ Box Kilograms not set for product {}.\n').format(product.name)) - discount4 = line[385:395].split() - discount4 = float(discount4.pop()) if discount4 else 0.0 + return vals - discount5 = line[395:405].split() - discount5 = float(discount5.pop()) if discount5 else 0.0 + def get_config_ordered_qty_for_kg_vals(self, vals): + for line in vals['pre_order_line_ids']: + ordered_qty = line[2]['ordered_qty']['original'] + product_uom = line[2]['product_uom'] - discount6 = line[405:415].split() - discount6 = float(discount6.pop()) if discount6 else 0.0 + if ordered_qty and product_uom == self.env.ref('uom.product_uom_kgm').id: + box_kg = line[2]['box_kg'] + if box_kg > 0: + line[2]['box_qty']['modified'] = ordered_qty / box_kg - comeback = line[415:425].split() - comeback = float(comeback.pop()) if comeback else 0.0 + return vals - return { - 'additional_line_expense': { - 'original': additional_line_expense, - '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}, - 'comeback': {'original': comeback, 'modified': comeback}, - 'discount1': {'original': discount1, 'modified': discount1}, - 'discount2': {'original': discount2, 'modified': discount2}, - 'discount3': {'original': discount3, 'modified': discount3}, - 'discount4': {'original': discount4, 'modified': discount4}, - 'discount5': {'original': discount5, 'modified': discount5}, - '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}, - '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_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}, - '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}, - 'uom': {'original': uom, 'modified': uom}, - 'use_unit': {'original': use_unit, 'modified': use_unit}, - 'vat_tax': {'original': vat_tax, 'modified': vat_tax}, - } - - def get_partner(self, ean_code): - ResPartner = self.env['res.partner'] - - partner = ResPartner.search([('partner_ean_code', '=', ean_code)]) - - return partner - - def get_branch(self, ean_code, product_type): - ResPartner = self.env['res.partner'] + def get_config_ordered_qty_vals(self, vals): + for line in vals['pre_order_line_ids']: + ordered_qty = line[2]['ordered_qty']['original'] + product_uom = line[2]['product_uom'] - branch = ResPartner.search([('partner_ean_code', '=', ean_code)]).filtered( - lambda x: x.zone_accounts_line_ids.rp_za_product_type_id == product_type - ) + if ordered_qty and product_uom != self.env.ref('uom.product_uom_kgm').id: + line[2]['box_qty']['modified'] = ordered_qty + else: + box_kg = line[2]['box_kg'] + if box_kg > 0: + line[2]['box_qty']['modified'] = ordered_qty / box_kg - return branch + return vals - def purchase_order_exists_for_branch(self, po_num, branch): - PreSaleOrder = self.env['pre.sale.order'] + def get_config_product_uom_vals(self, vals): + for line in vals['pre_order_line_ids']: + box_qty = line[2]['box_qty']['original'] + ordered_qty = line[2]['ordered_qty']['original'] + product_uom = line[2]['product_uom'] + content = line[2]['content'] - purchase_order = PreSaleOrder.search_count( - [('name', '=', 'ORD' + po_num), ('partner_shipping_id', '=', branch.id)] - ) + if box_qty and ordered_qty and product_uom and content and content > 0: + if product_uom == self.env.ref('uom.product_uom_unit').id: + line[2]['box_qty']['modified'] = int(box_qty / content) + elif product_uom == self.env.ref('uom.product_uom_kgm').id: + box_kg = line[2]['box_kg'] + if box_kg > 0: + _box_qty = int(ordered_qty / box_kg) - return purchase_order + if _box_qty < 1 and _box_qty > 0: + line[2]['box_qty']['modified'] = 1 + elif box_qty >= 1: + line[2]['box_qty']['modified'] = _box_qty - def vat_matches(self, partner, branch): - return partner.vat == branch.vat + return vals - def is_valid_date(self, date): - if isinstance(date, str): - if len(date) == 8: - date += '0000' + def get_config_units_as_boxes_vals(self, vals): + for line in vals['pre_order_line_ids']: + product_uom = line[2]['product_uom'] + units_per_box = line[2]['units_per_box']['original'] - try: - _date = datetime.strptime(date, '%Y%m%d%H%M') - return _date - except ValueError: - return False + if product_uom and product_uom == self.env.ref('uom.product_uom_kgm').id and units_per_box: + line[2]['box_qty']['modified'] = units_per_box - return False + return vals def get_dates(self, vals): errors = [] @@ -547,522 +436,621 @@ class CustomerPurchaseOrderImporter(models.Model): 'errors': errors, } - def get_time_offset(self): - try: - user_tz = pytz.timezone(self.env.context.get('tz') or self.env.user.tz) - time_offset = str(user_tz.localize(datetime.now()))[-6:-3] - offset = int(time_offset) * (-1) - except Exception: - offset = 3 - - return offset - - def start_date_is_earlier(self, start_date, end_date): - return start_date <= end_date - - def get_product_codes(self, lines): - codes = [] - for line in lines: - codes.append(line[10:24].replace(' ', '')) - - return codes - - def get_product_type(self, product_codes, partner): - ProductType = self.env['product.type'] - - errors = [] - product_types = [] + def get_full_file_data(self, lines): + msg_list = [] + address = None + partner = None + branch = None product_type = None + pricelist = None + header_data = self.get_header_data(lines) - if product_codes and partner: - for code in product_codes: - config = self.get_config_internal_code(partner.id if partner else None) + # PO number + po_number = header_data['purchase_order_number'] + if not po_number: + msg_list.append(_('~ Purchase order number not found.\n')) - _product = self.get_product(config, code, partner=partner) - product = _product.get('product') - if not product: - continue + # Partner + partner_ean_code = header_data['partner_ean_code'] + if not partner_ean_code: + msg_list.append(_('~ Partner EAN code not found.\n')) + else: + partner = self.get_partner(partner_ean_code) + if not partner: + msg_list.append(_('~ Partner not found for EAN code {}.\n').format(partner_ean_code)) + else: + if len(partner) > 1: + partner = None + msg_list.append( + _('~ More than one partner found with EAN code {}.\n').format(partner_ean_code) + ) else: - if product.product_type_id: - product_types.append(product.product_type_id.id) - else: - continue + # Product type + product_codes = self.get_product_codes(lines[1:]) + _product_type = self.get_product_type(product_codes, partner) + product_type = _product_type['product_type'] - product_types = set(product_types) - if product_types: - if len(product_types) > 1: - errors.append(_('~ Some products have different product types.\n')) + if _product_type['errors']: + msg_list.extend(_product_type['errors']) + + address = partner.address_get(['invoice']) + + # Branch + branch_ean_code = header_data['branch_ean_code'] + if not branch_ean_code: + msg_list.append(_('~ Branch EAN code not found.\n')) + else: + branch = self.get_branch(branch_ean_code, product_type) + if not branch: + msg_list.append(_('~ Branch not found for EAN code {}.\n').format(branch_ean_code)) else: - product_type = ProductType.browse(list(product_types)[0]) + if len(branch) > 1: + branch = None + msg_list.append( + _('~ More than one branch found with EAN code {}.\n').format(branch_ean_code) + ) + else: + # Pricelist + pricelist = branch.zone_accounts_line_ids.rp_za_pricelist + po_exists = self.purchase_order_exists_for_branch(po_number, branch) + if po_exists: + msg_list.append( + _('~ Purchase order ORD{} already exists for branch {}.\n').format( + po_number, branch.name + ) + ) - 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" + if partner and branch: + if not self.vat_matches(partner, branch): + msg_list.append( + _('~ Partner VAT code ({}) does not match branch VAT code ({}).\n').format( + partner.vat, branch.vat + ) ) - ) - return {'product_type': product_type, 'errors': errors} + # Dates + dates = self.get_dates(header_data) + date_order = dates['date_order'] + delivery_date = dates['delivery_date'] + due_date = dates['due_date'] - def get_product(self, config, code, is_ean_code=False, partner=None, description=''): - CustomerPurchaseOrderConfigLine = self.env['customer.purchase.order.config.line'] - ProductProduct = self.env['product.product'] + if dates['errors']: + msg_list.extend(dates['errors']) - errors = [] + # Product lines + po_lines = self.get_product_lines(lines, partner, product_type, pricelist, branch) - product = ProductProduct.search(['|', ('barcode', '=', code), ('default_code', '=', code)]) + if po_lines['errors']: + msg_list.extend(po_lines['errors']) - if code and partner: - if config: - config_line = CustomerPurchaseOrderConfigLine.search( - [ - ('product_internal_code', '=', code), - ('customer_purchase_order_config_id', '=', config.id), - ] - ) + if msg_list: + msg_list += [partner.name.upper() if partner else 'PARTNER_NOT_FOUND_'] + return msg_list + else: + po_data = { + 'name': 'ORD' + po_number, + 'partner': partner.id, + 'partner_invoice_id': address['invoice'], + 'partner_shipping_id': branch.id, + 'price_type': 'gross', + 'date_order': date_order, + 'delivery_date': delivery_date, + 'due_date': due_date, + 'manual_create': False, + 'summary_code': po_lines['summary_code'], + 'summary_qty': po_lines['summary_qty'], + 'pre_order_line_ids': po_lines['po_lines'], + 'product_type': product_type.id, + } - if config_line: - product = config_line.product + return po_data - if not product: - errors.append(_('~ Product %s with code %s not found.\n') % (description, code)) - elif len(product) > 1: - errors.append(_('~ More than one product found with EAN code %s.\n') % code) - product = None + """All data is retrieved from the file with these two methods. Not all of it is + required nor used to create a purchase order, though it may be used in future + developments. + """ - return {'product': product, 'errors': errors} + def get_header_data(self, lines): + partner_ean_code = lines[0][4:17].split() + partner_ean_code = partner_ean_code.pop() if partner_ean_code else None - def get_product_lines(self, lines, partner, product_type, pricelist, branch): - PreSaleOrder = self.env['pre.sale.order'] + supplier_ean_code = lines[0][17:30].split() + supplier_ean_code = supplier_ean_code.pop() if supplier_ean_code else None - errors = [] - po_lines = [] - summary_code = 0 - summary_qty = 0 - po_lines = [] + branch_ean_code = lines[0][30:43].split() + branch_ean_code = branch_ean_code.pop() if branch_ean_code else None - _last_line_is_empty = len(lines[-1]) == 0 - product_lines = lines[1 : -1 if _last_line_is_empty else None] - for line in product_lines: - po_line = () + emission_point_code = lines[0][44:47].split() + emission_point_code = emission_point_code.pop() if emission_point_code else None - if len(line) > 0: - product_data = self.get_product_data(line) + supplier_code = lines[0][57:67].split() + supplier_code = supplier_code.pop() if supplier_code else None - ean13_code = product_data['ean13_code']['original'] - ordered_qty = product_data['ordered_qty']['original'] - product_description = product_data['item_short_description']['original'] + supplier_description = lines[0][67:102].split() + supplier_description = supplier_description.pop() if supplier_description else None - config = self.get_config_internal_code(partner.id if partner else None) + supplier_address = lines[0][102:137].split() + supplier_address = supplier_address.pop() if supplier_address else None - _product = self.get_product( - config, ean13_code, is_ean_code=True, partner=partner, description=product_description - ) - if _product.get('errors'): - errors.extend(_product.get('errors')) - continue + supplier_phone = lines[0][137:177].split() + supplier_phone = supplier_phone.pop() if supplier_phone else None - product = _product.get('product') - if product: - if branch and ean13_code: - self.save_branch_ean_code(branch, ean13_code, product) + supplier_fax = lines[0][177:217].split() + supplier_fax = supplier_fax.pop() if supplier_fax else None - if product_type: - summary_code += int(product.default_code) - summary_qty += ordered_qty + supplier_city = lines[0][217:252].split() + supplier_city = supplier_city.pop() if supplier_city else None - product_data.update( - { - 'box_kg': product.box_kgs, - 'content': product.content, - 'is_maxipiece': product.is_maxipiece, - 'name': product.name, - 'product_id': product.id, - 'product_uom': product.uom_id.id, - 'pricelist_price': ( - PreSaleOrder._get_pricelist_price(product, pricelist) - if pricelist - else 0.0 - ), - 'tax_id': [(6, 0, product.taxes_id.ids)], - } - ) + supplier_zip = lines[0][252:257].split() + supplier_zip = supplier_zip.pop() if supplier_zip else None - po_line = (0, 0, product_data) + date_order = lines[0][257:265].split() + date_order = date_order.pop() if date_order else None - po_lines.append(po_line) + delivery_date = lines[0][267:275].split() + delivery_date = delivery_date.pop() if delivery_date else None - return { - 'po_lines': po_lines, - 'summary_code': summary_code, - 'summary_qty': summary_qty, - 'errors': errors, - } + due_date = lines[0][277:285].split() + due_date = due_date.pop() if due_date else None - def save_branch_ean_code(self, branch, ean13_code, product): - BranchEANCode = self.env['branch.ean.code'] + delivery_shift_date = lines[0][464:476].split() + delivery_shift_date = delivery_shift_date.pop() if delivery_shift_date else None - branch_ean_code = BranchEANCode.search( - [('branch_id', '=', branch.id), ('ean13_code', '=', ean13_code), ('product_id', '=', product.id)] - ) + payment_method = lines[0][287:322].split() + payment_method = payment_method.pop() if payment_method else None - if not branch_ean_code: - BranchEANCode.create({'branch_id': branch.id, 'ean13_code': ean13_code, 'product_id': product.id}) + total_lines = lines[0][322:327].split() + total_lines = int(total_lines.pop()) if total_lines else None - def get_config(self, partner): - CustomerPurchaseOrderConfig = self.env['customer.purchase.order.config'] + total_amount = lines[0][327:342].split() + total_amount = float(total_amount.pop()) if total_amount else 0.0 - if partner: - config = CustomerPurchaseOrderConfig.search([('partner', '=', partner)]) + sending_date = lines[0][342:348].split() + sending_date = sending_date.pop() if sending_date else None - return config + sending_time = lines[0][348:352].split() + sending_time = sending_time.pop() if sending_time else None - return False + payment_method_description1 = lines[0][352:387].split() + payment_method_description1 = ( + payment_method_description1.pop() if payment_method_description1 else None + ) - def get_config_internal_code(self, partner): - _config = self.get_config(partner) - config = None + payment_method_description2 = lines[0][387:422].split() + payment_method_description2 = ( + payment_method_description2.pop() if payment_method_description2 else None + ) - if _config: - for c in _config: - if c.config_type == 'internal_code': - config = c - break + purchase_order_payment_days = lines[0][422:425].split() + purchase_order_payment_days = ( + purchase_order_payment_days.pop() if purchase_order_payment_days else None + ) - return config + invoice_payment_days = lines[0][425:428].split() + invoice_payment_days = invoice_payment_days.pop() if invoice_payment_days else None - """Config methods receive a dict with all the values of each file --some of which will - probably be empty. The product lines key ('pre_order_line_ids') is a list of tuples - with the format (0, 0, {...product data...}). The dict has the following structure: + vat_percentage = lines[0][428:431].split() + vat_percentage = float(vat_percentage.pop()) if vat_percentage else 0.0 - { - 'some_value': { - 'original': 'some_value', - 'modified': 'some_value', - }, - } + tax_amount = lines[0][431:446].split() + tax_amount = float(tax_amount.pop()) if tax_amount else 0.0 - If the values of the product lines are to be modified, you can access them this way: + purchase_order_gross_amount = lines[0][446:461].split() + purchase_order_gross_amount = ( + float(purchase_order_gross_amount.pop()) if purchase_order_gross_amount else 0.0 + ) - for line in vals['pre_order_line_ids']: - variable = line[2]['key']['original'] - ...some logic... - line[2]['key']['modified'] = variable + basic_percentage_per_unit = lines[0][461:464].split() + basic_percentage_per_unit = ( + float(basic_percentage_per_unit.pop()) if basic_percentage_per_unit else 0.0 + ) - Return the modified dict (the only method that should return the dict untouched is - get_config_basic_vals). + purchase_order_number = lines[0][476:496].split() + purchase_order_number = purchase_order_number.pop() if purchase_order_number else None - -------------------------------------------------------------------------------------- + purchase_order_down_index = lines[0][496:496].split() + purchase_order_down_index = purchase_order_down_index.pop() if purchase_order_down_index else None - The names of these methods must be defined using the following format: + pier = lines[0][497:507].split() + pier = pier.pop() if pier else None - get_config_{config_type}_vals + purchase_order_type = lines[0][507:542].split() + purchase_order_type = purchase_order_type.pop() if purchase_order_type else None - Where {config_type} is the value of the config_type selection field of model - 'customer.purchase.order.config' (see the file customer_purchase_order_config.py). - This is required for the methods to be called based on the config_type value set in - the configuration found for the imported purchase order. + shipping_destination_ean_code = lines[0][542:555].split() + shipping_destination_ean_code = ( + shipping_destination_ean_code.pop() if shipping_destination_ean_code else None + ) - -------------------------------------------------------------------------------------- + return { + 'basic_percentage_per_unit': basic_percentage_per_unit, + 'branch_ean_code': branch_ean_code, + 'date_order': date_order, + 'delivery_date': delivery_date, + 'delivery_shift_date': delivery_shift_date, + 'due_date': due_date, + 'emission_point_code': emission_point_code, + 'invoice_payment_days': invoice_payment_days, + 'partner_ean_code': partner_ean_code, + 'payment_method': payment_method, + 'payment_method_description1': payment_method_description1, + 'payment_method_description2': payment_method_description2, + 'pier': pier, + 'purchase_order_down_index': purchase_order_down_index, + 'purchase_order_gross_amount': purchase_order_gross_amount, + 'purchase_order_number': purchase_order_number, + 'purchase_order_payment_days': purchase_order_payment_days, + 'purchase_order_type': purchase_order_type, + 'sending_date': sending_date, + 'sending_time': sending_time, + 'shipping_destination_ean_code': shipping_destination_ean_code, + 'supplier_address': supplier_address, + 'supplier_city': supplier_city, + 'supplier_code': supplier_code, + 'supplier_description': supplier_description, + 'supplier_ean_code': supplier_ean_code, + 'supplier_fax': supplier_fax, + 'supplier_phone': supplier_phone, + 'supplier_zip': supplier_zip, + 'tax_amount': tax_amount, + 'total_amount': total_amount, + 'total_lines': total_lines, + 'vat_percentage': vat_percentage, + } - You should be aware that some keys of the dict are added by the method - get_product_lines (see above). These keys are: 'box_kg', 'content', 'is_maxipiece', - 'name', 'pricelist_price', 'product_id', 'product_uom', 'tax_id'. The values of these - keys cannot be accessed by the key 'original', and they mustn't be modified. + """This method retrieves all product line data and returns a dictionary with lists as + values. Each list wil have two items, both are the same. In spite of its redundance, + this will help updating the file data through the config type methods without + overwriting the original data. This is useful in the cases where more than one config + type is used in the same file. """ - def get_config_basic_vals(self, vals): - return vals + def get_lines(self, file_txt): + lines = file_txt.split('\r\n') - def get_config_boxes_based_on_box_kg_vals(self, vals): - ProductProduct = self.env['product.product'] + if lines[-1] == '': + lines.pop() - for line in vals['pre_order_line_ids']: - ordered_qty = line[2]['ordered_qty']['original'] - box_kg = line[2]['box_kg'] + return lines - if ordered_qty and box_kg and box_kg > 0: - line[2]['box_qty']['modified'] = int(ordered_qty / box_kg) - else: - product_id = line[2]['product_id'] - product = ProductProduct.browse(product_id) + def get_partner(self, ean_code): + ResPartner = self.env['res.partner'] + partner = ResPartner.search([('partner_ean_code', '=', ean_code)]) + return partner - if product: - line[2]['errors'].append( - _('~ Box Kilograms not set for product {}.\n').format(product.name) - ) + def get_product(self, config, code, is_ean_code=False, partner=None, description=''): + CustomerPurchaseOrderConfigLine = self.env['customer.purchase.order.config.line'] + ProductProduct = self.env['product.product'] - return vals + errors = [] - def get_config_boxes_based_on_units_vals(self, vals): - for line in vals['pre_order_line_ids']: - ordered_qty = line[2]['ordered_qty']['original'] - units_per_box = line[2]['units_per_box']['original'] + product = ProductProduct.search(['|', ('barcode', '=', code), ('default_code', '=', code)]) - if ordered_qty and units_per_box and units_per_box > 0: - line[2]['box_qty']['modified'] = int(ordered_qty / units_per_box) + if code and partner: + if config: + config_line = CustomerPurchaseOrderConfigLine.search( + [ + ('product_internal_code', '=', code), + ('customer_purchase_order_config_id', '=', config.id), + ] + ) - return vals + if config_line: + product = config_line.product - def get_config_custom_uom_vals(self, vals): - ProductProduct = self.env['product.product'] + if not product: + errors.append(_('~ Product %s with code %s not found.\n') % (description, code)) + elif len(product) > 1: + errors.append(_('~ More than one product found with EAN code %s.\n') % code) + product = None - for line in vals['pre_order_line_ids']: - box_qty = line[2]['box_qty']['original'] - content = line[2]['content'] - uom = line[2]['uom']['original'] + return {'product': product, 'errors': errors} - if box_qty and content and content > 0: - if uom and uom.upper() == 'PZA': - line[2]['box_qty']['modified'] = int(box_qty / content) - else: - product_id = line[2]['product_id'] - product = ProductProduct.browse(product_id) + def get_product_codes(self, lines): + codes = [] + for line in lines: + codes.append(line[10:24].replace(' ', '')) - line[2]['errors'].append(_('~ Content not set for product {}.\n').format(product.name)) + return codes - return vals + def get_product_data(self, line): + line_number = line[4:10].split() + line_number = line_number.pop() if line_number else None - def get_config_item_gross_price_vals(self, vals): - ProductProduct = self.env['product.product'] + ean13_code = line[10:23].split().pop() + ean13_code = ean13_code if ean13_code else None - for line in vals['pre_order_line_ids']: - box_qty = line[2]['box_qty']['original'] + item_size_description = line[24:34].split() + item_size_description = item_size_description.pop() if item_size_description else None - if box_qty and box_qty > 0: - line[2]['gross_price_unit']['modified'] /= box_qty - else: - product_id = line[2]['product_id'] - product = ProductProduct.browse(product_id) + item_colour_description = line[34:44].split() + item_colour_description = item_colour_description.pop() if item_colour_description else None - line[2]['errors'].append(_('~ Box Quantity not set for product {}.\n').format(product.name)) + item_short_description = line[44:79] + item_short_description = item_short_description if item_short_description else None - return vals + item_description1 = line[79:114].split() + item_description1 = item_description1.pop() if item_description1 else None - def get_config_maxipiece_vals(self, vals): - ProductProduct = self.env['product.product'] + item_description2 = line[114:149].split() + item_description2 = item_description2.pop() if item_description2 else None - for line in vals['pre_order_line_ids']: - box_kg = line[2]['box_kg'] - ordered_qty = line[2]['ordered_qty']['original'] + item_description3 = line[149:184].split() + item_description3 = item_description3.pop() if item_description3 else None - if box_kg and box_kg > 0 and ordered_qty: - is_maxipiece = line[2]['is_maxipiece'] - if is_maxipiece: - line[2]['box_qty']['modified'] = ordered_qty / box_kg - else: - product_id = line[2]['product_id'] - product = ProductProduct.browse(product_id) + item_description4 = line[184:219].split() + item_description4 = item_description4.pop() if item_description4 else None - line[2]['errors'].append(_('~ Box Kilograms not set for product {}.\n').format(product.name)) + supplier_product_internal_code = line[219:233].split() + supplier_product_internal_code = ( + supplier_product_internal_code.pop() if supplier_product_internal_code else None + ) - return vals + uom = line[233:236].split() + uom = uom.pop() if uom else None - def get_config_ordered_qty_vals(self, vals): - for line in vals['pre_order_line_ids']: - ordered_qty = line[2]['ordered_qty']['original'] - product_uom = line[2]['product_uom'] + use_unit = line[236:240].split() + use_unit = use_unit.pop() if use_unit else None - if ordered_qty and product_uom != self.env.ref('uom.product_uom_kgm').id: - line[2]['box_qty']['modified'] = ordered_qty - else: - box_kg = line[2]['box_kg'] - if box_kg > 0: - line[2]['box_qty']['modified'] = ordered_qty / box_kg + box_qty = line[240:247].split() + box_qty = float(box_qty.pop()) if box_qty else 0.0 - return vals + box_qty_ean_set = line[247:254].split() + box_qty_ean_set = float(box_qty_ean_set.pop()) if box_qty_ean_set else 0.0 - def get_config_ordered_qty_for_kg_vals(self, vals): - for line in vals['pre_order_line_ids']: - ordered_qty = line[2]['ordered_qty']['original'] - product_uom = line[2]['product_uom'] + box_qty_pallets = line[254:261].split() + box_qty_pallets = float(box_qty_pallets.pop()) if box_qty_pallets else 0.0 - if ordered_qty and product_uom == self.env.ref('uom.product_uom_kgm').id: - box_kg = line[2]['box_kg'] - if box_kg > 0: - line[2]['box_qty']['modified'] = ordered_qty / box_kg + ordered_qty = line[261:272].split() + ordered_qty = float(ordered_qty.pop()) if ordered_qty else 0.0 - return vals + units_per_box = line[272:279].split() + units_per_box = float(units_per_box.pop()) if units_per_box else 0.0 - def get_config_product_uom_vals(self, vals): - for line in vals['pre_order_line_ids']: - box_qty = line[2]['box_qty']['original'] - ordered_qty = line[2]['ordered_qty']['original'] - product_uom = line[2]['product_uom'] - content = line[2]['content'] + net_price_unit = line[279:294].split() + net_price_unit = float(net_price_unit.pop()) if net_price_unit else 0.0 - if box_qty and ordered_qty and product_uom and content and content > 0: - if product_uom == self.env.ref('uom.product_uom_unit').id: - line[2]['box_qty']['modified'] = int(box_qty / content) - elif product_uom == self.env.ref('uom.product_uom_kgm').id: - box_kg = line[2]['box_kg'] - if box_kg > 0: - _box_qty = int(ordered_qty / box_kg) + gross_price_unit = line[294:309].split() + gross_price_unit = float(gross_price_unit.pop()) if gross_price_unit else 0.0 - if _box_qty < 1 and _box_qty > 0: - line[2]['box_qty']['modified'] = 1 - elif box_qty >= 1: - line[2]['box_qty']['modified'] = _box_qty + line_total_amount = line[309:324].split() + line_total_amount = float(line_total_amount.pop()) if line_total_amount else 0.0 - return vals + additional_line_expense = line[324:339].split() + additional_line_expense = float(additional_line_expense.pop()) if additional_line_expense else 0.0 - def get_config_units_as_boxes_vals(self, vals): - for line in vals['pre_order_line_ids']: - product_uom = line[2]['product_uom'] - units_per_box = line[2]['units_per_box']['original'] + vat_tax = line[339:347].split() + vat_tax = float(vat_tax.pop()) if vat_tax else 0.0 - if product_uom and product_uom == self.env.ref('uom.product_uom_kgm').id and units_per_box: - line[2]['box_qty']['modified'] = units_per_box + internal_tax = line[347:355].split() + internal_tax = float(internal_tax.pop()) if internal_tax else 0.0 - return vals + discount1 = line[355:365].split() + discount1 = float(discount1.pop()) if discount1 else 0.0 - # This method will be used to add last-minute changes to the values of the file - def _update_file_vals(self, vals): - PreSaleOrder = self.env['pre.sale.order'] - ProductProduct = self.env['product.product'] + discount2 = line[365:375].split() + discount2 = float(discount2.pop()) if discount2 else 0.0 - for line in vals['pre_order_line_ids']: - product_id = line[2]['product_id'] - product = ProductProduct.browse(product_id) + discount3 = line[375:385].split() + discount3 = float(discount3.pop()) if discount3 else 0.0 - box_qty = line[2]['box_qty']['modified'] - if box_qty < 1 and box_qty > 0: - box_qty = 1 + discount4 = line[385:395].split() + discount4 = float(discount4.pop()) if discount4 else 0.0 - line[2]['product_uom_qty'] = PreSaleOrder._calc_ordered_qty(product, box_qty) - line[2]['box_qty']['modified'] = box_qty + discount5 = line[395:405].split() + discount5 = float(discount5.pop()) if discount5 else 0.0 - return vals + discount6 = line[405:415].split() + discount6 = float(discount6.pop()) if discount6 else 0.0 - # Rebuild the values of the file to be imported so as to adapt product values to the - # pre-sale order line values format --removes original dict format from method. - def _refactor_product_vals_before_import(self, vals): - for line in vals['pre_order_line_ids']: - for k, v in line[2].items(): - if isinstance(v, dict) and 'modified' in v: - line[2][k] = v['modified'] + comeback = line[415:425].split() + comeback = float(comeback.pop()) if comeback else 0.0 - return vals + return { + 'additional_line_expense': { + 'original': additional_line_expense, + '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}, + 'comeback': {'original': comeback, 'modified': comeback}, + 'discount1': {'original': discount1, 'modified': discount1}, + 'discount2': {'original': discount2, 'modified': discount2}, + 'discount3': {'original': discount3, 'modified': discount3}, + 'discount4': {'original': discount4, 'modified': discount4}, + 'discount5': {'original': discount5, 'modified': discount5}, + '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}, + '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_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}, + '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}, + 'uom': {'original': uom, 'modified': uom}, + 'use_unit': {'original': use_unit, 'modified': use_unit}, + 'vat_tax': {'original': vat_tax, 'modified': vat_tax}, + } - def do_import(self, files=None, manual_import=False): # noqa - CustomerPurchaseOrderConfig = self.env['customer.purchase.order.config'] + def get_product_lines(self, lines, partner, product_type, pricelist, branch): PreSaleOrder = self.env['pre.sale.order'] - msg = '' - imported = [] - not_imported = [] - - paths = self._get_paths() - import_path = paths['import'] - processed_path = paths['processed'] - rejected_path = paths['rejected'] - - if not manual_import: - _logger.info('Importing PO files from ' + import_path) + errors = [] + po_lines = [] + summary_code = 0 + summary_qty = 0 + po_lines = [] - ftp = connect_ftp() + _last_line_is_empty = len(lines[-1]) == 0 + product_lines = lines[1 : -1 if _last_line_is_empty else None] + for line in product_lines: + po_line = () - ftp.cwd(import_path) + if len(line) > 0: + product_data = self.get_product_data(line) - files = self.standarize_file_names(import_path, ftp) - for _file in files: - if not _file: - continue + ean13_code = product_data['ean13_code']['original'] + ordered_qty = product_data['ordered_qty']['original'] + product_description = product_data['item_short_description']['original'] - po_vals = {} + config = self.get_config_internal_code(partner.id if partner else None) - file = BytesIO() - ftp.retrbinary('RETR ' + _file, file.write) - msg_list = [] + _product = self.get_product( + config, ean13_code, is_ean_code=True, partner=partner, description=product_description + ) + if _product.get('errors'): + errors.extend(_product.get('errors')) + continue - with self.env.cr.savepoint(): - file_txt = file.getvalue().decode('utf-8') + product = _product.get('product') + if product: + if branch and ean13_code: + self.save_branch_ean_code(branch, ean13_code, product) - lines = self.get_lines(file_txt) + if product_type: + summary_code += int(product.default_code) + summary_qty += ordered_qty - file.close() + product_data.update( + { + 'box_kg': product.box_kgs, + 'content': product.content, + 'is_maxipiece': product.is_maxipiece, + 'name': product.name, + 'product_id': product.id, + 'product_uom': product.uom_id.id, + 'pricelist_price': ( + PreSaleOrder._get_pricelist_price(product, pricelist) + if pricelist + else 0.0 + ), + 'tax_id': [(6, 0, product.taxes_id.ids)], + } + ) - file_vals = self.get_full_file_data(lines) + po_line = (0, 0, product_data) - if isinstance(file_vals, list): - rejected_file = '' + po_lines.append(po_line) - partner_name = file_vals.pop(-1).replace(' ', '_') - if partner_name not in _file: - rejected_file = _file.replace('ORD_', 'ORD_%s_' % partner_name) + return { + 'po_lines': po_lines, + 'summary_code': summary_code, + 'summary_qty': summary_qty, + 'errors': errors, + } - msg_list.insert(0, _('FILE: %s\n') % rejected_file) - msg_list.extend(file_vals) - not_imported.append(''.join(msg_list) + '\n') + def get_product_type(self, product_codes, partner): + ProductType = self.env['product.type'] - if rejected_file in ftp.nlst(rejected_path): - ftp.delete(rejected_path + rejected_file) + errors = [] + product_types = [] + product_type = None - ftp.rename(import_path + _file, rejected_path + rejected_file) + if product_codes and partner: + for code in product_codes: + config = self.get_config_internal_code(partner.id if partner else None) + _product = self.get_product(config, code, partner=partner) + product = _product.get('product') + if not product: continue else: - partner = file_vals['partner'] - config = self.get_config(partner) - config_type = 'basic' + if product.product_type_id: + product_types.append(product.product_type_id.id) + else: + continue - if config: - for c in config: - if c.config_type: - if c.config_type != 'internal_code': - config_type = c.config_type + product_types = set(product_types) + if product_types: + if len(product_types) > 1: + errors.append(_('~ Some products have different product types.\n')) + else: + product_type = ProductType.browse(list(product_types)[0]) - try: - po_vals.update( - getattr(self, 'get_config_{}_vals'.format(config_type))(file_vals) - ) - except AttributeError: - _type = CustomerPurchaseOrderConfig._fields['config_type'].selection - config_type_name = dict(_type).get(config_type) + 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" + ) + ) - msg_list.insert(0, _('FILE: {}\n').format(_file)) - msg_list.append( - _( - '~ 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 + return {'product_type': product_type, 'errors': errors} - imported.append(_file) + def get_time_offset(self): + try: + user_tz = pytz.timezone(self.env.context.get('tz') or self.env.user.tz) + time_offset = str(user_tz.localize(datetime.now()))[-6:-3] + offset = int(time_offset) * (-1) + except Exception: + offset = 3 - po_vals.update(self._update_file_vals(file_vals)) - po_vals = self._refactor_product_vals_before_import(po_vals) - PreSaleOrder.create(po_vals) + return offset - ftp.rename(import_path + _file, processed_path + _file) + def is_valid_date(self, date): + if isinstance(date, str): + if len(date) == 8: + date += '0000' - if len(imported) > 0: - msg += _('The following files have been imported successfully:\n') + ', '.join(sorted(imported)) + try: + _date = datetime.strptime(date, '%Y%m%d%H%M') + return _date + except ValueError: + return False - if len(not_imported) > 0: - not_imported_msg = ''.join(sorted(not_imported)) + return False - self._create_error_file(rejected_path, not_imported_msg, ftp) + def purchase_order_exists_for_branch(self, po_num, branch): + PreSaleOrder = self.env['pre.sale.order'] - msg += ( - _('The following files have errors that need to be corrected before importing them:\n\n') - ) + not_imported_msg + purchase_order = PreSaleOrder.search_count( + [('name', '=', 'ORD' + po_num), ('partner_shipping_id', '=', branch.id)] + ) - self.env.cr.commit() + return purchase_order - _logger.info(_('Scheduled customer purchase order import finished!')) + def save_branch_ean_code(self, branch, ean13_code, product): + BranchEANCode = self.env['branch.ean.code'] - if manual_import: - raise UserError(msg) + branch_ean_code = BranchEANCode.search( + [('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}) + + def start_date_is_earlier(self, start_date, end_date): + return start_date <= end_date + + def vat_matches(self, partner, branch): + return partner.vat == branch.vat - def _create_error_file(self, path, msg, ftp): + def _create_error_file(self, ftp, path, msg): IrAttachment = self.env['ir.attachment'] + path = path.replace('tmp\\', '') + datas = bytes(msg, 'utf-8') today = str(datetime.today().date()) @@ -1076,6 +1064,8 @@ class CustomerPurchaseOrderImporter(models.Model): file_name = '_not_imported_{}.txt'.format(today.replace('-', '')) + ftp.cwd(path) + file = BytesIO(datas) ftp.storbinary('STOR ' + file_name, file) @@ -1086,7 +1076,44 @@ class CustomerPurchaseOrderImporter(models.Model): file.close() - def standarize_file_names(self, path, ftp): + def _create_tmp_files(self, ftp, files, path='/tmp/'): + if files: + if 'tmp' not in ftp.nlst(): + ftp.mkd('tmp') + + ftp.cwd('tmp') + + for file in files: + file_name = file.name + if file_name in ftp.nlst(): + ftp.delete(file_name) + + tmp_file = BytesIO(b64decode(file.datas)) + + ftp.storbinary('STOR %s' % file_name, tmp_file) + + tmp_file.close() + + def _get_paths(self): + IrConfigParameter = self.env['ir.config_parameter'] + + import_path = IrConfigParameter.sudo().get_param('po_importer_path') + processed_path = IrConfigParameter.sudo().get_param('po_importer_processed_path') + rejected_path = import_path + 'RECHAZADOS\\' + + return {'import': import_path, 'processed': processed_path, 'rejected': rejected_path} + + # Rebuild the values of the file to be imported so as to adapt product values to the + # pre-sale order line values format --removes original dict format from method. + def _refactor_product_vals_before_import(self, vals): + for line in vals['pre_order_line_ids']: + for k, v in line[2].items(): + if isinstance(v, dict) and 'modified' in v: + line[2][k] = v['modified'] + + return vals + + def _standarize_file_names(self, path, ftp): files = [] old_file_names = ftp.nlst() @@ -1101,12 +1128,20 @@ class CustomerPurchaseOrderImporter(models.Model): return files - def _get_paths(self): - IrConfigParameter = self.env['ir.config_parameter'] - import_path = IrConfigParameter.sudo().get_param('po_importer_path') + # This method will be used to add last-minute changes to the values of the file + def _update_file_vals(self, vals): + PreSaleOrder = self.env['pre.sale.order'] + ProductProduct = self.env['product.product'] - processed_path = IrConfigParameter.sudo().get_param('po_importer_processed_path') + for line in vals['pre_order_line_ids']: + product_id = line[2]['product_id'] + product = ProductProduct.browse(product_id) - rejected_path = import_path + 'RECHAZADOS\\' + box_qty = line[2]['box_qty']['modified'] + if box_qty < 1 and box_qty > 0: + box_qty = 1 - return {'import': import_path, 'processed': processed_path, 'rejected': rejected_path} + line[2]['product_uom_qty'] = PreSaleOrder._calc_ordered_qty(product, box_qty) + line[2]['box_qty']['modified'] = box_qty + + return vals diff --git a/models/pre_sale_order.py b/models/pre_sale_order.py index 23cde16..f5fe660 100644 --- a/models/pre_sale_order.py +++ b/models/pre_sale_order.py @@ -21,8 +21,7 @@ class PreSaleOrder(models.Model): account = fields.Char(string='Account', compute='_compute_account', readonly=True, store=True) cooling_type = fields.Selection( - [('cold', 'Cold'), ('frozen', 'Frozen'), ('not_cold', 'Not Cold')], - string='Cooling Type', + [('cold', 'Cold'), ('frozen', 'Frozen'), ('not_cold', 'Not Cold')], string='Cooling Type' ) date_order = fields.Datetime(string='Order Date', readonly=True) date_order_exit = fields.Datetime(string='Exit Order Date') @@ -38,11 +37,7 @@ class PreSaleOrder(models.Model): name = fields.Char(string='Pre Sale Order Number') partner = fields.Many2one('res.partner', string='Company', readonly=True) partner_invoice_id = fields.Many2one('res.partner', string='Invoice Address', readonly=True) - partner_shipping_id = fields.Many2one( - 'res.partner', - string='Delivery Address', - readonly=True, - ) + partner_shipping_id = fields.Many2one('res.partner', string='Delivery Address', readonly=True) pre_order_line_ids = fields.One2many( comodel_name='pre.sale.order.line', inverse_name='pre_orders_id', @@ -71,6 +66,15 @@ class PreSaleOrder(models.Model): readonly=True, ) + def unlink(self): + for rec in self: + if rec.state == 'created' and ( + rec.sale_order_count > 0 or rec.pre_order_line_ids.mapped('sale_id') + ): + raise ValidationError(_("You can't delete a purchase order with associated sale orders.")) + + return super(PreSaleOrder, self).unlink() + @api.depends('zone_account') def _compute_account(self): for rec in self: @@ -184,7 +188,7 @@ class PreSaleOrder(models.Model): ) if len(products) > 0: - msg = _('The following products don\'t have a price set:\n') + ''.join(sorted(products)) + msg = _("The following products don't have a price set:\n") + ''.join(sorted(products)) raise ValidationError(msg) @@ -204,10 +208,10 @@ class PreSaleOrder(models.Model): def approve_pre_order(self): if len([x for x in self.pre_order_line_ids.mapped('selected_item') if x]) > 15: - raise ValidationError(_('You can\'t create a sale order with more than 15 products')) + raise ValidationError(_("You can't create a sale order with more than 15 products")) if any([line.line_state == 'draft' for line in self.pre_order_line_ids]): - raise ValidationError(_('Lines can\'t be in draft state')) + raise ValidationError(_("Lines can't be in draft state")) self._check_all_prices_are_set() @@ -236,14 +240,11 @@ class PreSaleOrder(models.Model): for rec in self or pso: lines = rec.pre_order_line_ids.filtered( - lambda x: x.line_state_done != 'rejected' - and not x.sale_id + lambda x: x.line_state_done != 'rejected' and not x.sale_id ) selected_lines = lines.filtered(lambda x: x.selected_item) if not selected_lines: - raise ValidationError( - _('You must select at least one line to create a sale ' 'order.') - ) + raise ValidationError(_('You must select at least one line to create a sale ' 'order.')) partner_pricelist_line = rec.partner_shipping_id.zone_accounts_line_ids.filtered( lambda x: x.rp_za_product_type_id.id == rec.product_type.id, ) @@ -254,9 +255,7 @@ class PreSaleOrder(models.Model): if not len(partner_pricelist_line): raise ValidationError( - _( - 'Can\'t create sale order from purchase order %s because partner has no pricelist set.' - ) + _("Can't create sale order from purchase order %s because partner has no pricelist set.") % rec.name ) @@ -434,9 +433,11 @@ class PreSaleOrderLine(models.Model): def _get_invoice_data(self): if self.sale_id.invoice_ids: - invoice_lines = self.sale_id.invoice_ids.sorted( - key=lambda x: x.date_invoice, reverse=True - ).mapped('invoice_line_ids').filtered(lambda x: x.product_id == self.product_id) + invoice_lines = ( + self.sale_id.invoice_ids.sorted(key=lambda x: x.date_invoice, reverse=True) + .mapped('invoice_line_ids') + .filtered(lambda x: x.product_id == self.product_id) + ) if invoice_lines: date = invoice_lines[0].invoice_id.date_invoice invoiced_kg = sum(invoice_lines.mapped('quantity')) @@ -478,7 +479,8 @@ class PreSaleOrderLine(models.Model): 'pricelist_price': pricelist_price, 'product_uom': self.product_id.uom_id.id, 'tax_id': self.product_id.taxes_id.ids, - } + invoice_data + } + + invoice_data ) @api.onchange('line_state') diff --git a/wizard/customer_purchase_order_importer_wizard.py b/wizard/customer_purchase_order_importer_wizard.py index 5f41b1b..833803b 100644 --- a/wizard/customer_purchase_order_importer_wizard.py +++ b/wizard/customer_purchase_order_importer_wizard.py @@ -1,9 +1,13 @@ +# -- coding: utf-8 -- +############################################################################## +# +# Copyright (c) 2022-2023 Eynes SRL (Eynes - Ingenieria del software) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# +############################################################################## + 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 odoo import fields, models _logger = logging.getLogger(__name__) @@ -26,34 +30,7 @@ class CustomerPurchaseOrderImporterWizard(models.TransientModel): return no_dups - def create_tmp_and_move_file(self, file, new_name, path='/tmp/'): - if file: - ftp = connect_ftp() - ftp.cwd(path) - - tmp_file = BytesIO(b64decode(file.datas)) - - ftp.storbinary('STOR %s' % file.name, tmp_file) - - tmp_file.seek(0) - tmp_file.close() - def do_import(self): CustomerPurchaseOrderImporter = self.env['customer.purchase.order.importer'] - - file_list = [] - - import_path = CustomerPurchaseOrderImporter._get_paths()['import'] - files = self.prevent_dups(self.files) - for file in files: - try: - 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) - - CustomerPurchaseOrderImporter.do_import(files=file_list, manual_import=True) + CustomerPurchaseOrderImporter.do_import(files=files, manual_import=True) -- GitLab