← Back to team overview

kicad-developers team mailing list archive

Feature request: Extended BOM (Part, Footprint, etc.) management

 

Hello!

This is my first post to this mailing list and I wasn't following recent
development in other posts so I don't know if something similar is
already in planning.

Last autumn my problem was, that I had a quite big project with multiple
sub-schematas and stuff and wanted it to be manufactured externally.
Fabs always want a nice fabs for the parts to use and sometimes it's
better/cheaper to use similar parts.
For example you need 4 SM0603/2uF capacitors and one SM0402/4uF
capacitor you may want to use 5 of either because it doesn't really
matter. Now it's really hard to find the parts you want to change, you
have to generate a BOM find the part-number, search it in the schemata
change the uF value, then export the new netlist, search the part again 
in the CvPcb and edit the footprint for each part.
Also if you want to change some other field it would be much easier to 
just edit it in a spreadsheet application like excel or so.

Because of that I hacked together some (very ugly!) python scripts with 
my own (even uglier!!) eeschema-parser (Kicad_schema_parser.py).

Now the process for editing fields looks like this :

Generate_BOM_from_N-Schematas.py
- ./Generate_BOM_from_N-Schematas.py MyTopSchema.sch
- this generates a MyTopSchema_BOM.csv file from MyTopSchema.sch and all
   it's sub-schematas
  - it only exports fields specified in the field_names-variable of the
    parts class-instance in Generate_BOM_from_N-Schematas.py
	parts=Parts(
		field_names=[
			"Value",
			"Footprint",
			"Type",
			"Voltage Rating",
			"Manufacturer",
			"Part Number",
			"Comments"
		]
	)
  - if a field isn't specified for a part it will be empty in the csv
  - the rows are sorted by 'Chip Name', then by field_names
    ("Value",Footprint",etc..)
  - the fields 'Chip Name', Count and References will always be
    generated (and exported)
   - Count is only informative, the count of space separated references
     in the References row
   - References are grouped if they have exactly the same field-values
     (which is not so good if not even 'Value' is specified)
I think you can also add new columns but I'm not quite sure
- Now I edit the csv, fill in footprints/manufacturers/etc. where
   applicable

After editing the BOM i save it again in the same csv-file (also same 
format!)

Add_user_fields_to_components.py
  - /Add_user_fields_to_components.py -BOM=MyTopSchema_BOM.csv
    MyTopSchema.sch
  - this changes the fields specified in the third line of
    MyTopSchema_BOM.csv
   - if a field is empty it is deleted
   - if a field is not specified on row 3 it's left unchanged, maybe :)

Reopen MyTopSchema.sch with eeschema.
  Export the netlist
Run CvPcb, save and close CvPcb

Update_Footprints_in_CMP.py MyTopSchema.sch
  - ./Update_Footprints_in_CMP.py MyTopSchema.sch change
  - this changes the footprints in MyTopSchema.cmp according to the
    footprint field

Run Pcbnew
  Import netlist with change/replace footprints and so..

Fertig!

Would it be hard to implement something similar directly in Kicad?
I think the first thing to change is the BOM-export form. Or maybe it 
would be better if there was a project based user-field-manager or so..


Cheers,
Oli


PS:
   Most of the scripts have 'help-notices' if you run them without
   arguments. If your interested in the scripts, I could clean them up
   for you. If you want to try them out make backups first!

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from sys import argv
from os.path import basename, splitext, isfile, isdir, join
from operator import itemgetter
from pprint import pprint

from Kicad_schema_parser import *


chip_name = ""
alterations = []
files  = []
try :
	chip_name = "*"
	argv = split_save_str((" ".join(argv)).strip(), ' ')
	if argv[1][:len("-BOM=")]=="-BOM=" :
		alterations = BOM_to_alterations(argv[1][len("-BOM="):])
		if alterations is None or len(alterations)==0 :
			raise Exception("no alterations found in BOM")
		file_idx=1
	else :
		chip_name      = clean_str(argv[1])
		file_idx = argv.index("-")
		for f in argv[2:file_idx] :
			f=clean_str(f)
			f = f.split("?")
			if len(f) <> 2: raise Exception("malformed expression "+str(f))
			conditions = {}
			for f0 in f[0].split("&&") :
				f0=f0.strip()
				if not f0 : continue
				c = f0.split("==")
				if not len(c)==2: raise Exception("malformed condition "+str(f))
				conditions[clean_str(c[0])] = clean_str(c[1])
			changes = {}
			for f1 in f[1].split(";") :
				c = f1.split("=")
				if len(c) <> 2: raise Exception("malformed field-assignment "+str(f))
				changes[clean_str(c[0])] = clean_str(c[1])
			if len(changes)==0: raise Exception("shit! no changes!")
			alterations.append((conditions,changes))

	files    = argv[file_idx+1:]
	for f in files[::] :
		if not isfile(f) : raise Exception("shit! not a file")
	if len(files)==0 : raise Exception("specify at least one file")
	
except Exception, ex:
	print ex.message
	print
	print "usage A : Add_user_fields_to_components.py [Chipname] [F==V [&& F==V].. ].. ? F=V[;F=V].. - [filepath].."
	print "eg. ./Add_user_fields_to_components.py C \"Value==100n?Voltage=10V;Type=X5R;\" - file1.sch file2.sch"
	print "=> where Chipname is 'C' and Value=='100n'"
	print "   set/create field Voltage='10V' and field Type='X5R'"
	print
	print "usage B : Add_user_fields_to_components.py -BOM=BOM-File.csv [filepath].."
	exit(0)


processed_files=[]
while len(files) :
	f = files.pop()
	skip=False
	for p in processed_files :
		if f.find(p)>-1 :
			print "duplicated file %s"%p
			skip=True
			break
	if skip : continue
	processed_files.append(basename(f))

	print "changing",f
	data = open(f,'r').read().split("\n")
	nf=""
	part=None
	is_in_comp  = False
	is_in_sheet = False
	for l in data :
		if   l=="$Comp" :
			is_in_comp=True
			part = Part()
		elif l=="$EndComp" :
			part.feed_comp_line(l)
			if (part.chip_name==chip_name or chip_name=="*") and len(part.fields) :
				part.alter_fields(alterations)
			nf += part.get_component_string()
			part = None
			is_in_comp=False
			continue
		elif l=="$Sheet" :
			is_in_sheet=True
		elif l=="$EndSheet" :
			is_in_sheet=False

		if is_in_comp :
			part.feed_comp_line(l)
			continue
		elif is_in_sheet and len(l)>3 and l[:3]=="F1 " :
			files.append(clean_str(split_save_str(l.strip()," ")[1]))
		
		nf += l+"\n"
	
	open(f,'w').write(nf.strip())
			


print "done."

#!/usr/bin/env python

from sys import argv
from os.path import basename
import re

print "usage :  ./Change_Sheet-IDs.py add[:min] file1-without.type file2-without.type ..."

v=argv[1].split(":")
shift=int(v[0])
min_val=0
if len(v)>1 : min_val=int(v[1])
pot=0
s=abs(shift)
while s :
	pot+=1
	s/=10

print shift, min_val, pot
def update_file(f, searchstr,endel) :
	global pot, shift, min_val
	data = open(f,'r').read()
	nf = open(basename(f)+".edit",'w')
	x=0
	pot_bkp = pot
	while 1 :
		off=x
		y=data[x:].find(searchstr)
		if y<0 : break
		x+=y
		x+=len(searchstr)
		y=data[x:].find(endel)
		if y<0 : break
		x+=y
		while pot>2 :
			try :
				y=x-pot
				val=int(data[y:x])
				break;
			except :
				pot-=1
		if pot==2 : exit(1)
		if val>=min_val : val+=shift
		nf.write(data[off:y]+str(val))
		#break
	nf.write(data[off:])
	nf.close()

for f in argv[2:] :
	update_file(f+".kicad_pcb"," reference "," ")
	update_file(f+".cmp","Reference = ",";")

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from sys import argv
from os.path import basename, splitext, isfile, isdir, join
from operator import itemgetter
from pprint import pprint

from Kicad_schema_parser import *

ref = ""
alterations = []
files  = []
try :
	if len(argv)<2 or len(argv)>2 : raise Exception("check usage..")
	start_file = argv[1]
	if not isfile(argv[1]) : raise Exception("not a file")
	
except Exception, ex:
	print ex.message
	print
	print "usage : ./Generate_BOM_from_N-Schematas.py [filepath]"
	print "eg. ./Generate_BOM_from_N-Schematas.py file1.sch"
	exit(0)

files = [ start_file ]

parts=Parts(
	field_names=[
		"Value",
		"Footprint",
		"Type",
		"Voltage Rating",
		"Manufacturer",
		"Part Number",
		"Comments"
	]
)

processed_files=[]
while len(files) :
	f = files.pop()
	skip=False
	for p in processed_files :
		if f.find(p)>-1 :
			print "duplicated file %s"%p
			skip=True
			break
	if skip : continue
	processed_files.append(basename(f))

	print "collecting parts from",f
	data = open(f,'r').read().split("\n")
	part=None
	is_in_comp  = False
	is_in_sheet = False
	for l in data :
		if   l=="$Comp" :
			is_in_comp=True
			part = Part()
		elif l=="$EndComp" :
			part.feed_comp_line(l)
			parts.add_part(part)
			# only dif from "Add_user_field_loop.."
			#if (part.chip_name==chip_name or chip_name=="*") and len(part.fields) :
			#	part.alter_fields(alterations)
			#nf += part.get_component_string()
			part = None
			is_in_comp=False
			continue
		elif l=="$Sheet" :
			is_in_sheet=True
		elif l=="$EndSheet" :
			is_in_sheet=False

		if is_in_comp :
			part.feed_comp_line(l)
			continue
		elif is_in_sheet and len(l)>3 and l[:3]=="F1 " :
			files.append(clean_str(split_save_str(l.strip()," ")[1]))

open(splitext(start_file)[0]+"_BOM.csv",'w').write(str(parts))
print parts.num_parts, "parts found."

print "done."

# -*- coding: utf-8 -*-

import re

MANDATORY_FIELD_NAMES = ["Reference","Value","Footprint","Datasheet"]

def clean_str(f) :
	f=f.strip()
	if f=='"' or f=='' : return ''
	s=0;e=0
	if f[0] =='"' : s=1
	if f[-1]=='"' : e=1
	if (s+e) :
		f=f[s:-e]
	return f

def save_str(f) :
	return '"'+str(f).replace('"','')+'"'

#def split_save_str(s,d) :
	#first = True
	#save_part = False
	#r=[]
	#for x in s.split('"') :
		##x = x.strip()
		#if save_part :
			#r.append(save_str(x))
		#elif len(x)>int(not first)  : r += x.split(d)[:-1]
		#save_part = not save_part
		#first=False
	#return r

def split_save_str(s,d) :
	save_part_start = s.find('"')
	if save_part_start==-1 : return s.split(d)
	else                   : save_part_end = s.find('"',save_part_start+1)
	current_offset = 0
	r = []
	next_split = s.find(d,current_offset)
	while current_offset<len(s) :
		if   next_split == -1                                      :
			r.append(s[current_offset:])
			break
		elif next_split  < save_part_start or save_part_start==-1  :
			r.append(s[current_offset:next_split])
			if next_split==len(s)-1 :
				r.append("")
				break
			current_offset = next_split+1
			next_split = s.find(d,current_offset)
		elif next_split  < save_part_end                           :
			next_split = s.find(d,save_part_end+1)
		elif save_part_start<>-1                                   :
			save_part_start = s.find('"',save_part_end+1)
			if save_part_start<>-1 : 
				save_part_end = s.find('"',save_part_start+1)
				if save_part_end==-1 : #bzw. next_split=-1
					r.append(s[current_offset:])
					break
			else                   : save_part_end = -1
	return r

def ref_cmp(a,b) :
	a=int(re.search('[0-9]*$',a).group(0))
	b=int(re.search('[0-9]*$',b).group(0))
	if a==b : return 0
	if a<b  : return -1
	return 1


def extract_value_field(part) :
	value=part.fields[1][1] # see MANDATORY_FIELD_NAMES value index..
	m=re.search('^[0-9.,]*',value)
	i = m.end()
	if i>0 :
		v=float(m.group(0))
		if   i==len(value) : return v,""
		p=value[i]
		if   p=='f': v/=1000000000000000
		elif p=='p': v/=1000000000000
		elif p=='n': v/=1000000000
		elif p=='u' or p=='µ': v/=1000000
		elif p=='m' : v/=1000
		elif p=='k' : v*=1000
		elif p=='M' : v*=1000000
		elif p=='G' : v*=1000000000
		elif p=='T' : v*=1000000000000
		else : i-=1
		value=value[i+1:]
	else : v=0
	return v,value

def part_cmp(a,b) :
	if not (a.complete and b.complete) : raise Exception("Part not complete")
	
	if a.chip_name<b.chip_name : return -1
	if a.chip_name>b.chip_name : return  1
	
	a_v, a_value = extract_value_field(a)
	b_v, b_value = extract_value_field(b)	
	if a_v<b_v         : return -1
	if a_v>b_v         : return  1
	if a_value<b_value : return -1
	if a_value>b_value : return  1
	
	return 0

#pprint( alterations )
class Part :
	def __init__(self) :
		self.chip_name=""
		self.references=[]
		self.fields={}
		self.comp =""
		self.complete = False
		self.shit=False
	
	def feed_comp_line(self,l) :
		if self.complete : raise Exception("Part already complete")
		if not len(l)>2 : return
		
		fitems = split_save_str(l.strip()," ")
		if   fitems[0]=='L' :
			self.chip_name=fitems[1]
		elif fitems[0]=='AR' :
			self.add_reference(clean_str(fitems[2].split("=")[1]))
			#print "dropping AR-FIELD" ; return
		if fitems[0]=='F' :
			if len(self.fields)==0 : self.comp+=">>>fields<<<\n"
			self.add_field(fitems)
		else :
			self.comp+=l+"\n"
		
		if l=="$EndComp" :
			self.complete=True
	
	def get_component_string(self) :
		if not self.complete : raise Exception("Part not complete")
		fields = ""
		for fitems in self.fields.itervalues() :
			fields+=" ".join(fitems[2])+"\n"
		fields=fields.strip()
		return self.comp.replace(">>>fields<<<", fields)
	
	def add_reference(self,ref) :
		try :    self.references.index(ref)
		except : self.references.append(ref)

	def add_field(self,fitems) :
		fi = int(fitems[1])
		if fi<len(MANDATORY_FIELD_NAMES) : name = MANDATORY_FIELD_NAMES[fi]
		else                   : 
			name = clean_str(fitems[-1])
			if name=="Comment" :
				name="Comments"
				fitems[-1] = save_str(name)
				print "Changing field name 'Comment' to 'Comments'"
		
		value = clean_str(fitems[2])
		if value=="~" :
			value=""
			fitems[2]='""'
		if fi>3 and value=="" : return # remove empty fields (except mandatory fields)..
		
		if fi>1 : # hide all fields, except "Reference","Value" (leave those untouched..)
			fitems[8]="0001";
		
		if fi==0 :
			self.add_reference(value)
		self.fields[fi] = (name,value,fitems)
	
	def get_field(self, field_name) :
		fi = -1
		try : return self.fields[MANDATORY_FIELD_NAMES.index(field_name)]
		except : pass
		try :
			for k,f in self.fields.iteritems() :
				if k<len(MANDATORY_FIELD_NAMES) : continue
				if f[0]==field_name : return self.fields[k]
		except : pass
		return None
	
	def alter_fields(self,alterations) :
			for alter in alterations :
				cond = True
				for key,value in alter[0].iteritems() :
					for fi,finfo in self.fields.iteritems() : # match the field name
						if finfo[0]==key :                #
							cond &= finfo[1]==value       # update the condition
							break
				if cond :
					for key,value in alter[1].iteritems() :
						field_existing = False
						changed = False
						remove_fields=[]
						for fi,finfo in self.fields.iteritems() : # match the field name
							if finfo[0]==key :                #
								field_existing = True
								if value<>finfo[1] :
									if fi>=len(MANDATORY_FIELD_NAMES) and value=="" :
										# removing the field
										print "removing field[%d] %s            (%s)" %(fi,finfo[0]," ".join(self.references))
										remove_fields.append(fi)
										#finfo[2][2]=save_str(value) # changed for field
									else :
										# set the new value
										print "changing field[%d] %s:  %s -> %s (%s)" %(fi,finfo[0],finfo[1],value," ".join(self.references))
										#finfo[1]=c[1] #unchanged for cond
										finfo[2][2]=save_str(value) # changed for field
									changed=True
								break
						for fi in remove_fields : self.fields.pop(fi)
						if not field_existing and value<>"" : # add a new field
							#pprint( cfields )
							
							keys = self.fields.keys()
							keys.sort()
							fi_l = keys[-1] # find the last field
							finfo_l = self.fields[fi_l]
							
							fi = fi_l+1
							fitems = finfo[2][::]
							fitems[1]=str(fi)
							fitems[2]=save_str(value)
							if   fitems[-1]=="CNN" : fitems.append(save_str(key))
							elif fitems[-2]=="CNN" : fitems[-1]=save_str(key)
							else : raise Exception("Wrong field? " + " ".join(fitems))
							fitems[-4] = "0001" # make field invisible..
							
							print "  adding field[%d] %s:  %s       (%s)" %(fi,key,value," ".join(self.references))
							self.fields[fi] = (key,value,fitems)
			
			#for finfo in self.fields.itervalues() :
			#	nf += " ".join(finfo[2])+"\n"
	
	
	def compare(self,part) :
		if not self.complete : raise Exception("Part not complete")
		if not part.complete : raise Exception("Part not complete")
		
		if len(self.fields)<>len(part.fields) : return False
		for f in self.fields.itervalues() :
			if f[0] == "Reference" : continue
			found = False
			for f2 in part.fields.itervalues() :
				if f[0] == f2[0] and f[1] == f2[1] : found=True
			if not found : return False
		return True
	
	def to_string(self, field_names=None): 
		self.references.sort(ref_cmp)
		ret = '%s\t%d\t%s' %( save_str(self.chip_name),len(self.references),save_str(" ".join(self.references)) )
		if field_names==None :
			for f in self.fields.itervalues() :
				if f[0] == "Reference" : continue
				ret += '\t%s=%s'%(f[0],f[1])
		else :
			for fn in field_names :
				ret += "\t"
				for f in self.fields.itervalues() :
					if f[0]==fn : ret += save_str(f[1])
		return ret
	
	def __str__(self):
		if self.complete :
			return self.get_component_string()
		else :
			return "incomplete part"

class Parts :
	def __init__(self, field_names=[]) :
		self.num_parts = 0
		self.parts=[]
		self.field_names=field_names
	
	def add_part(self, part) :
		if not part.complete : raise Exception("Part not complete")
		for p in self.parts :
			if p.compare(part) :
				off=len(p.references)
				for ref in part.references :
					p.add_reference(ref)
				self.num_parts+=len(p.references)-off
				return
		self.num_parts+=len(part.references)
		self.parts.append(part)
		for f in part.fields.itervalues() :
			if f[0] == "Reference" : continue
			if f[1]=="" : continue # skip fields with empty values (mostly the mandatory fields like Datasheet..)
			try    : self.field_names.index(f[0])
			except : self.field_names.append(f[0])
	
	def find_reference(self, reference) :
		if reference=="" : return None
		for p in self.parts :
			for r in p.references :
				if r==reference : return p
		return None
	
	def __str__(self) :
		ret = "%d parts\n"%self.num_parts
		ret+= '\n"Chip Name"\t"Count"\t"References"'
		for f in self.field_names :
			ret += '\t%s' %save_str(f)
		ret+="\n"
		self.sort()
		for p in self.parts :
			ret += p.to_string(self.field_names)+"\n"
		
		return ret
	
	def sort(self) :
		self.parts.sort(part_cmp)

#   conditions-format = {field_name: value}
#      changes-format = {field_name: value}
#   alteration-format = (conditions,changes)

from os.path import isfile
def BOM_to_alterations(f) :
	if not isfile(f) : raise Exception("%s is not a file"% f)
	print "BOM_to_alterations(%s)" %f
	data = open(f,'r').read().split("\n")
	alterations=[]
	BOM_started = False
	field_names=None
	for l in data :
		if l=="": continue
		items=split_save_str(l,'\t')
		if BOM_started :
			if len(items)<>len(field_names) :
				raise Exception("wrong item count in BOM!\n%s\n%s"%(str(items), str(field_names)))
			changes={}
			for i in range(3,len(items)) : 
				#if items[i]=="" : print "deleting field.."
				changes[field_names[i]] = clean_str(items[i])
			for reference in clean_str(items[2]).split(' ') :
				alterations.append(({"Reference":reference},changes))
		elif len(items) and clean_str(items[0])=="Chip Name" :
			if len(items)<4 : raise Exception("too little information!")
			BOM_started=True
			field_names=[]
			for f in items : field_names.append(clean_str(f))
	return alterations

def walk_schema(f) :
	print "tobedone.."

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from sys import argv
from os.path import basename, splitext, isfile, isdir, join
from operator import itemgetter
from pprint import pprint

def clean_str(f) :
	return f.strip().replace("'",'').replace('"','')
def save_str(f) :
	return '"'+f+'"'

files  = []
try :
	files = argv[1:]
	for f in files[::] :
		if not isfile(f) : raise Exception("shit! not a file")
	if len(files)==0 : raise Exception("specify at least a file")
	
except Exception, ex:
	print ex.message
	print
	print "usage : Remove_user_fields_from_components.py [filepath].."
	exit(0)

FIELD_NAMES = ["Reference","Value","Footprint","Datasheet"]
cfields = None

for f in files :
	c=-1
	print "changeing",f
	data = open(f,'r').read().split('\n')
	nf=""
	editing_fields = False
	for l in data :
		c+=1
		if l[:5] == "F 0 \"" :
			editing_fields=True
			cfields = {}

		if editing_fields and len(l) and l[0]=='F' :
			fitems = l.split(" ")
			fi = int(fitems[1])
			if fi<len(FIELD_NAMES) :
				name = FIELD_NAMES[fi]
				value = clean_str(fitems[2])
				cfields[fi] = (name,value,fitems)
			else                   : print "dropping user field %s " %clean_str(fitems[-1])
			continue
		
		if not cfields is None :
			for finfo in cfields.itervalues() :
				nf += " ".join(finfo[2])+"\n"
		
		cfields = None
		editing_fields=False
		nf += l+"\n"
	
	open(f,'w').write(nf.strip())

print "done."

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from sys import argv
from os.path import basename, splitext, isfile, isdir, join
from operator import itemgetter
from pprint import pprint

from Kicad_schema_parser import *

ref = ""
alterations = []
start_file=""
cmp_file=""
save_changes=False
try :
	if len(argv)<>3 : raise Exception("check usage..")
	start_file = argv[1]
	if not isfile(start_file) : raise Exception("not a file")
	cmp_file = splitext(start_file)[0]+".cmp"
	if not isfile(cmp_file) : raise Exception("not a file")
	print cmp_file
	if   argv[2]=="dry_run" : save_changes=False
	elif argv[2]=="change"  : save_changes=True
	else : raise Exception("check usage..")
	
	
except Exception, ex:
	print ex.message
	print
	print "usage : ./Update_Footprints_in_CMP.py filepath dry_run|change"
	print "eg. ./Update_Footprints_in_CMP.py file1.sch change"
	exit(1)

files = [ start_file ]

parts=Parts(
	field_names=[
		"Value",
		"Footprint",
		"Type",
		"Voltage Rating",
		"Manufacturer",
		"Part Number",
		"Comments"
	]
)

processed_files=[]
while len(files) :
	f = files.pop()
	skip=False
	for p in processed_files :
		if f.find(p)>-1 :
			print "duplicated file %s"%p
			skip=True
			break
	if skip : continue
	processed_files.append(basename(f))

	print "collecting parts from",f
	data = open(f,'r').read().split("\n")

	part=None
	is_in_comp  = False
	is_in_sheet = False
	for l in data :
		if   l=="$Comp" :
			is_in_comp=True
			part = Part()
		elif l=="$EndComp" :
			part.feed_comp_line(l)
			parts.add_part(part)
			# only dif from "Add_user_field_loop.."
			#if (part.chip_name==chip_name or chip_name=="*") and len(part.fields) :
			#	part.alter_fields(alterations)
			#nf += part.get_component_string()
			part = None
			is_in_comp=False
			continue
		elif l=="$Sheet" :
			is_in_sheet=True
		elif l=="$EndSheet" :
			is_in_sheet=False

		if is_in_comp :
			part.feed_comp_line(l)
			continue
		elif is_in_sheet and len(l)>3 and l[:3]=="F1 " :
			files.append(clean_str(split_save_str(l.strip()," ")[1]))

print parts.num_parts, "parts found."

data = open(cmp_file,'r').read().split("\n")
is_in_comp  = False
reference = None
nf=""
for l in data :
	if   l=="BeginCmp" :
		is_in_comp=True
	elif l=="EndCmp" :
		reference=None
		is_in_comp=False
	elif is_in_comp :
		if   l[:len("Reference = ")]=="Reference = " :
			reference = l[len("Reference = "):-1]
		elif l[:len("IdModule  = ")]=="IdModule  = " :
			footprint = l[len("IdModule  = "):-1]
			part = parts.find_reference(reference)
			if part==None :
				exit(1)
			f = part.get_field("Footprint")
			if f==None or f[1]=="":
				print "% 20s : has no footprint assigned!" %reference
			elif f[1]<>footprint :
				print "%20s : %20s > %20s" %(reference,footprint,f[1])
				if save_changes :
					l="IdModule  = %s;" %f[1]
	nf += l+"\n"

open(cmp_file,'w').write(nf)

print "done."


Follow ups