README.md in ba_upload-0.3.0 vs README.md in ba_upload-0.4.0

- old
+ new

@@ -31,11 +31,12 @@ # your supplied certificate and passphrase connection = BaUpload.open_connection(file_path: 'config/Zertifikat-1XXXX.p12', passphrase: 'YOURPASSPHRASE') # Upload a xml-file -connection.upload(file: File.open('/opt/vam-transfer/data/DSP000132700_2016-08-08_05-00-09.xml')) +file_path = "/opt/vam-transfer/data/DSP000132700_2016-08-08_05-00-09.xml" +connection.upload(file: file_path)) # later cronjob to download all error files connection.error_files.each do |error_file| target_path = "/opt/vam-transfer/data/#{error_file.filename}" @@ -84,9 +85,191 @@ connection.upload(file: 'your_file_path', partner_id: 'P000XXXXXX') connection.error_files(partner_id: 'P000XXXXXX') connection.misc(partner_id: 'P000XXXXXX') ``` + +## Appendix: Berufe + +Sooner or later, you have to provide a TitleCode = Vocation "Beruf" for each job. To fetch and process the Berufe, we create a ActiveRecord Model in our database: + +Here an example of a implementation at Empfehlungsbund. You can also use our [search mask](https://login.empfehlungsbund.de/arbeitsagentur) to search for occupations. + +We put the "help" / "validation" messages, that we found in the appropriate scopes, too, as "Ausbildungen" and "Duale Studiengänge" need different types of professions. + +<details> +<summary>ActiveRecord Model for Ba::Profession</summary> + +```ruby +# migration: +create_table :ba_professions do |t| + t.string "bkz" + t.string "typ" + t.string "lbkgruppe" + t.string "hochschulberuf" + t.string "kuenstler" + t.string "bezeichnung_nl" + t.string "bezeichnung_nk" + t.string "suchname_nl" + t.datetime "created_at" + t.datetime "updated_at" + t.integer "ebene" + t.integer "qualifikationsniveau" + t.datetime "deleted_on" +end + +class Ba::Profession < ApplicationRecord + has_many :jobs + + scope :undeleted, -> { where 'deleted_on is null' } + scope :berufe, -> { where typ: 'B' } + scope :ausbildungen, -> { where typ: 'A' } + scope :sorted, -> { order(Arel.sql('deleted_on is not null, bezeichnung_nl')) } + # Bei Auswahl von „Ausbildung“ (EducationType=0) sind die Berufe mit dem + # Qualifikationsniveau 2 zulässig. Zusätzlich sind hier alle Berufe folgender + # berufskundlicher Gruppen erlaubt: [...] + scope :reine_ausbildungen, -> { + where(qualifikationsniveau: 2).or( + where(lbkgruppe: [1150, 3110, 5130]) + ).ausbildungen + } + # Wird ein Stellenangebot vom Typ „Duales Studium“ (EducationType=1) übermittelt, sind der + # Studiengang und der ggf. vorhandene Ausbildungsberuf getrennt anzugeben. Als + # Studiengang (Course) sind Berufe mit ausschließlich dem Qualifikationsniveau 4 zulässig. + # Diese Berufe entstammen alle der berufskundlichen Gruppe 3120 („A Grundständige + # Studienfächer/-gänge“). Der als Ausbildung (TitleCode) angegebene Beruf darf + # dementsprechend nicht ausschließlich das Qualifikationsniveau 4 haben. + scope :duale_studiengaenge, -> { ausbildungen.where ebene: 3, qualifikationsniveau: 4 } + + def duales_studium? + ebene == 3 && qualifikationsniveau == 4 && typ == 'A' + end + + def self.download_from_ba + require 'tty/prompt' + prompt = TTY::Prompt.new + link = Ba::Distributor.ba_connection.misc.last do |link| + link.click + target = "public/ba/#{link.href}" + response = link.click + File.open(target, "wb+") { |f| f.write(response.body) } + + puts "Unzipping vam_beruf_kurz.xml..." + `unzip -o -d public/ba/ #{target} vam_beruf_kurz.xml` + end + + def self.import(path: 'public/ba/vam_beruf_kurz.xml') + doc = Nokogiri::XML.parse(File.open(path)) + berufe_vorher = Ba::Beruf.undeleted.pluck(:id) + doc.search('beruf').each do |beruf_doc| + beruf = where(id: beruf_doc['id']).first_or_initialize + + beruf.bkz = beruf_doc['bkz'] + + beruf.typ = beruf_doc.at('typ').text == 't' ? 'B' : 'A' + beruf.qualifikationsniveau = beruf_doc.at('qualifikationsNiveau[niveau]')['niveau'] + beruf_doc.search(*%w[lbkgruppe hochschulberuf ebene kuenstler bezeichnung_nl bezeichnung_nk suchname_nl]).each do |i| + beruf.send("#{i.name}=", i.text) + end + beruf.save + berufe_vorher.delete(beruf.id) + end + Ba::Beruf.where(id: berufe_vorher).update_all deleted_on: Time.zone.now if berufe_vorher.any? + end + scope :duale_studiengaenge, -> { where ebene: 3, qualifikationsniveau: 4 } + + def display_name + prefix = if deleted_on? + "[!VERALTET!] " + end + if typ == 'A' + if ebene == 3 && qualifikationsniveau == 4 + "#{prefix}#{bezeichnung_nk} (DUALES STUDIUM/praxisorientiert)" + else + "#{prefix}#{bezeichnung_nk} (AUSBILDUNG)" + end + else + "#{prefix}#{bezeichnung_nk}" + end + end + + def as_json(opts = {}) + { + id: id, + display_name: display_name + } + end +``` + +</details> + +## Appendix: How to construct a Job-Posting XML file to upload + +- Download the most recent JobPosting xsd from https://baxml.arbeitsagentur.de/geschuetzt/download/ +- You can visualize the xsd here: http://www.xml-tools.net/schemaviewer.html +- Now, you can construct the file with xml-builder: + +<details> +<summary>Example for constructing a feed using XmlBuilder</summary> + +```ruby + xml = Builder::XmlMarkup.new(indent: 1) + xml.instruct! + xml.tag!("HRBAXMLJobPositionPosting") do + xml.tag!("Header") do + xml.tag!("SupplierId", SUPPLIER_ID) + xml.tag!("Timestamp", Time.zone.now.to_s(:db).tr(" ", "T")) + xml.tag!("Amount", obs.count) + # F: Full + # D: Diff + if @only_jobs + xml.tag!("TypeOfLoad", "D") + else + xml.tag!("TypeOfLoad", "F") + end + end + xml.tag!("Data") do + jobs.each do |job| + generate_xml_for_job(xml, job) + end + + jobs_to_delete.each do |job| + xml.tag! "DeleteEntry" do + xml.tag! "EntryId", id + end + end + end + end + xml +``` +</details> + +- Then, you should validate your feed: + +```ruby +xsd = Nokogiri::XML::Schema(File.open("vendor/ba/HRBAXML_JobPosition_Current.xsd")) +doc = Nokogiri::XML(xml.to_s) +xsd.validate(doc) +``` + +- Then, you can put that into a file - so you will need to generate a filename **according to the spec**: + +<details> +<summary>Generate a filename</summary> +```ruby +# for historic reasons, you could transmit a bunch of files with the same timestamp using an index/offset, but usually, just putting 0 here should be enought +index = 0 +number_of_feeds_to_push_now = 1 +ended = index == (number_of_feeds_to_push_now - 1) +flag = ended ? "E" : "C" +date = Time.zone.now.strftime "%Y-%m-%d_%H-%M-%S_F#{'%03d' % (index + 1)}#{flag}" +"DS#{SUPPLIER_ID}_#{date}.xml" +``` +</details> + +- Upload the file using this Gem. You should wait a "couple of minutes" (tip: enqueue a activeJob for 10 minutes later), to fetch the resulting **error file**, and analyse that. + + ## License The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).