def import_file(self, file_text, only_check=False): self.success = True self.errors = [] xml = open_xml(file_text, _type='bur', print_error=self.print_errors) if nspath(xml, '//bur:BUR_List'): i = 0 for x in nspath(xml, '//bur:BUR_List/bur:BUR'): i += 1 # First, we try to check the records try: text = xml_to_string(x) self.import_file(text, only_check=True) except Warning as w: print 'error>>>' * 5 print text print '<<<error' * 5 raise Warning('Error on sub BUR file number {0}'.format(i), format_exception(w)) for x in nspath(xml, '//bur:BUR_List/bur:BUR'): self.import_file(xml_to_string(x), only_check=False) return True validate_xml('bur', xml, print_error=False) imports = [] product_obj = self.pool.get("product.product") lot_obj = self.pool.get('stock.production.lot') connection_obj = self.pool.get('stock.connect') mapping_bur_transactiontypes_obj = self.pool.get( 'mapping_bur_transactiontypes') location_obj = self.pool.get('stock.location') # Gets the warehouse of the YellowCube. warehouse = connection_obj.browse( self.cr, self.uid, self.connection_id, context=self.context).warehouse_ids[0] # Header fields (under <GoodsReceiptHeader>) header = nspath(xml, "//bur:GoodsMovementsHeader")[0] # <BookingVoucherID> and <BookingVoucherYear>. # TODO: Check or save the value booking_voucher_id = nspath(header, "bur:BookingVoucherID")[0].text # TODO: Check or save the value booking_voucher_year = nspath(header, "bur:BookingVoucherYear")[0].text depositor_no = nspath(header, "bur:DepositorNo")[0].text self._check(warehouse, depositor_no == self.get_param('depositor_no'), _('Invalid DepositorNo')) for article in nspath(xml, "//bur:BookingList/bur:BookingDetail"): partial_success = True element = {} # YCArticleNo element['yc_YCArticleNo'] = nspath(article, "bur:YCArticleNo")[0].text search_domain = [("yc_YCArticleNo", "=", element['yc_YCArticleNo']) ] # ArticleNo article_no = nspath(article, "bur:ArticleNo") if len(article_no) > 0: # ArticleNo: Only set on dictionary, when needed for search (this avoids overwrite) element['default_code'] = article_no[0].text search_domain = [("default_code", "=", element['default_code']) ] ids = product_obj.search(self.cr, self.uid, search_domain, context=self.context) if len(ids) > 0: element['id'] = ids[0] else: element['id'] = -1 imports.append(element) if not self._check( warehouse, len(ids) == 1, _('Invalid search domain {0}').format(search_domain)): continue product = product_obj.browse(self.cr, self.uid, ids, context=self.context)[0] # EAN ean13 = nspath(article, 'bur:EAN') if ean13: element['ean13'] = ean13[0].text if product.ean13: partial_success &= self._check( product, product.ean13 == element['ean13'], _('Product EAN13 {0} differs from XML EAN {1}').format( product.ean13, element['ean13'])) # BVPosNo # TODO: Check or save the value bv_pos_no = nspath(article, 'bur:BVPosNo')[0].text # Plant plant = nspath(article, 'bur:Plant')[0].text partial_success &= self._check(product, plant == self.get_param('plant_id'), _('Mismatching PlantID')) # MovePlant # TODO: Check or save move_plant = nspath(article, 'bur:MovePlant') # StorageLocation source_location = nspath(article, 'bur:StorageLocation')[0].text # MoveStorageLocation destination_location = nspath(article, "bur:MoveStorageLocation") if destination_location: destination_location = destination_location[0].text else: destination_location = False # TransactionType transaction_type = nspath( article, "bur:TransactionType" )[0].text # Mandatory field, so won't fail. # We now determine the origin and destination locations based on the fields # StorageLocation, MoveStorageLocation, and TransactionType. # Have a look at task with ID=3725 for the algorithm which is copied below: # IF StorageLocation is recognized as a valid location in Odoo # AND MoveSorageLocation is recognized as a valid location in Odoo # THEN use those if location_obj.search(self.cr, self.uid, [('name', '=', source_location)], context=self.context, count=True) and \ destination_location and \ location_obj.search(self.cr, self.uid, [('name', '=', destination_location)], context=self.context, count=True): element['location'] = source_location element['destination'] = destination_location else: # ELSE look up the TransactionType given in the BUR in the configured TransactionType list, # and if found and the locations are valid, use them. is_mapped, mapped_origin_location, mapped_destination_location = \ mapping_bur_transactiontypes_obj.get_mapping(self.cr, self.uid, [], transaction_type, context=self.context) if is_mapped and mapped_origin_location and mapped_destination_location: element['location'] = mapped_origin_location.name element['destination'] = mapped_destination_location.name else: # ELSE create an issue and stop processing the BUR. after resolving the TransactionType mapping, the import can be restarted... self.success = False # We know now that we had no success. error_message = _( 'Error when importing BUR: StorageLocation and/or MoveStorageLocation were not defined or incorrect, AND ' 'no correct mapping was defined for TransactionType={0}' ).format(transaction_type) self.post_issue(warehouse, error_message) # YCLot # TODO: check yc_lot = nspath(article, 'bur:YCLot') if yc_lot: element['yellowcube_lot'] = yc_lot[0].text # Lot lot = nspath(article, 'bur:Lot') if len(lot) > 0: element['lot'] = lot[0].text lot_id = lot_obj.search(self.cr, self.uid, [('product_id', '=', product.id), ('name', '=', element['lot'])], context=self.context) if not self._check( product, len(lot_id) <= 1, _('Impossible to find a unique lot {0}'.format( element['lot']))): continue if not lot_id: values = {'product_id': product.id, 'name': element['lot']} production_date = nspath(article, "bur:ProductionDate") if production_date: values['date'] = self.str_date_to_postgres( production_date[0].text) lot_use_date = nspath(article, "bur:BestBeforeDate") if lot_use_date: values['use_date'] = self.str_date_to_postgres( lot_use_date[0].text) if only_check: lot_id = None else: lot_id = [ lot_obj.create(self.cr, self.uid, values, context=self.context) ] if lot_id is None and only_check: lot = None else: lot = lot_obj.browse(self.cr, self.uid, lot_id, context=self.context)[0] # StockType element['stock_type'] = nspath(article, 'bur:StockType')[0].text # Quantity element['qty_available'] = nspath(article, "bur:QuantityUOM")[0].text # QuantityUOM qty_uom = nspath(article, "bur:QuantityUOM")[0].attrib['QuantityISO'] qty_uom_ids = self.pool.get('product.uom').search( self.cr, self.uid, [('uom_iso', '=', qty_uom)], context=self.context) partial_success &= self._check( product, qty_uom_ids, _('There is not any Unit of Measure with ISO code being {0}.'. format(qty_uom))) if partial_success: element['qty_uom_id'] = qty_uom_ids[0] write_on_lot = {} # BestBeforDate lot_use_date = nspath(article, "bur:BestBeforeDate") element['lot_use_date'] = False if len(lot_use_date) > 0: lot_use_date = lot_use_date[0].text element['lot_use_date'] = self.str_date_to_postgres( lot_use_date) if lot is None: self._check(product, only_check, _('The lot may not exists in a two step file')) else: if not lot.use_date: write_on_lot['use_date'] = element['lot_use_date'] else: partial_success &= self._check( product, self.keep_only_date( lot.use_date) == element['lot_use_date'], _('Mismatch with lot best before date')) # ProductionDate production_date = nspath(article, "bur:ProductionDate") element['date'] = False if production_date: lot_date = lot_use_date[0].text element['lot_date'] = self.str_date_to_postgres(lot_date) if not lot.date: write_on_lot['date'] = element['lot_date'] else: partial_success &= self._check( product, self.keep_only_date(lot.date) == element['lot_date'], _('Mismatch with lot fabrication date')) if write_on_lot and partial_success: lot.write(write_on_lot) element['name'] = "BUR-{0}-{1}".format( nspath(xml, "//bur:ControlReference/bur:Timestamp")[0].text, nspath(article, "bur:BVPosNo")[0].text) # print imports if not self.context.get('force_import', False): bad_imports = [x['yc_YCArticleNo'] for x in imports if x['id'] < 0] if len(bad_imports) > 0: raise Exception( "Invalid XML Elements: {0}".format(bad_imports)) if not self.success: raise Warning( 'There where errors on the import process. See import log thread.', self.errors) if only_check: # Everything was OK, and it could be imported in a second step. return True stock_move_pool = self.pool.get("stock.move") for article in imports: _id = article['id'] self.mark_record(_id, 'product.product') _lot = False if 'lot' in article: _lot = self.pool.get('stock.production.lot').search( self.cr, self.uid, [('name', '=', article['lot']), ('product_id', '=', _id)], context=self.context) if len(_lot) > 0: _lot = _lot[0] else: _lot = self.pool.get('stock.production.lot').create( self.cr, self.uid, { 'name': article['lot'], 'product_id': _id, 'date': element.get('lot_date', None), 'use_date': element.get('lot_use_date', None) }, context=self.context) def loc(is_input, element, warehouse): key = 'location' if is_input else 'destination' if (not input) and element['stock_type'] not in ['', '0', 'F']: return warehouse.lot_blocked_id.id if element[key] == 'YROD': return warehouse.lot_input_id.id if element[key] == 'YAFS': return warehouse.lot_stock_id.id return self.pool['ir.model.data'].get_object_reference( self.cr, self.uid, 'stock', 'location_inventory')[1] stock_move_id = stock_move_pool.create( self.cr, self.uid, { 'name': article['name'], 'product_id': _id, 'location_id': loc(True, article, warehouse), 'location_dest_id': loc(False, article, warehouse), 'product_uom_qty' if V8 else 'product_qty': article['qty_available'], 'product_uom': article['qty_uom_id'], 'state': 'done', 'restrict_lot_id' if V8 else 'prodlot_id': _lot, 'origin': 'YellowCube', 'type': 'internal', 'yc_booking_voucher_id': booking_voucher_id, 'yc_booking_voucher_year': booking_voucher_year, }, context=self.context) self.mark_record(_id, 'product.product') return True
def import_file(self, file_text): self.success = True self.errors_list = [] # Stores the errors found. product_obj = self.pool.get("product.product") lot_obj = self.pool.get('stock.production.lot') lot_model = 'stock.quant' if version_info[ 0] > 7 else 'stock.report.prodlots' stock_report_prodlots_obj = self.pool[lot_model] xml = open_xml(file_text, _type='bar', print_error=self.print_errors) imports = [] imported_lots = [] stock_obj = self.pool.get("stock.change.product.qty") warehouse = self.pool.get('stock.connect').browse( self.cr, self.uid, self.connection_id, context=self.context).warehouse_ids[0] location = warehouse loc_stock = location.lot_stock_id.id loc_blocked = location.lot_blocked_id.id article_list = nspath(xml, "//bar:ArticleList/bar:Article") self._check(None, article_list, _('This BAR has no products in it.')) if not article_list: logger.debug("This is an empty BAR file") self.post_issue(location, _('This BAR has no products in it.')) for article in article_list: partial_success = True element = {'location_id': loc_stock} quantity_uom = float(nspath(article, "bar:QuantityUOM")[0].text) element['yc_YCArticleNo'] = nspath(article, "bar:YCArticleNo")[0].text article_no = nspath(article, "bar:ArticleNo") ean = nspath(article, "bar:EAN") # We attempt to identify the product. # Priority: YCArticleNo, ArticleNo(default_code), EAN(ean13). search_domain = [] search_domain.append([("yc_YCArticleNo", "=", element['yc_YCArticleNo'])]) if article_no: element['default_code'] = article_no[0].text search_domain.append([("default_code", "=", element['default_code'])]) else: element['default_code'] = None if ean: element['ean13'] = ean[0].text search_domain.append([("ean13", "=", element['ean13'])]) else: element['ean13'] = None # Attempts to identify the product. for domain in search_domain: product_id = product_obj.search(self.cr, self.uid, domain, context=self.context) if product_id: break if quantity_uom == 0 and not product_id: self.post_issue( location, _('Product for domain {0} does not exists, and a zero stock update was received' ).format(search_domain)) continue # If we don't have success identifying the product, we skip this line since it's impossible to continue. partial_success &= self._check( location, product_id, _('There is not article for domain {0}.').format( search_domain)) if not partial_success: continue partial_success &= self._check( location, len(product_id) == 1, _('There is more than one article for domain {0}.').format( search_domain)) if not partial_success: continue product_id = product_id[0] product = product_obj.browse(self.cr, self.uid, product_id, self.context) element['id'] = product_id for k in ('ean13', 'default_code', 'yc_YCArticleNo'): if product[k] and element[k]: self._check( product, element[k] == product[k], _('Mismatching product reference {0}').format(k)) # Checks/Saves Plant plant = nspath(article, "bar:Plant")[0].text current_plant = self.get_param('plant_id', required=True) self._check( product, current_plant, _("Configuration parameter YC PlantID is not defined for product {0}" ).format(product.name)) self._check( product, current_plant and current_plant == plant, _('Plant does not match with the value of the configuration parameter YC PlantID for product {0}' ).format(product.name)) # Checks <StorageLocation> and <StockType> # Notes: Check together with StorageLocation against location_id on stock.move - alarm if wrong. # If free type (' ', '0', 'F', 'U') use the StorageLocation, otherwise location YBLK. storage_location = nspath(article, "bar:StorageLocation")[0].text stock_type = nspath(article, "bar:StockType")[0].text # If there exists the tag <StockType>, then we follow the rules. location_to_use_ids = False if stock_type in ('X', 'S', '2', '3', '0', 'F', 'U', ' '): if stock_type in ('0', 'F', 'U', ' '): location_to_use = storage_location else: location_to_use = 'YBLK' location_to_use_ids = self.pool.get('stock.location').search( self.cr, self.uid, [('name', '=', location_to_use)], context=self.context) self._check( product, len(location_to_use_ids) == 1, _("Location '{0}' was not found in the system, or was found multiple times." ).format(location_to_use)) else: self._check( product, False, _("StockType had value '{0}', which is not allowed for product {1}" ).format(stock_type, product.name)) # Determines the lot: Lot has precedence over YCLot. lot_id = None lot_search_domain = None lot_to_use = False yc_lot = nspath(article, "bar:YCLot") if yc_lot: lot_to_use = yc_lot[0].text lot_search_domain = [('name', '=', lot_to_use), ('product_id', '=', product_id)] lot = nspath(article, "bar:Lot") if lot: lot_to_use = lot[0].text lot_search_domain = [('name', '=', lot_to_use), ('product_id', '=', product_id)] if lot_search_domain: lot_id = lot_obj.search(self.cr, self.uid, lot_search_domain, context=self.context) if len(lot_id) > 0: lot_id = lot_id[0] # check(product, lot_search_domain is None or lot_id, _("Lot={0} does not exist.").format(lot_search_domain or '<no-lot-found>')) if (not lot_id) and lot_search_domain: lot_id = lot_obj.create(self.cr, self.uid, { 'name': lot_search_domain[0][2], 'product_id': product_id }, context=self.context) self._check( product, (not product.track_production) or lot_id, _("Product {0} does not have a lot and it is required so."). format(product.name)) # If we have a lot, we load it, # AND add it to the list of lots which need to update its last appearance in the BAR. if lot_id: lot = lot_obj.browse(self.cr, self.uid, lot_id, self.context) imported_lots.append(lot_id) # Dates: BestBeforeDate. best_before_date = nspath(article, "bar:BestBeforeDate") if best_before_date: best_before_date = best_before_date[0].text if self._check( product, lot_search_domain, _('BestBeforeDate defined but unknown lot to use in product {0}' ).format(product.name)): self._check( product, (not lot.use_date) or (self.keep_only_date(lot.use_date) == self.str_date_to_postgres(best_before_date)), _("Use date ({1}) of the lot {0} and tag BestBeforeDate ({2}) do not match for product {3}." ).format(lot.name, self.keep_only_date(lot.use_date), self.str_date_to_postgres(best_before_date), product.name)) if lot_id and not lot.use_date: lot_obj.write(self.cr, self.uid, lot_id, { 'use_date': self.str_date_to_postgres(best_before_date) }, self.context) # Dates: ProductionDate. production_date = nspath(article, "bar:ProductionDate") if production_date: production_date = production_date[0].text self._check( product, (not lot_id) or (not lot.production_date) or (self.keep_only_date(lot.production_date) == self.str_date_to_postgres(production_date)), _("Use date ({1}) of the lot {0} and tag ProductionDate ({2}) do not match for product {3}." ).format(lot.name, self.keep_only_date(lot.production_date), self.str_date_to_postgres(production_date), product.name)) if lot_id and not lot.date: lot_obj.write( self.cr, self.uid, lot_id, {'date': self.str_date_to_postgres(production_date)}, self.context) # Checks the QuantityUOM and QuantityISO. if stock_type in ('0', 'F', ' '): # If is free location. if location_to_use_ids: element['location_id'] = location_to_use_ids[0] if lot_id and location_to_use_ids: # We have a lot and a location: check against the quantity of the lot+location. stock_report_prodlot_id = stock_report_prodlots_obj.search( self.cr, self.uid, [('location_id', '=', location_to_use_ids[0]), ('lot_id' if version_info[0] > 7 else 'prodlot_id', '=', lot_id), ('product_id', '=', product_id)], context=self.context) partial_success &= self._check( product, stock_report_prodlot_id, _("No combination of location={0} and lot={1} was found in the system for product {2}." ).format(location_to_use, lot_to_use, product.name)) if partial_success and stock_report_prodlot_id: # Checks that the quantity is correct. stock_report_prodlot = stock_report_prodlots_obj.browse( self.cr, self.uid, stock_report_prodlot_id[0], self.context) if stock_report_prodlot.qty != quantity_uom: self.post_issue( product, _("QuantityUOM does not match for the combination of location={0} and lot={1}. Scraping will be make." ).format(location_to_use, lot_to_use), create=True, reopen=True) else: # We don't have a lot: check against the quantity of the product. if product.qty_available != quantity_uom: self.post_issue( product, _("QuantityUOM does not match the quantity indicated by the field 'product_qty' of the original product. Scraping will be make." ), create=True, reopen=True) else: # No free stock type element['location_id'] = loc_blocked element['qty_available'] = quantity_uom # Checks that the QuantityUOM matches with the product. quantity_iso = nspath(article, "bar:QuantityUOM")[0].attrib['QuantityISO'] uom_iso_list = self.pool.get('product.uom').search( self.cr, self.uid, [('uom_iso', '=', quantity_iso)], context=self.context) if uom_iso_list: element['yc_bar_uom_id'] = uom_iso_list[ 0] # Stores the last UOM sent with the BAR. uom = self.pool.get('product.uom').browse( self.cr, self.uid, uom_iso_list[0], self.context) uom_category = self.pool.get('product.uom.categ').browse( self.cr, self.uid, uom.category_id.id, context=self.context) # Checks that the UOM indicated is either the same, or of the same category, # than that of the product. same_uom_than_product = ( uom.id == product.uom_id.id) if product.uom_id else False same_category_than_products_uom = ( product.uom_id.category_id.id == uom_category.id) if product.uom_id else False self._check( product, same_uom_than_product or (not same_uom_than_product and same_category_than_products_uom), _("The UOM of the product {0} is not the same, and they are from different categories." ).format(product.name)) # self._check(product, # not(product.uom_id and (uom_name != product.uom_id.name)), # _("Unit of measure '{0}' does not match with that of the product ('{1}')").format(uom_name, product.uom_id.name)) self._check( product, product.uom_id, _("Product {0} does not have a unit of measure.").format( product.name)) else: self._check( product, False, _("Unit of measure '{0}' was not found in the system."). format(quantity_iso)) if lot_id: element['lot'] = lot_id # We accept blocked location if True or stock_type in ('0', 'F', ' '): # If it's free location... imports.append(element) if self.success: # print imports if not self.context.get('force_import', False): bad_imports = [ x['yc_YCArticleNo'] for x in imports if x['id'] < 0 ] if len(bad_imports) > 0: raise Warning( "Elements not defined in openERP: {0}".format( bad_imports)) else: logger.error( "This code is not part of YellowCube's and its use is only intended for developers: START OF THE CODE." ) new_imports = [ x for x in imports if x['id'] < 0 and x['qty_available'] != 0 ] for article in new_imports: imports.remove(article) del article['id'] article['type'] = 'product' _id = product_obj.create(self.cr, self.uid, article, context=self.context) location_id = article['location_id'] del article['location_id'] values = { 'product_id': _id, 'location_id': location_id, 'new_quantity': article['qty_available'] } if 'lot' in article: values['lot_id' if version_info[0] > 7 else 'prodlot_id'] = article['lot'] _update = stock_obj.create(self.cr, self.uid, values, context=self.context) self.context['active_id'] = _id stock_obj.browse( self.cr, self.uid, _update, context=self.context).change_product_qty() self.mark_record(_id, 'product.product') for same in [ x for x in new_imports if x['yc_YCArticleNo'] == article['yc_YCArticleNo'] ]: same['id'] = _id logger.error( "This code is not part of YellowCube's and its use is only intended for developers: FINISH OF THE CODE." ) # We return by context, the lots that were updated. self.context['imported_lots'] = imported_lots # We return by context, the products that where updated self.context['imported_products'] = imported_products = [] for article in imports: # For each article it creates a wizard and updates its stock (if the actual stock is # lower than that expect, the quantity is moved to the loss-things warehouse; # otherwise the quantity is updated). _id = article['id'] imported_products.append(_id) if _id <= 0: continue del article['id'] if 'name' in article: del article[ 'name'] # OpenERP is the master, so the name <ArticleDescription> is not overwritten. del article[ 'default_code'] # OpenERP is the master, so the default code <ArticleNo> is not overwritten. location_id = article['location_id'] del article['location_id'] values = { 'product_id': _id, 'location_id': location_id, 'new_quantity': article['qty_available'] } if 'lot' in article: self.mark_record(article['lot'], 'stock.production.lot') values['lot_id' if version_info[0] > 7 else 'prodlot_id'] = article['lot'] del article['lot'] product_obj.write(self.cr, self.uid, _id, article, context=self.context) logger.debug("Updating stock {0}".format(values)) _update = stock_obj.create(self.cr, self.uid, values, context=self.context) self.context['active_id'] = _id stock_obj.browse(self.cr, self.uid, _update, context=self.context).change_product_qty() self.mark_record(_id, 'product.product') if 'active_ids' in self.context: del self.context['active_ids'] if not self.success: raise Warning( _('Some errors where found when processing BAR file:\n -{0}'). format('\n -'.join(self.errors_list))) return self.success
def import_file(self, file_text): logger.debug("Processing WBA file") self.success = True self.errors = [] # Caches the pools. product_obj = self.pool.get('product.product') stock_obj = self.pool.get('stock.picking') stock_move_obj = self.pool.get('stock.move') warehouse_obj = self.pool.get('stock.warehouse') purchase_order_obj = self.pool.get('purchase.order') config_param_obj = self.pool.get('ir.config_parameter') connection_obj = self.pool.get('stock.connect') stock_production_lot_obj = self.pool.get('stock.production.lot') warehouse_id = connection_obj.browse(self.cr, self.uid, self.connection_id, context=self.context).warehouse_ids[0].id warehouse = warehouse_obj.browse(self.cr, self.uid, warehouse_id, context=self.context) xml = open_xml(file_text, _type='wba', print_error=self.print_errors) if nspath(xml, '//wba:WBA_List'): i = 0 self.cr.execute("SAVEPOINT yellowcube_wba_xml_factory__WBAList;") for x in nspath(xml, '//wba:WBA_List/wba:WBA'): i += 1 # First, we try to check the records try: text = xml_to_string(x) self.import_file(text) except Warning as w: self.cr.execute("ROLLBACK TO SAVEPOINT yellowcube_wba_xml_factory__WBAList;") print 'error>>>' * 5 print text print '<<<error' * 5 raise Warning('Error on sub WBA file number {0}'.format(i), format_exception(w)) self.cr.execute("RELEASE SAVEPOINT yellowcube_wba_xml_factory__WBAList;") return True validate_xml('wba', xml, print_error=False) imports = [] # Gets the timestamp. timestamp_postgres = self.str_date_to_postgres(nspath(xml, "//wba:Timestamp")[0].text) # Header fields (under <GoodsReceiptHeader>) header = nspath(xml, "//wba:GoodsReceiptHeader")[0] # <BookingVoucherID> and <BookingVoucherYear>. booking_voucher_id = nspath(header, "wba:BookingVoucherID")[0].text booking_voucher_year = nspath(header, "wba:BookingVoucherYear")[0].text supplier_order_no = nspath(header, "wba:SupplierOrderNo")[0].text # This is the stock.picking's ID. picking_in_ids = stock_obj.search(self.cr, self.uid, [('yellowcube_customer_order_no', '=', supplier_order_no), ('state', 'not in', ['cancel', 'done']), ], context=self.context) # Checks if the stock.picking exists. Otherwise, logs an issue an continues with the next one. self._check(warehouse, len(picking_in_ids) > 0, _("There is not any stock.picking with SupplierOrderNo (id) ={0}").format(supplier_order_no)) if not self.success: raise Warning('There where some errors in the WBA file.', self.errors) # Gets the stock picking in associated to this purchase order. picking_in = stock_obj.browse(self.cr, self.uid, picking_in_ids[0], self.context) # <SupplierNo>. # We first check if the supplier has a supplier number, and if that's the case we # compare against it. Otherwise, we compare against the default supplier number # set for the connector. supplier_no = nspath(header, "wba:SupplierNo")[0].text if picking_in.partner_id.supplier and picking_in.partner_id.yc_supplier_no: yc_supplier_no = picking_in.partner_id.yc_supplier_no self._check(warehouse, yc_supplier_no == supplier_no, _("Configuration variable YC SupplierNo does not match with that of tag 'SupplierNo' on the supplier.")) else: yc_supplier_no = self.get_param('supplier_no', required=True) self._check(warehouse, yc_supplier_no, _("Configuration variable YC SupplierNo is not defined in the system.")) self._check(warehouse, yc_supplier_no == supplier_no, _("Configuration variable YC SupplierNo does not match with that of tag 'SupplierNo' on the connector.")) # <SupplierOrderNo>. stock_picking_in_count = stock_obj.search(self.cr, self.uid, [('yellowcube_customer_order_no', '=', supplier_order_no)], context=self.context, count=True) self._check(warehouse, stock_picking_in_count > 0, _("Stock picking in with ID={0} does not exist in the system, thus can not be processed in the WBA.").format(supplier_order_no)) id_table = {} last_posno = 0 # Update missing values for line in picking_in.move_lines: if line.yc_posno: if line.yc_posno > last_posno: last_posno = line.yc_posno id_table[line.id] = line for line_id in sorted([x for x in id_table]): line = id_table[line_id] if not line.yc_posno: last_posno += 1 line.yc_posno = last_posno line.write({'yc_posno': last_posno}) # Refresh the record picking_in = stock_obj.browse(self.cr, self.uid, picking_in_ids[0], self.context) for article in nspath(xml, "//wba:GoodsReceiptList/wba:GoodsReceiptDetail"): partials = {} partial = {} # <SupplierOrderPosNo> pos_no = int(nspath(article, "wba:SupplierOrderPosNo")[0].text) # Gets the stock.move associated to this line. move_line = None for line in picking_in.move_lines: if line.yc_posno == pos_no: move_line = line break self._check(picking_in, move_line is not None, _('Mismatch with stock picking line number {0}/{1}').format(pos_no, [x.yc_posno for x in picking_in.move_lines])) if not self.success: raise Warning('Error parsing wba file', self.errors) partials[move_line if V8 else "move{0}".format(move_line.id)] = partial partial['delivery_date'] = timestamp_postgres # Caches the product of the stock.move. product_id = move_line.product_id.id partial['product_id'] = product_id product = product_obj.browse(self.cr, self.uid, product_id, self.context) # <YCArticleNo> yc_article_no = nspath(article, "wba:YCArticleNo")[0].text if not product.yc_YCArticleNo: product_obj.write(self.cr, self.uid, product_id, {'yc_YCArticleNo': yc_article_no}, self.context) product.message_post(_('Product {0} with ID={1} did not have a YCArticleNo, so it was created with value {2}').format(product.name, product_id, yc_article_no)) else: # If the product already had a YCArticleNo, then we check if the values match. self._check(warehouse, product.yc_YCArticleNo == yc_article_no, _("The 'YCArticleNo' does not match with the field 'YCArticleNo' of the product.")) # <ArticleNo> article_no = nspath(article, "wba:ArticleNo") if article_no: article_no = article_no[0].text if not product.default_code: product_obj.write(self.cr, self.uid, product_id, {'default_code': article_no}, self.context) product.message_post(_('Product {0} with ID={1} did not have a default_code, so it was created with value {2}').format(product.name, product_id, article_no)) else: # If the product already has an ArticleNo (field 'default_code' in Odoo), then we check if the values match. self._check(warehouse, product.default_code == article_no, '{0} [{1}!={2}]'.format(_("The 'ArticleNo' does not match with the field 'default_code' of the product."), product.default_code, article_no)) # <EAN> ean = nspath(article, "wba:EAN") if ean: ean = ean[0].text if not product.ean13: product_obj.write(self.cr, self.uid, product_id, {'ean13': ean}, self.context) product.message_post(_('Product {0} with ID={1} did not have an ean13 code, so it was created with value {2}').format(product.name, product_id, ean)) else: # If the product already has an EAN (field 'ean13' in Odoo) then we check if both values match. self._check(warehouse, product.ean13 == ean, _("The 'EAN' does not match with the field 'ean13' of the product.")) # <Lot> lot_search_domain = [('product_id', '=', product_id)] lot = nspath(article, 'wba:Lot') if lot: lot = lot[0].text lot_search_domain.append(('name', '=', lot)) # <YCLot> yc_lot = nspath(article, 'wba:YCLot') if yc_lot: yc_lot = yc_lot[0].text # lot_search_domain.append(('yellowcube_lot', '=', yc_lot)) # If a lot was indicated but it does not exist in the system, create it. lot_ids = stock_production_lot_obj.search(self.cr, self.uid, lot_search_domain, context=self.context) if lot and (not lot_ids): lot_id_ = stock_production_lot_obj.create(self.cr, self.uid, {'name': lot, 'yellowcube_lot': yc_lot or False, 'product_id': product_id, 'date': timestamp_postgres}, self.context) lot_ids = [lot_id_] lot = stock_production_lot_obj.browse(self.cr, self.uid, lot_id_, self.context) lot.message_post(_('Stock.production.lot {0} with ID={1} did not existed, and it was created by VoucherID {2}').format(lot.name, lot.id, booking_voucher_id),) lot = None if lot_ids: lot = stock_production_lot_obj.browse(self.cr, self.uid, lot_ids[0], self.context) # If <YCLot> exists but the stock production lot does not have it, stores it. If it has it, checks. if yc_lot and lot: if not lot.yellowcube_lot: stock_production_lot_obj.write(self.cr, self.uid, lot.id, {'yellowcube_lot': yc_lot}, self.context) lot.message_post(_('Stock.production.lot {0} with ID={1} did not have a yellowcube_lot, so it was set with value {2}').format(lot.name, lot.id, yc_lot)) else: self._check(warehouse, lot.yellowcube_lot == yc_lot, _("YCLot in the WBA does not match with the value of the stock.production.lot")) if lot_ids: partial['restrict_lot_id' if V8 else 'prodlot_id'] = lot_ids[0] if product.track_incoming: self._check(warehouse, lot is not None, _("The WBA file must contain a lot, otherwise the stock.move can not be updated for product {0}".format(product.name))) # <Plant> plant = nspath(article, 'wba:Plant')[0].text current_plant = self.get_param('plant_id', required=True) if current_plant: self._check(warehouse, current_plant == plant, _('Plant does not match with the value of the configuration parameter YC PlantID.')) elif not current_plant: self.set_param('plant_id', plant) # <QuantityUOM> quantity_uom = float(nspath(article, "wba:QuantityUOM")[0].text) self._check(picking_in, move_line.product_qty >= quantity_uom, _('Product {0}: QuantityUOM is greater than that of the stock.move.').format(product.name)) partial['product_qty'] = quantity_uom # <QuantityISO> quantity_iso = nspath(article, "wba:QuantityUOM")[0].attrib['QuantityISO'] uom_iso_list = self.pool.get('product.uom').search(self.cr, self.uid, [('uom_iso', '=', quantity_iso)], context=self.context) if len(uom_iso_list) > 0 and move_line.product_uom and (quantity_iso != move_line.product_uom.uom_iso): self._check(picking_in, False, _('Product {0}: Attribute QuantityISO does not match the ISO code indicated of the original stock.move.').format(product.name)) else: if not move_line.product_uom: product_uom = uom_iso_list[0] partial['product_uom'] = product_uom else: self._check(warehouse, move_line.product_uom.uom_iso == quantity_iso, _('Product {0}: Attribute QuantityISO does not match that of the stock.move.').format(product.name)) partial['product_uom'] = move_line.product_uom.id # Checks <StorageLocation> and <StockType> # Notes: Check together with StorageLocation against location_id on stock.move - alarm if wrong. # If free type (' ', '0', 'F') use the StorageLocation, otherwise location YBLK. storage_location = nspath(article, "wba:StorageLocation")[0].text stock_type = nspath(article, "wba:StockType") if move_line.location_id: if stock_type: # If there exists the tag <StockType>, then we follow the rules. stock_type = stock_type[0].text if stock_type not in ('X', 'S', '2', '3', '0', 'F', ' '): self._check(picking_in, False, _("Product {0}: StockType had value '{1}', which is not allowed.").format(product.name, stock_type), self.context) elif stock_type in ('0', 'F', ' '): if move_line.location_dest_id.name != storage_location: self._check(picking_in, False, _('Product {0}: StorageLocation {1} does not match with the location indicated in the stock.move {2}.').format(product.name, storage_location, move_line.location_dest_id.name)) else: if move_line.location_dest_id.name != 'YBLK': self._check(picking_in, False, _("Product {0}: StorageLocation must be 'YBLK' since StockType is not a free type.").format(product.name)) else: # If <StockType> does not exist, it just checks that the values match. if move_line.location_dest_id.name != storage_location: self._check(picking_in, False, _('Product {0}: StorageLocation {1} does not match with the location indicated in the stock.move {2}.').format(product.name, storage_location, move_line.location_dest_id.name)) else: self._check(picking_in, False, _('Product {0}: The stock move does not have a location_id.').format(product.name)) # <EndOfDeliveryFlag> if self.success: end_of_delivery_flag = nspath(article, "wba:EndOfDeliveryFlag")[0].text complete_move_ids = [] if V8: for move in partials: vals = partials[move] new_move_id = stock_move_obj.split(self.cr, self.uid, move, vals['product_qty'], restrict_lot_id=vals.get('restrict_lot_id', False), context=self.context) stock_move_obj.action_done(self.cr, self.uid, [new_move_id], context=self.context) complete_move_ids.append(new_move_id) else: complete_move_ids = picking_in.do_partial(partials) if end_of_delivery_flag == '1': # delivery is completed. number_of_pending_moves = stock_move_obj.search(self.cr, self.uid, [('picking_id', '=', picking_in.id), ('state', 'in', ('draft', 'waiting', 'confirmed', 'assigned')), ], context=self.context, count=True) if number_of_pending_moves > 0: pass # They don't want this alarm __for the moment__. #self.post_issue(picking_in, _('Tag EndOfDeliveryFlag was set, but there exists some stock move which are not in state finish nor cancelled.')) else: picking_in.action_done() # Closes the picking. # moves may have been deleted in the process (???) # So that is why we need to iterate over those which are kept. move_ids = stock_move_obj.search(self.cr, self.uid, [('id', 'in', complete_move_ids)], context=self.context) stock_move_obj.write(self.cr, self.uid, move_ids, {'yc_booking_voucher_id': booking_voucher_id, 'yc_booking_voucher_year': booking_voucher_year, }, self.context) if self.success: self.mark_record(picking_in.id, 'stock.picking' if V8 else 'stock.picking.in') # Only confirm when received the end of delivery flag # picking_in.action_done() else: raise Warning('There where some errors in the WBA file', self.errors) return True
def import_file(self, file_text): configuration_data = self.pool.get('configuration.data').get( self.cr, self.uid, []) logger.debug("Processing WAR file") self.success = True self.errors = [] stock_obj = self.pool.get("stock.picking") partner_obj = self.pool.get('res.partner') stock_move_obj = self.pool.get('stock.move') product_obj = self.pool.get('product.product') connection_obj = self.pool.get('stock.connect') # Gets the warehouse of the YellowCube. warehouse = connection_obj.browse( self.cr, self.uid, self.connection_id, context=self.context).warehouse_ids[0] xml = open_xml(file_text, _type='war', print_error=self.print_errors) if nspath(xml, '//warr:WAR_List'): i = 0 self.cr.execute("SAVEPOINT yellowcube_war_xml_factory__WARList;") for x in nspath(xml, '//warr:WAR_List/warr:WAR'): i += 1 # First, we try to check the records try: text = xml_to_string(x) self.import_file(text) except Warning as w: self.cr.execute( "ROLLBACK TO SAVEPOINT yellowcube_war_xml_factory__WARList;" ) print 'error>>>' * 5 print text print '<<<error' * 5 raise Warning('Error on sub WAR file number {0}'.format(i), format_exception(w)) self.cr.execute( "RELEASE SAVEPOINT yellowcube_war_xml_factory__WARList;") return True validate_xml('war', xml, print_error=False) order_header = nspath(xml, "//warr:CustomerOrderHeader")[0] customer_order_no = nspath(order_header, "warr:CustomerOrderNo")[0].text stock_ids = stock_obj.search( self.cr, self.uid, [('yellowcube_customer_order_no', '=', customer_order_no), ('state', 'in', ['confirmed', 'assigned'])], context=self.context) # Checks if the stock.picking exists. Otherwise, logs an issue an continues with the next one. self._check( warehouse, len(stock_ids) > 0, _("There is not any stock.picking with CustomerOrderNo ={0} in state confirmed or assigned." ).format(customer_order_no)) if not self.success: raise Warning( 'There where some errors in the WAR file: {0}'.format( '\n'.join(self.errors))) # Gets the stock picking out associated to this WAR. picking_out = stock_obj.browse(self.cr, self.uid, stock_ids, context=self.context)[0] # Saves BookingVoucherID and BookingVoucherYear on the stock.move goods_issue_header = nspath(xml, "//warr:GoodsIssueHeader")[0] booking_voucher_id = nspath(goods_issue_header, "warr:BookingVoucherID")[0].text booking_voucher_year = nspath(goods_issue_header, "warr:BookingVoucherYear")[0].text # TODO: Put this at the end, like in the WBA. # for move_line in picking_out.move_lines: # stock_move_obj.write(self.cr, self.uid, move_line.id, {'booking_voucher_id': booking_voucher_id, # 'booking_voucher_year': booking_voucher_year, # }, self.context) # Validates DepositorNo against the system's parameter. If does not match, then aborts and logs an issue. depositor_no = nspath(goods_issue_header, "warr:DepositorNo")[0].text expected_depositor_no = self.get_param('depositor_no', required=True) self._check( warehouse, expected_depositor_no, _("Variable YC DepositorNo is not defined in the configuration data." )) self._check( warehouse, depositor_no == expected_depositor_no, _("Configuration variable YC DepositorNo does not match with that of tag 'DepositorNo'" )) # <YCDeliveryNo> yellowcube_delivery_no = nspath(order_header, "warr:YCDeliveryNo")[0].text if yellowcube_delivery_no and picking_out.yellowcube_delivery_no and picking_out.yellowcube_delivery_no != yellowcube_delivery_no: self.post_issue( warehouse, _('YCDeliveryNo {0} does not match its current value {1} in the stock picking.' ).format(picking_out.yellowcube_delivery_no, yellowcube_delivery_no), create=True, reopen=True) if picking_out.yellowcube_delivery_no != yellowcube_delivery_no: stock_obj.write(self.cr, self.uid, [picking_out.id], {'yellowcube_delivery_no': yellowcube_delivery_no}, context=self.context) # <YCDeloveryDate> yellowcube_delivery_date = nspath(order_header, "warr:YCDeliveryDate")[0].text if yellowcube_delivery_date and picking_out.yellowcube_delivery_date and picking_out.yellowcube_delivery_date != yellowcube_delivery_date: self.post_issue( warehouse, _('YCDeliveryDate {0} does not match its current value {1} in the stock picking.' ).format(picking_out.yellowcube_delivery_date, yellowcube_delivery_date), create=True, reopen=True) if picking_out.yellowcube_delivery_date != yellowcube_delivery_date: stock_obj.write( self.cr, self.uid, [picking_out.id], {'yellowcube_delivery_date': yellowcube_delivery_date}, context=self.context) # <PartnerReference> partner_reference = nspath(order_header, "warr:PartnerReference") if partner_reference: partner_reference = partner_reference[0].text if picking_out.partner_id.ref: self._check( warehouse, picking_out.partner_id.ref == partner_reference, _('PartnerReference does not match its current value in the stock picking.' )) else: partner_obj.write(self.cr, self.uid, picking_out.partner_id.id, {'ref': partner_reference}, context=self.context) # <PostalShipmentNo> carrier_tracking_ref = nspath(order_header, "warr:PostalShipmentNo")[0].text stock_obj.write(self.cr, self.uid, [picking_out.id], {'carrier_tracking_ref': carrier_tracking_ref}, context=self.context) partials = {} id_table = {} i = 1 for line in sorted([x.id for x in picking_out.move_lines]): id_table[i] = line i += 1 for order_move in nspath(xml, "//warr:CustomerOrderDetail"): partial = {} pos_no = int(nspath(order_move, "warr:CustomerOrderPosNo")[0].text) # Gets the stock.move associated to this line. move_line = None for line in picking_out.move_lines: if line.id == id_table[pos_no]: move_line = line break # Checks that the line exists. self._check( picking_out, move_line is not None, _('CustomerOrderPosNo={0}: Mismatch with stock picking line number' ).format(pos_no)) if not self.success: raise Warning('Error parsing WAR file: {0}'.format('\n'.join( self.errors))) partials[move_line if V8 else "move{0}".format(move_line.id )] = partial # Caches the product of the stock.move. product_id = move_line.product_id.id partial['product_id'] = product_id product = product_obj.browse(self.cr, self.uid, product_id, self.context) # <YCArticleNo> yc_article_no = nspath(order_move, "warr:YCArticleNo")[0].text if product.yc_YCArticleNo: self._check( picking_out, product.yc_YCArticleNo == yc_article_no, _('Product {0} (id={1}): YCArticleNo does not match with YCArticleNo.' ).format(product.name, product_id)) else: product_obj.write(self.cr, self.uid, product_id, {'yc_YCArticleNo': yc_article_no}, self.context) # <ArticleNo> article_no = nspath(order_move, "warr:ArticleNo") if article_no: article_no = article_no[0].text self._check( picking_out, product.default_code == article_no, _('Product {0} (id={1}): ArticleNo does not match with default_code.' ).format(product.name, product_id)) # <EAN> ean = nspath(order_move, "warr:EAN") if ean: ean = ean[0].text if product.ean13: self._check( picking_out, product.ean13 == ean, _('Product {0} (id={1}): EAN does not match with ean13.' ).format(product.name, product_id)) else: product_obj.write(self.cr, self.uid, product_id, {'ean13': ean}, self.context) # <Lot> lot = nspath(order_move, "warr:Lot") if lot: lot = lot[0].text # Searches for that lot in the system. lot_ids = self.pool.get('stock.production.lot').search( self.cr, self.uid, [('name', '=', lot), ('product_id', '=', product_id)], context=self.context) if not lot_ids: self._check( warehouse, False, _('Lot {0} for product {1} (id={2}) does not exist in the system' ).format(lot, product.name, product_id)) elif getattr(move_line, 'restrict_lot_id' if V8 else 'prodlot_id'): if self._check( picking_out, getattr(move_line, 'restrict_lot_id' if V8 else 'prodlot_id').name == lot, _('Product {0} (id={1}): Lot does not match the lot indicated of the original stock.move.' ).format(product_obj.name, product_id)): partial['restrict_lot_id' if V8 else 'prodlot_id'] = lot_ids[0] if product.track_outgoing: self._check( warehouse, lot, _("The WAR file must contain a lot, otherwise the stock.move can not be updated for product {0}" .format(product.name))) # <Plant> plant = nspath(order_move, "warr:Plant")[0].text current_plant = self.get_param('plant_id', required=True) if current_plant: self._check( picking_out, current_plant == plant, _('Product {0} (id={1}): Plant does not match with the value of the configuration parameter YC PlantID.' ).format(product.name, product_id)) elif not current_plant: configuration_data.write(self.cr, self.uid, configuration_data.id, {'yc_plant_id': plant}, self.context) # <QuantityUOM> quantity_uom = float( nspath(order_move, "warr:QuantityUOM")[0].text) self._check( picking_out, move_line.product_qty >= quantity_uom, _('Product {0} (id={1}): QuantityUOM is greater than that of the stock.move.' ).format(product.name, product_id)) partial['product_qty'] = quantity_uom # <QuantityISO> quantity_iso = nspath(order_move, "warr:QuantityUOM")[0].attrib['QuantityISO'] uom_iso_list = self.pool.get('product.uom').search( self.cr, self.uid, [('uom_iso', '=', quantity_iso)], context=self.context) if len(uom_iso_list) > 0 and move_line.product_uom and ( quantity_iso != move_line.product_uom.uom_iso): self._check( picking_out, False, _('Product {0} (id={1}): Attribute QuantityISO does not match the ISO code indicated of the original stock.move.' ).format(product.name, product_id)) else: if not move_line.product_uom: product_uom = uom_iso_list[0] partial['product_uom'] = product_uom else: self._check( picking_out, move_line.product_uom.uom_iso == quantity_iso, _('Product {0} (id={1}): Attribute QuantityISO does not match that of the stock.move.' ).format(product.name, product_id)) partial['product_uom'] = move_line.product_uom.id # Checks <StorageLocation> and <StockType> # Notes: Check together with StorageLocation against location_id on stock.move - alarm if wrong. # If free type (' ', '0', 'F') use the StorageLocation, otherwise location YBLK. storage_location = nspath(order_move, "warr:StorageLocation")[0].text stock_type = nspath(order_move, "warr:StockType") if move_line.location_id or move_line.location_dest_id: location_names = [] if move_line.location_id: location_names.append(move_line.location_id.name) if move_line.location_dest_id: location_names.append(move_line.location_dest_id.name) if stock_type: # If there exists the tag <StockType>, then we follow the rules. stock_type = stock_type[0].text if stock_type not in ('X', 'S', '2', '3', '0', 'F', ' '): self._check( picking_out, False, _("Product {0} (id={1}): StockType had value '{2}', which is not allowed." ).format(product.name, product_id, stock_type)) elif stock_type in ('0', 'F', ' '): self._check( picking_out, storage_location in location_names, _('Product {0} (id={1}): StorageLocation {2} and StockType {3} does not match with the location indicated in the stock.move {4}' ).format(product.name, product_id, storage_location, stock_type, location_names)) else: self._check( picking_out, 'YBLK' in location_names, _("Product {0} (id={1}): StorageLocation must be 'YBLK' since StockType is not a free type." ).format(product.name, product_id)) else: # If <StockType> does not exist, it just checks that the values match. if storage_location not in location_names: self._check( picking_out, False, _('Product {0} (id={1}): StorageLocation {2} does not match with the location indicated in the stock.move {3}' ).format(product.name, product_id, storage_location, location_names)) else: self._check( picking_out, False, _('Product {0} (id={1}): The stock move does not have a location_id.' ).format(product.name, product_id)) # <Serial Numbers> serial_numbers = nspath(order_move, "warr:SerialNumbers") if serial_numbers: serial_numbers = serial_numbers[0].text if move_line.serial_number_scanned: self._check( picking_out, move_line.serial_number_scanned == serial_numbers, _('Product {0} (id={1}): SerialNumbers does not match the serial_number_scanned indicated of the original stock.move.' ).format(product.name, product_id)) else: stock_move_obj.write( self.cr, self.uid, move_line.id, {'serial_number_scanned': serial_numbers}, self.context) if self.success: picking_id = picking_out.id picking_out.message_post( _('Imported WAR file BookingVoucherID={0} BookingVoucherYear={1}' ).format(booking_voucher_id, booking_voucher_year)) if V8: for move in partials: vals = partials[move] new_move_id = stock_move_obj.split( self.cr, self.uid, move, vals['product_qty'], restrict_lot_id=vals.get('restrict_lot_id', False), context=self.context) stock_move_obj.action_done(self.cr, self.uid, [new_move_id], context=self.context) else: backorder_id, picking_id = picking_out.wrapper_do_partial( partials) # Pickings created by WARs as backorders are not sent to the warehouse, by default. if backorder_id: stock_obj.write(self.cr, self.uid, backorder_id, { 'do_not_send_to_warehouse': True, }, context=self.context) picking_to_deliver = stock_obj.browse(self.cr, self.uid, picking_id, context=self.context) picking_to_deliver.action_done() picking_to_deliver.set_stock_moves_done() # Stores the values associated to BookingVoucherId and BookingVoucherYear, for reference. move_ids = [move.id for move in picking_out.move_lines] stock_move_obj.write( self.cr, self.uid, move_ids, { 'yc_booking_voucher_id': booking_voucher_id, 'yc_booking_voucher_year': booking_voucher_year, }, self.context) self.mark_record(picking_out.id, 'stock.picking' if V8 else 'stock.picking.out') # The message is sent ONLY if we had success. # picking_out.message_post(body=_("""Your order has been shipped through {0} and it can be tracked in the next link:\ # <br/>\ # <a href='https://www.post.ch/swisspost-tracking?formattedParcelCodes={1}'>Track&Trace</a>\ # """).format(picking_out.carrier_id.name, urllib.quote(picking_out.carrier_tracking_ref)), # type='comment', # subtype="mail.mt_comment", # context=self.context, # partner_ids=picking_out.carrier_id and [picking_out.carrier_id.partner_id.id] or []) else: raise Warning( 'There where some errors in the WAR file: {0}'.format( '\n'.join(self.errors))) return True