update cost generator
This commit is contained in:
parent
ec58128ae3
commit
4c3e594085
|
@ -9,6 +9,8 @@ attribs
|
||||||
*_notes.txt
|
*_notes.txt
|
||||||
*_bom.csv
|
*_bom.csv
|
||||||
*_cost.csv
|
*_cost.csv
|
||||||
|
octopart.apikey
|
||||||
|
*_costs.csv
|
||||||
*.net
|
*.net
|
||||||
*.cmd
|
*.cmd
|
||||||
*.new.pcb
|
*.new.pcb
|
||||||
|
|
483
Rakefile
483
Rakefile
|
@ -82,8 +82,13 @@ task :bom => boms
|
||||||
CLEAN.include(boms)
|
CLEAN.include(boms)
|
||||||
|
|
||||||
desc "get cost estimation for BOMs"
|
desc "get cost estimation for BOMs"
|
||||||
costs = targets.collect{|target| "#{target[:name]}_cost.csv"}
|
cost = targets.collect{|target| "#{target[:name]}_cost.csv"}
|
||||||
task :cost => costs
|
task :cost => cost
|
||||||
|
CLEAN.include(cost)
|
||||||
|
|
||||||
|
desc "get cost comparison for BOMs"
|
||||||
|
costs = targets.collect{|target| "#{target[:name]}_costs.csv"}
|
||||||
|
task :costs => costs
|
||||||
CLEAN.include(costs)
|
CLEAN.include(costs)
|
||||||
|
|
||||||
desc "convert schematic to pcb layout"
|
desc "convert schematic to pcb layout"
|
||||||
|
@ -149,7 +154,7 @@ def bom2(schematic, attributes)
|
||||||
[attributes.to_s]
|
[attributes.to_s]
|
||||||
end
|
end
|
||||||
# generate bom2
|
# generate bom2
|
||||||
list = `gnetlist -g bom2 -Oattribs=#{attributes*','} -q -o - #{schematic}`
|
list = `gnetlist -g bom2 -O attribs=#{attributes*','} -q -o - #{schematic}`
|
||||||
list.encode!("UTF-8","ISO-8859-1")
|
list.encode!("UTF-8","ISO-8859-1")
|
||||||
list.gsub!(/(\d[Mkmµ]?)\?/,'\1Ω') # UTF-8 characters like Ω are replaced with ? by gnetlist
|
list.gsub!(/(\d[Mkmµ]?)\?/,'\1Ω') # UTF-8 characters like Ω are replaced with ? by gnetlist
|
||||||
list.gsub!(/(https?:\/\/[^:]*):/,'"\1":') # ':' (like in links) are not protected
|
list.gsub!(/(https?:\/\/[^:]*):/,'"\1":') # ':' (like in links) are not protected
|
||||||
|
@ -168,134 +173,104 @@ end
|
||||||
# return actual USD $ to EUR € rate
|
# return actual USD $ to EUR € rate
|
||||||
def usd2eur_rate
|
def usd2eur_rate
|
||||||
unless $u2e then
|
unless $u2e then
|
||||||
rate_http = Net::HTTP.get_response(URI.parse("https://rate-exchange.appspot.com/currency?from=USD&to=EUR"))
|
rate_http = Net::HTTP.get_response(URI.parse("https://api.fixer.io/latest?base=USD"))
|
||||||
rate_json = JSON.parse(rate_http.body)
|
rate_json = JSON.parse(rate_http.body)
|
||||||
$u2e = rate_json["rate"].to_f
|
$u2e = rate_json["rates"]["EUR"].to_f
|
||||||
end
|
end
|
||||||
return $u2e
|
return $u2e
|
||||||
end
|
end
|
||||||
|
|
||||||
# return part price
|
# get part prices (and stock) from octopart
|
||||||
def octopart(seller,sku)
|
# octopart provides a nice API (requiring a key) and prices for numerous distributors, but the result aren't always accurate
|
||||||
seller = {'digikey-id'=>"Digi-Key", 'farnell-id'=>"Farnell", 'mouser-id'=>"Mouser"}[seller]
|
# provide either the ocotopart_id, or manufacturer_name and manufacturer_part, or distributor_name and distributor_sku
|
||||||
return nil unless seller
|
APIKEY_FILE = "octopart.apikey"
|
||||||
to_return = {stock: nil, currency: nil, prices: []}
|
def octopart_prices(octopart_id, manufacturer_name, manufacturer_part, distributor_name, distributor_sku)
|
||||||
# octopart API key to get cost estimations
|
to_return = nil
|
||||||
unless $octopart_api_key then
|
# get API key
|
||||||
raise "provide octopart API key in 'octopart' file to get cost estimations" unless File.exist? "octopart"
|
raise "octopart API key required in #{APIKEY_FILE}" unless File.file? APIKEY_FILE
|
||||||
key = IO.read("octopart").lines[0]
|
apikey = IO.read(APIKEY_FILE).lines[0]
|
||||||
raise "provide octopart API key in 'octopart' file to get cost estimations" if key.empty?
|
raise "octopart API key required in #{APIKEY_FILE}" if apikey.empty?
|
||||||
$octopart_api_key = key
|
# get part information
|
||||||
end
|
url = "https://octopart.com/api/v3/parts/match?&apikey=#{apikey}&queries="
|
||||||
# query octopart
|
to_return = if octopart_id then
|
||||||
query = [{:seller => seller, :sku => sku}]
|
url = "https://octopart.com/api/v3/parts/#{octopart_id}?apikey=#{apikey}"
|
||||||
url = 'http://octopart.com/api/v3/parts/match?'
|
resp = Net::HTTP.get_response(URI.parse(url))
|
||||||
url += 'queries=' + URI.encode(JSON.generate(query))
|
JSON.parse(resp.body)
|
||||||
url += '&apikey=' + $octopart_api_key
|
elsif manufacturer_name and manufacturer_part then
|
||||||
resp = Net::HTTP.get_response(URI.parse(url))
|
url = "https://octopart.com/api/v3/parts/match?&apikey=#{apikey}&queries="+URI.encode(JSON.generate([{:brand => manufacturer_name, :mpn => manufacturer_part}]))
|
||||||
octo_info = JSON.parse(resp.body)
|
resp = Net::HTTP.get_response(URI.parse(url))
|
||||||
# get the right response
|
json = JSON.parse(resp.body)
|
||||||
octo_info['results'].each do |result|
|
if json["results"].empty? or json["results"][0]["items"].empty? then
|
||||||
result['items'].each do |item|
|
nil
|
||||||
item['offers'].each do |offer|
|
else
|
||||||
next unless offer['seller']['name']==seller
|
json["results"][0]["items"][0]
|
||||||
next unless offer['sku']==sku
|
end
|
||||||
to_return[:stock] = offer['in_stock_quantity']
|
elsif distributor_name and distributor_sku then
|
||||||
offer['prices'].each do |currency,prices|
|
url = "https://octopart.com/api/v3/parts/match?&apikey=#{apikey}&queries="+URI.encode(JSON.generate([{:seller => distributor_name, :sku => distributor_sku}]))
|
||||||
next unless currency=="USD" or currency=="EUR"
|
resp = Net::HTTP.get_response(URI.parse(url))
|
||||||
next if to_return[:currency]=="EUR"
|
json = JSON.parse(resp.body)
|
||||||
to_return[:currency] = currency
|
if json["results"].empty? or json["results"][0]["items"].empty? then
|
||||||
prices.each do |price|
|
nil
|
||||||
price[1] = price[1].to_f
|
else
|
||||||
end
|
json["results"][0]["items"][0]
|
||||||
to_return[:prices] = prices
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
return to_return
|
|
||||||
end
|
|
||||||
|
|
||||||
DIGIKEY_URL = "http://www.digikey.de/product-detail/en/all/"
|
|
||||||
def scrape_digikey(sku)
|
|
||||||
to_return = {stock: nil, currency: "EUR", prices: []}
|
|
||||||
# get page
|
|
||||||
url = DIGIKEY_URL+sku
|
|
||||||
doc = Nokogiri::HTML(open(URI.escape(url)))
|
|
||||||
# get stock
|
|
||||||
stock_doc = doc.xpath('//td[@id="quantityavailable"]')[0]
|
|
||||||
to_return[:stock] = stock_doc.text.gsub(/[ ,]+/,"").scan(/\d+/)[0].to_i
|
|
||||||
# get prices
|
|
||||||
doc.xpath('//table[@id="pricing"]/tr').each do |row|
|
|
||||||
next unless (col=row.xpath('td')).size==3
|
|
||||||
qty = col[0].text.gsub(/[^\d]+/,"")
|
|
||||||
qty = (qty =~ /^\d+$/ ? qty.to_i : nil)
|
|
||||||
price = col[1].text.gsub(/[^\d\.]+/,"")
|
|
||||||
price = (price =~ /^\d+(\.\d+)?$/ ? price.to_f : nil)
|
|
||||||
to_return[:prices] << [qty,price] if qty and price
|
|
||||||
end
|
|
||||||
return to_return
|
|
||||||
end
|
|
||||||
|
|
||||||
FARNELL_URL = "http://de.farnell.com/"
|
|
||||||
def scrape_farnell(sku)
|
|
||||||
to_return = {stock: nil, currency: "EUR", prices: []}
|
|
||||||
# get page
|
|
||||||
url = FARNELL_URL+sku
|
|
||||||
doc = Nokogiri::HTML(open(URI.escape(url)))
|
|
||||||
# get stock
|
|
||||||
stock_doc = doc.xpath('//td[@class="prodDetailAvailability"]')[0]
|
|
||||||
if stock_doc then
|
|
||||||
to_return[:stock] = stock_doc.text.lines.to_a[-1].to_i
|
|
||||||
else # when several stocks are available
|
|
||||||
stock_doc = doc.xpath('//div[@class="stockDetail"]')[0]
|
|
||||||
to_return[:stock] = stock_doc.text.gsub(".","").scan(/\d+/)[-1].to_i # the last match should be for EU
|
|
||||||
end
|
|
||||||
# get prices
|
|
||||||
doc.xpath('//div[@class="price"]/*/tr').each do |row|
|
|
||||||
next unless row.xpath('td').size==2
|
|
||||||
qty = row.xpath('td')[0].text.split("-")[0].gsub(/[^\d]+/,"")
|
|
||||||
qty = (qty =~ /^\d+$/ ? qty.to_i : nil)
|
|
||||||
price = row.xpath('td')[1].text.gsub(",",".").gsub(/[^\d\.]+/,"")
|
|
||||||
price = (price =~ /^\d+(\.\d+)?$/ ? price.to_f : nil)
|
|
||||||
to_return[:prices] << [qty,price] if qty and price
|
|
||||||
end
|
|
||||||
return to_return
|
|
||||||
end
|
|
||||||
|
|
||||||
MOUSER_URL = "http://www.mouser.com/Search/ProductDetail.aspx?R=0virtualkey0virtualkey"
|
|
||||||
def scrape_mouser(sku)
|
|
||||||
to_return = {stock: nil, currency: "EUR", prices: []}
|
|
||||||
# get page
|
|
||||||
url = MOUSER_URL+sku
|
|
||||||
doc = Nokogiri::HTML(open(URI.escape(url),:allow_redirections => :all))
|
|
||||||
# get stock
|
|
||||||
stock_doc = doc.xpath('//table[contains(@id,"availability")]/tr/td')[0]
|
|
||||||
to_return[:stock] = stock_doc.text.gsub(".","").to_i
|
|
||||||
# get prices
|
|
||||||
doc.xpath('//table[@class="PriceBreaks"]/tr').each do |row|
|
|
||||||
qty_doc = row.xpath('td[@class="PriceBreakQuantity"]')
|
|
||||||
qty = qty_doc[0].text.gsub(/[\s:\.]+/,"").to_i unless qty_doc.empty?
|
|
||||||
price_doc = row.xpath('td[@class="PriceBreakPrice"]')
|
|
||||||
price = price_doc[0].text.gsub(/\s+/,"").gsub("€","").gsub(",",".").to_f unless price_doc.empty?
|
|
||||||
price = nil if price==0
|
|
||||||
to_return[:prices] << [qty,price] if qty and price
|
|
||||||
end
|
|
||||||
return to_return
|
|
||||||
end
|
|
||||||
|
|
||||||
def scrape_prices(seller,sku)
|
|
||||||
return case seller
|
|
||||||
when 'digikey-id'
|
|
||||||
scrape_digikey(sku)
|
|
||||||
when 'farnell-id'
|
|
||||||
scrape_farnell(sku)
|
|
||||||
when 'mouser-id'
|
|
||||||
scrape_mouser(sku)
|
|
||||||
else
|
else
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
end
|
sleep 0.33 if to_return # 3 queries/second rate limiting
|
||||||
|
return to_return
|
||||||
|
end
|
||||||
|
|
||||||
|
# get prices and stock for part from Digi-Key using the Digi-Key part number
|
||||||
|
def scrape_digikey(sku)
|
||||||
|
to_return = {stock: nil, currency: "EUR", prices: []}
|
||||||
|
# get page
|
||||||
|
url = "https://www.digikey.de/product-detail/en/all/"+sku
|
||||||
|
doc = Nokogiri::HTML(open(URI.escape(url)))
|
||||||
|
return nil if doc.xpath('//td[@id="quantityAvailable"]').empty? or doc.xpath('//table[@id="product-dollars"]').empty?
|
||||||
|
# get stock
|
||||||
|
stock_doc = doc.xpath('//td[@id="quantityAvailable"]/span[@id="dkQty"]')
|
||||||
|
to_return[:stock] = (stock_doc.empty? ? 0 : stock_doc.text.gsub('.','').to_i)
|
||||||
|
# get prices
|
||||||
|
doc.xpath('//table[@id="product-dollars"]/tr').each do |row|
|
||||||
|
next unless (col=row.xpath('td')).size==3
|
||||||
|
qty = col[0].text.gsub(/[^\d]+/,"")
|
||||||
|
qty = (qty =~ /^\d+$/ ? qty.to_i : nil)
|
||||||
|
price = col[1].text.gsub(/[^\d\.,]+/,"").gsub(".","").gsub(",",".")
|
||||||
|
price = (price =~ /^\d+(\.\d+)?$/ ? price.to_f : nil)
|
||||||
|
to_return[:prices] << [qty,price] if qty and price
|
||||||
|
end
|
||||||
|
|
||||||
|
return to_return
|
||||||
|
end
|
||||||
|
|
||||||
|
# get prices from AliExpress using SKU
|
||||||
|
def scrape_aliexpress(sku)
|
||||||
|
to_return = {stock: nil, currency: nil, quantity: nil, price: nil} # information to return (lot price, unit quantity, unit stock)
|
||||||
|
# get page
|
||||||
|
url = "https://www.aliexpress.com/item//#{sku}.html"
|
||||||
|
doc = Nokogiri::HTML(open(URI.escape(url),:allow_redirections => :all))
|
||||||
|
# all the values can be found in javascript variables (stock in even only there)
|
||||||
|
js_doc = doc.xpath('//div[@id="j-product-detail-bd"]//script[@type="text/javascript"]')
|
||||||
|
return nil if js_doc.empty?
|
||||||
|
js_text = js_doc[0].text
|
||||||
|
# get currency
|
||||||
|
return nil unless js_text.include?('window.runParams.currencyCode="')
|
||||||
|
to_return[:currency] = js_text.split('window.runParams.currencyCode="')[1].split('";')[0]
|
||||||
|
# get other values
|
||||||
|
return nil unless js_text.include?("var skuProducts=")
|
||||||
|
js_json = JSON.parse(js_text.split("var skuProducts=")[1].split(";")[0])[0]
|
||||||
|
to_return[:price] = js_json["skuVal"]["skuMultiCurrencyCalPrice"].to_f
|
||||||
|
if (js_json["skuVal"]["skuMultiCurrencyPerPiecePrice"]) then
|
||||||
|
to_return[:quantity] = (to_return[:price]/js_json["skuVal"]["skuMultiCurrencyPerPiecePrice"].to_f).round
|
||||||
|
else
|
||||||
|
to_return[:quantity] = 1
|
||||||
|
end
|
||||||
|
to_return[:stock] = (js_json["skuVal"]["availQuantity"]*to_return[:quantity]).to_i
|
||||||
|
to_return[:prices] = [[to_return[:quantity], to_return[:price]]]
|
||||||
|
|
||||||
|
return to_return
|
||||||
|
end
|
||||||
|
|
||||||
# ===============
|
# ===============
|
||||||
# file generation
|
# file generation
|
||||||
|
@ -307,7 +282,7 @@ targets.each do |target|
|
||||||
# embed symbols
|
# embed symbols
|
||||||
sh "cp #{t.prerequisites.join(' ')} #{t.name}"
|
sh "cp #{t.prerequisites.join(' ')} #{t.name}"
|
||||||
sh "gschlas -e #{t.name}"
|
sh "gschlas -e #{t.name}"
|
||||||
# on \ is to prevent ruby interpreting it, th other is for sed
|
# on \ is to prevent ruby interpreting it, the other is for sed
|
||||||
# the version
|
# the version
|
||||||
sh "sed --in-place 's/\\(version=\\)\\$Version\\$/\\1#{version}/' #{t.name}"
|
sh "sed --in-place 's/\\(version=\\)\\$Version\\$/\\1#{version}/' #{t.name}"
|
||||||
# the date
|
# the date
|
||||||
|
@ -321,7 +296,7 @@ desc "copy layout to include complete version (version, date, and run teardrops
|
||||||
targets.each do |target|
|
targets.each do |target|
|
||||||
file target[:vpcb] => target[:pcb] do |t|
|
file target[:vpcb] => target[:pcb] do |t|
|
||||||
sh "cp #{t.prerequisites.join(' ')} #{t.name}"
|
sh "cp #{t.prerequisites.join(' ')} #{t.name}"
|
||||||
# on \ is to prevent ruby interpreting it, th other is for sed
|
# one \ is to prevent ruby interpreting it, the other is for sed
|
||||||
# the version and revision
|
# the version and revision
|
||||||
version_revision = "v#{version}.#{target[:pcb_rev].to_s.rjust(3,'0')}"
|
version_revision = "v#{version}.#{target[:pcb_rev].to_s.rjust(3,'0')}"
|
||||||
sh "sed -i 's/\\$version\\$/#{version_revision}/' #{t.name}"
|
sh "sed -i 's/\\$version\\$/#{version_revision}/' #{t.name}"
|
||||||
|
@ -367,7 +342,7 @@ end
|
||||||
desc "generate BOM file from schematic"
|
desc "generate BOM file from schematic"
|
||||||
targets.each do |target|
|
targets.each do |target|
|
||||||
file "#{target[:name]}_bom.csv" => target[:sch] do |t|
|
file "#{target[:name]}_bom.csv" => target[:sch] do |t|
|
||||||
attributes = ["category","device","value","description","manufacturer","manufacturer-id","datasheet","digikey-id","farnell-id","mouser-id"]
|
attributes = ["category","device","value","description","manufacturer","manufacturer-id","datasheet","digikey-id","farnell-id","mouser-id","aliexpress-id","alternatives"]
|
||||||
bom_data = bom2(t.prerequisites[0],attributes)
|
bom_data = bom2(t.prerequisites[0],attributes)
|
||||||
CSV.open(t.name, "wb") do |csv|
|
CSV.open(t.name, "wb") do |csv|
|
||||||
all_attributes = ["refdes","qty"]+attributes
|
all_attributes = ["refdes","qty"]+attributes
|
||||||
|
@ -381,76 +356,228 @@ end
|
||||||
|
|
||||||
desc "generate cost estimation from schematic"
|
desc "generate cost estimation from schematic"
|
||||||
targets.each do |target|
|
targets.each do |target|
|
||||||
|
# this version uses Digi-Key and AliExpress
|
||||||
|
# Digi-Key is easily scrapable, while Mouser isn't
|
||||||
|
# Digi-Key is only one distributor, but the end prices across distributor is often similar
|
||||||
file "#{target[:name]}_cost.csv" => target[:sch] do |t|
|
file "#{target[:name]}_cost.csv" => target[:sch] do |t|
|
||||||
sellers = ['digikey-id','farnell-id','mouser-id'] # provide the (supported) seller SKU as value to this attribute name in the symbols/schematic
|
sellers = ['digikey-id','aliexpress-id'] # get seller SKU
|
||||||
boards = [1,10] # calculate the price for as many boards
|
boards = [1,10,100] # calculate the price for as many boards
|
||||||
sum = Array.new(sellers.size){Array.new(boards.size,0.0)} # total cost
|
total_price = Array.new(sellers.size){Array.new(boards.size, 0.0)} # total price for x boards
|
||||||
stocks = Array.new(sellers.size){Array.new(boards.size){[]}} # is there enough stock
|
unit_price = Array.new(sellers.size){Array.new(boards.size, 0.0)} # unit price for 1 board
|
||||||
# get component information
|
# get component information
|
||||||
attributes = ["manufacturer","manufacturer-id"]+sellers
|
attributes = ["value","manufacturer","manufacturer-id"]+sellers # BOM fields to get
|
||||||
parts = bom2(t.prerequisites[0],attributes)
|
parts = bom2(t.prerequisites[0],attributes) # get field values
|
||||||
parts.collect!{|part| part['manufacturer'] and part['manufacturer-id'] ? part : nil}
|
|
||||||
parts.compact!
|
|
||||||
# put result in CVS
|
# put result in CVS
|
||||||
CSV.open(t.name, "wb") do |csv|
|
CSV.open(t.name, "wb") do |csv|
|
||||||
csv << ["refdes","quantity","manufacturer","part number"]+(sellers.collect{|seller| [seller,"stock","currency"]+boards.collect{|qty| ["unit price for #{qty} board(s)","total price for #{qty} board(s)"]}}).flatten
|
csv << ["refdes","quantity","manufacturer","part number"]+(sellers.collect{|seller| [seller,"stock"]+boards.collect{|qty| ["unit price for #{qty} board(s)","total price for #{qty} board(s)"]}}).flatten
|
||||||
parts.each do |part|
|
parts.each do |part|
|
||||||
part['qty'] = part['qty'].to_i
|
part['qty'] = part['qty'].to_i # converted quantity from BOM string to integer for later calculation
|
||||||
line = [part['refdes'],part['qty'],part['manufacturer'],part['manufacturer-id']]
|
line = [part['refdes'],part['qty'],part['manufacturer'],part['manufacturer-id']] # start CSV line
|
||||||
sellers.each_index do |i|
|
sellers.each_index do |seller_i| # go through all seller
|
||||||
seller = sellers[i]
|
seller = sellers[seller_i] # current seller
|
||||||
if part[seller] then
|
if part[seller] and !part[seller].empty? then
|
||||||
begin
|
|
||||||
prices = scrape_prices(seller,part[seller])
|
|
||||||
rescue
|
|
||||||
$stderr.puts "#{part['manufacturer']} #{part['manufacturer-id']} not available using #{seller} #{part[seller]}"
|
|
||||||
line += [part[seller]]+[nil]*(2+boards.size*2)
|
|
||||||
next
|
|
||||||
end
|
|
||||||
line << part[seller]
|
line << part[seller]
|
||||||
line << prices[:stock]
|
price = case seller
|
||||||
line << prices[:currency]
|
when 'aliexpress-id'
|
||||||
unit = [] # the unit price
|
scrape_aliexpress(part[seller])
|
||||||
boards.each_index do |j|
|
when 'digikey-id'
|
||||||
if prices[:prices].empty? then
|
scrape_digikey(part[seller])
|
||||||
line += [nil]*2
|
else
|
||||||
else
|
nil
|
||||||
prices[:prices].each do |price|
|
end
|
||||||
unit[j] = price[1] if (!unit[j] or price[1]<unit[j]) and price[0]<=part['qty']*boards[j]
|
if price then
|
||||||
|
line << price[:stock]
|
||||||
|
boards.each_index do |boards_i|
|
||||||
|
quantity = boards[boards_i]
|
||||||
|
# find lowest price (considering the quantity and quantity prices)
|
||||||
|
unit = nil
|
||||||
|
total = nil
|
||||||
|
price[:prices].each do |p|
|
||||||
|
if !unit or !total then
|
||||||
|
unit = p[1].to_f
|
||||||
|
total = [quantity,p[0].to_i].max*unit
|
||||||
|
end
|
||||||
|
if [quantity,p[0].to_i].max*p[1].to_f<total then
|
||||||
|
unit = p[1].to_f
|
||||||
|
total = [quantity,p[0].to_i].max*unit
|
||||||
|
end
|
||||||
end
|
end
|
||||||
if unit[j] then
|
if "USD"==price[:currency] then
|
||||||
line << unit[j]
|
unit *= usd2eur_rate()
|
||||||
line << unit[j]*part['qty']*boards[j]
|
total *= usd2eur_rate()
|
||||||
stocks[i][j] << true if part['qty']*boards[j]<=prices[:stock]
|
|
||||||
sum[i][j] += unit[j]*part['qty']*boards[j]
|
|
||||||
|
|
||||||
else # use the minimum quantity
|
|
||||||
line << prices[:prices][0][1]
|
|
||||||
line << prices[:prices][0][1]*prices[:prices][0][0]
|
|
||||||
stocks[i][j] << true
|
|
||||||
sum[i][j] += prices[:prices][0][1]*prices[:prices][0][0]
|
|
||||||
end
|
end
|
||||||
|
line << unit
|
||||||
|
unit_price[seller_i][boards_i] += line[-1]
|
||||||
|
line << total
|
||||||
|
total_price[seller_i][boards_i] += line[-1]
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
line += [nil]*(1+boards.size*2)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
line += [nil]*(3+boards.size*2)
|
line += [nil]*(2+boards.size*2)
|
||||||
end
|
end # seller
|
||||||
end
|
end # sellers
|
||||||
csv << line
|
csv << line
|
||||||
end
|
end # parts
|
||||||
# summary
|
# summary
|
||||||
line = [nil]*4
|
line = [nil]*4
|
||||||
sellers.each_index do |i|
|
sellers.each_index do |seller_i|
|
||||||
line += [nil,nil,nil]
|
line += [nil,nil]
|
||||||
boards.each_index do |j|
|
boards.each_index do |boards_i|
|
||||||
line << (stocks[i][j].size==parts.size ? "" : "not ")+"enough stock"
|
line << unit_price[seller_i][boards_i]
|
||||||
line << sum[i][j]
|
line << total_price[seller_i][boards_i]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
csv << line
|
csv << line
|
||||||
end
|
# details
|
||||||
end
|
csv << []
|
||||||
end
|
csv << ["all prices and stocks have been retrieved from Digi-Key and AliExpress on #{Time.now.to_s}"]
|
||||||
|
csv << ["all prices are in EUR. prices originally in USD have been converted at a rate of #{usd2eur_rate}"]
|
||||||
|
end # CSV file
|
||||||
|
end # end cost file
|
||||||
|
end # end target file
|
||||||
|
|
||||||
|
desc "generate cost comparison from schematic"
|
||||||
|
targets.each do |target|
|
||||||
|
# this version uses octopart and AliExpress
|
||||||
|
# octopart has the advantage to have and API and provides prices for numerous distributors
|
||||||
|
# the disadvantage is that the octopart information are not always accurate
|
||||||
|
file "#{target[:name]}_costs.csv" => target[:sch] do |t|
|
||||||
|
sellers = ['digikey-id','farnell-id','mouser-id','aliexpress-id'] # get seller SKU
|
||||||
|
SELLER_ID = {'digikey-id' => 459, 'farnell-id' => 819, 'mouser-id' => 2401} # octopart seller IDs
|
||||||
|
boards = [1,10,100] # calculate the price for as many boards
|
||||||
|
sum = Array.new(sellers.size){Array.new(boards.size, 0.0)} # total cost
|
||||||
|
stocks = Array.new(sellers.size){Array.new(boards.size, true)} # is there enough stock
|
||||||
|
# get component information
|
||||||
|
attributes = ["value","manufacturer","manufacturer-id","octopart-id"]+sellers # BOM fields to get
|
||||||
|
parts = bom2(t.prerequisites[0],attributes) # get field values
|
||||||
|
# put result in CVS
|
||||||
|
CSV.open(t.name, "wb") do |csv|
|
||||||
|
csv << ["refdes","quantity","manufacturer","part number"]+(sellers.collect{|seller| [seller,"stock"]+boards.collect{|qty| ["unit price for #{qty} board(s)","total price for #{qty} board(s)"]}}).flatten
|
||||||
|
parts.each do |part|
|
||||||
|
part['qty'] = part['qty'].to_i # converted quantity from BOM string to integer for later calculation
|
||||||
|
line = [part['refdes'],part['qty'],part['manufacturer'],part['manufacturer-id']] # start CSV line
|
||||||
|
# get prices
|
||||||
|
prices = nil
|
||||||
|
if part['octopart-id'] and !part['octopart-id'].empty? then
|
||||||
|
prices = octopart_prices(part['octopart-id'], nil, nil, nil, nil)
|
||||||
|
elsif part['manufacturer'] and !part['manufacturer'].empty? and part['manufacturer-id'] and !part['manufacturer-id'].empty?
|
||||||
|
prices = octopart_prices(nil, part['manufacturer'], part['manufacturer-id'], nil, nil)
|
||||||
|
end
|
||||||
|
# get prices per seller
|
||||||
|
sellers.each_index do |seller_i|
|
||||||
|
seller = sellers[seller_i] # current seller
|
||||||
|
if 'aliexpress-id'==seller then # AliExpress is an exception since it's not in octopart
|
||||||
|
if part[seller] and !part[seller].empty? then # AliExpress link provided
|
||||||
|
price = scrape_aliexpress(part[seller])
|
||||||
|
if price then
|
||||||
|
line << part[seller]
|
||||||
|
line << price[:stock]
|
||||||
|
boards.each_index do |boards_i|
|
||||||
|
quantity = boards[boards_i]
|
||||||
|
line << price[:price]
|
||||||
|
if quantity<price[:quantity] then
|
||||||
|
line << price[:price]*usd2eur_rate()
|
||||||
|
else
|
||||||
|
line << price[:price]*usd2eur_rate()*(quantity/price[:quantity]).ceil
|
||||||
|
end
|
||||||
|
sum[seller_i][boards_i] += line[-1]
|
||||||
|
stocks[seller_i][boards_i] &= (quantity<=price[:stock])
|
||||||
|
end
|
||||||
|
else # no prices available
|
||||||
|
if part['manufacturer'] and part['manufacturer-id'] then
|
||||||
|
$stderr.print "#{part['manufacturer']} #{part['manufacturer-id']} "
|
||||||
|
else
|
||||||
|
$stderr.print "part "
|
||||||
|
end
|
||||||
|
$stderr.puts "not available using #{seller} #{part[seller]}"
|
||||||
|
line += [nil]*(2+boards.size*2)
|
||||||
|
end
|
||||||
|
else # no aliexpress-id provided
|
||||||
|
line += [nil]*(2+boards.size*2)
|
||||||
|
end
|
||||||
|
else # not AliExpress
|
||||||
|
price = nil # prices from this seller
|
||||||
|
if prices and prices["offers"] and !prices["offers"].empty? then # verify if we already have this seller in our price list
|
||||||
|
prices["offers"].each do |offer|
|
||||||
|
price = offer if !price and offer["seller"]["id"].to_i==SELLER_ID[seller] and (!offer["moq"] or offer["moq"].to_i==1)
|
||||||
|
end
|
||||||
|
price = nil if price and part[seller] and !part[seller].empty? and price["sku"]!=part[seller] # check if distributor part number matches
|
||||||
|
end
|
||||||
|
if !price and part[seller] and !part[seller].empty? then # get prices using distributor part number if not already found
|
||||||
|
distributor_prices = octopart_prices(nil, nil, nil, seller.split("-")[0], part[seller])
|
||||||
|
if distributor_prices and distributor_prices["offers"] and !distributor_prices["offers"].empty? then
|
||||||
|
distributor_prices["offers"].each do |offer|
|
||||||
|
price = offer if !price and offer["seller"]["id"].to_i==SELLER_ID[seller] and (!offer["moq"] or offer["moq"].to_i==1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if price then # display price
|
||||||
|
line << part[seller]
|
||||||
|
line << price["in_stock_quantity"]
|
||||||
|
boards.each_index do |boards_i|
|
||||||
|
quantity = boards[boards_i]
|
||||||
|
# find lowest price (considering the quantity and quantity prices)
|
||||||
|
unit_price = nil
|
||||||
|
total_price = nil
|
||||||
|
(price["prices"]["EUR"]||[]).each do |p|
|
||||||
|
if !unit_price or !total_price then
|
||||||
|
unit_price = p[1].to_f
|
||||||
|
total_price = [quantity,p[0].to_i].max*unit_price
|
||||||
|
end
|
||||||
|
if [quantity,p[0].to_i].max*p[1].to_f<total_price then
|
||||||
|
unit_price = p[1].to_f
|
||||||
|
total_price = [quantity,p[0].to_i].max*unit_price
|
||||||
|
end
|
||||||
|
end
|
||||||
|
(price["prices"]["USD"]||[]).each do |p|
|
||||||
|
if !unit_price or !total_price then
|
||||||
|
unit_price = p[1].to_f*usd2eur_rate()
|
||||||
|
total_price = [quantity,p[0].to_i].max*unit_price
|
||||||
|
end
|
||||||
|
if [quantity,p[0].to_i].max*p[1].to_f<total_price then
|
||||||
|
unit_price = p[1].to_f*usd2eur_rate()
|
||||||
|
total_price = [quantity,p[0].to_i].max*unit_price
|
||||||
|
end
|
||||||
|
end
|
||||||
|
line << unit_price
|
||||||
|
line << total_price
|
||||||
|
sum[seller_i][boards_i] += line[-1]
|
||||||
|
stocks[seller_i][boards_i] &= (quantity<=price["in_stock_quantity"])
|
||||||
|
end
|
||||||
|
else # no price available
|
||||||
|
if part[seller] and !part[seller].empty? then
|
||||||
|
if part['manufacturer'] and part['manufacturer-id'] then
|
||||||
|
$stderr.print "#{part['manufacturer']} #{part['manufacturer-id']} "
|
||||||
|
else
|
||||||
|
$stderr.print "part "
|
||||||
|
end
|
||||||
|
$stderr.puts "not available using #{seller} #{part[seller]}"
|
||||||
|
end
|
||||||
|
line += [nil]*(2+boards.size*2)
|
||||||
|
end
|
||||||
|
end # seller
|
||||||
|
end # sellers
|
||||||
|
csv << line
|
||||||
|
end # parts
|
||||||
|
# summary
|
||||||
|
line = [nil]*4
|
||||||
|
sellers.each_index do |seller_i|
|
||||||
|
line += [nil,nil]
|
||||||
|
boards.each_index do |boards_i|
|
||||||
|
line << (stocks[seller_i][boards_i] ? "" : "not ")+"enough stock"
|
||||||
|
line << sum[seller_i][boards_i]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
csv << line
|
||||||
|
# details
|
||||||
|
csv << []
|
||||||
|
csv << ["all prices and stocks have been retrieved from octopart and AliExpress on #{Time.now.to_s}"]
|
||||||
|
csv << ["all prices are in EUR. prices originally in USD have been converted at a rate of #{usd2eur_rate}"]
|
||||||
|
end # CSV file
|
||||||
|
end # end cost file
|
||||||
|
end # end target file
|
||||||
|
|
||||||
desc "generate photo realistic picture from layout (front side)"
|
desc "generate photo realistic picture from layout (front side)"
|
||||||
targets.each do |target|
|
targets.each do |target|
|
||||||
|
|
Loading…
Reference in New Issue