17、费用报销混搭插件开发指南

费用报销混搭插件开发

费用报销混搭插件开发指南

在开发费用报销混搭应用时,我们会用到多个工具和技术,下面为你详细介绍整个开发过程。

1. 所需工具和库
  • PayPal Sandbox :这是一个独立的环境,开发者可以在其中对PayPal应用进行原型设计和测试。它几乎模拟了实际PayPal环境中的所有功能,我们将使用它来模拟工资和费用报销的混搭。
  • Google APIs :我们会使用四种不同的Google API,分别是Google账户认证API、Google数据API、Google文档列表数据API和Google电子表格数据API。
    • Google账户认证 :第三方应用可通过此API向Google API进行认证,有两种认证方式:
      • 使用ClientLogin API(主要用于桌面应用,需要访问用户的登录凭证)。
      • 使用AuthSub API(用于Web应用,无需用户的登录凭证)。在本次混搭中,我们使用ClientLogin API。
    • Google数据API :为读写Google服务的数据提供了简单、标准的协议,使用基于XML的Atom 1.0和RSS 2.0联合格式以及Atom发布协议。获取数据需发送HTTP Get请求,更新数据发送HTTP Put请求,删除数据发送HTTP Delete请求,返回的数据为RSS或Atom提要。
    • Google文档列表数据API :允许第三方应用通过Google数据API提要访问Google Docs中存储的文档,我们将用它在“approved - claims”文件夹中搜索电子表格,并将文字处理文档作为PDF文档检索。
    • Google电子表格数据API :允许第三方应用通过Google数据API提要查看和更新Google电子表格中的数据,我们会用它从费用报销电子表格中获取数据并填充到支付数据库中。
  • Ruby - PayPal库 :是围绕PayPal NVP API的轻量级包装库,为PayPal NVP API的输入提供基本验证支持,减少输入错误时的处理时间,还能对PayPal的响应进行解释,为Ruby开发者提供简单接口。安装命令如下:
$gem install ruby - paypal
  • Acts_as_state_machine插件 :虽不是本次混搭严格必需的,但我们会用它让Payment类表现为状态机,可减少编写模拟Payment对象状态变化的代码量。安装命令如下:
$./script/plugin install http://elitists.textdriven.com/svn/plugins/acts_as_state_machine/trunk/
  • XmlSimple :是一个Ruby API,可轻松读写XML格式的数据,是Perl模块XML::Simple的Ruby翻译版本,基于REXML(Ruby发行版中包含的XML解析器)编写。安装命令如下:
$gem install xml - simple
2. 开发步骤
  1. 创建Rails应用
$rails Chapter8

我们不会使用该应用的大部分界面,而是运行位于 $RAILS_ROOT/lib/tasks 文件夹中的rake脚本,且脚本运行应定期自动化。
2. 设置数据库
- 首先创建数据迁移脚本:

$./script/generate migration create_payments

生成的 001_create_payments.rb 文件内容如下:

class CreatePayments < ActiveRecord::Migration
  def self.up
    create_table :payments do |t|
      t.column 'type', :string, :default => 'salary' # either salary or claims
      t.column 'name', :string
      t.column 'description', :string
      t.column 'email', :string
      t.column 'amount', :float
      t.column 'state', :string, :default => 'pending' # states are pending, suspended and paid
      t.column 'expense_evidence', :binary, :limit => 10.megabytes
      t.column 'created_on', :datetime
      t.column 'updated_on', :datetime
    end
    create_table :claim_items do |t|
      t.column 'claim_id', :integer
      t.column 'expense_date', :date
      t.column 'project', :string
      t.column 'item', :string
      t.column 'remarks', :string
      t.column 'created_on', :datetime
      t.column 'amount', :float
    end
  end

  def self.down
    drop_table :payments
    drop_table :claim_items
  end
end
- 运行迁移命令创建表:
$rake db:migrate
  1. 创建Payment和Claim Item支架
$./script/generate scaffold Payment
$./script/generate scaffold ClaimItem
  1. 修改Payment并创建其子类
    修改 $RAILS_ROOT/app/models 文件夹中的 payment.rb 文件:
class Payment < ActiveRecord::Base
  acts_as_state_machine :initial => :pending
  state :pending, :enter => :add_to_account_payable
  state :paid, :enter => :log_payment
  state :suspended
  event :pay do
    transitions :from => :pending, :to => :paid
  end
  event :suspend do
    transitions :from => :pending, :to => :suspended
  end
  event :unsuspend do
    transitions :from => :suspended, :to => :pending
  end
  def add_to_account_payable
    # hook up with accounts system to record expense in account payable
  end
  def log_payment
    # hook up with accounts system to log actual payment
  end
end

创建 Salary Claim 子类:

class Salary < Payment
end

class Claim < Payment
  has_many :claim_items
end
3. 创建Google API访问库

$RAILS_ROOT/lib 文件夹中创建 gdata.rb 文件:

require 'net/http'
require 'net/https'
require 'open-uri'
require 'xmlsimple'
GOOGLE_CLIENT_LOGIN_URL = 'www.google.com/accounts/ClientLogin'
GOOGLE_DOCS_URL = 'docs.google.com'
GOOGLE_SPREADSHEETS_URL = 'spreadsheets.google.com'

SPREADSHEET_CATEGORY = {"term"=>"http://schemas.google.com/docs/2007#spreadsheet",
  "scheme"=>"http://schemas.google.com/g/2005#kind",
  "label"=>"spreadsheet"}

# convenience module to use HTTP (mainly used for login)
module Net
  class HTTPS < HTTP
    def initialize(address, port = nil)
      super(address, port)
      self.use_ssl = true
    end
  end
end

class GData
  def login(email, password)
    @user_id = email
    gdoc_params = { 'Email' => email,
      'Passwd' => password, 
      'source' => 'saush-gdocs-01',
      'accountType' => 'HOSTED_OR_GOOGLE',
      'service' => 'writely'
    }
    gss_params = { 'Email' => email,
      'Passwd' => password, 
      'source' => 'saush-gss-01',
      'accountType' => 'HOSTED_OR_GOOGLE',
      'service' => 'wise'
    }
    gdoc_response = Net::HTTPS.post_form(
       URI.parse("https://#{GOOGLE_CLIENT_LOGIN_URL}"),
       gdoc_params)
    gdoc_response.error! unless gdoc_response.kind_of? 
       Net::HTTPSuccess
    @gdoc_token = gdoc_response.body.split(/=/).last
    gss_response = Net::HTTPS.post_form(
       URI.parse("https://#{GOOGLE_CLIENT_LOGIN_URL}"),
       gss_params)
    gss_response.error! unless gss_response.kind_of? 
       Net::HTTPSuccess
    @gss_token = gss_response.body.split(/=/).last
  end

  # Get a Google Docs feed 
  def gdoc_feed(feed)
    results = ''
    open("http://" + GOOGLE_DOCS_URL + feed, 'Authorization' => 
         "GoogleLogin auth=#{@gdoc_token}") { |s| 
      results = XmlSimple::xml_in(s.read, 'force_array' => false) 
    }
    return results
  end

  # Get a Google Spreadsheets feed 
  def gss_feed(feed)
    results = ''
    open("http://" + GOOGLE_SPREADSHEETS_URL + feed, 'Authorization' 
         => "GoogleLogin auth=#{@gss_token}") { |s| 
      results = XmlSimple::xml_in(s.read, 'force_array' => false) 
    }
    return results
  end

  # returns all spreadsheets in a given folder
  # returns an array of GSpreadsheet objects
  def spreadsheets_in_folder(folder)
    feed = gdoc_feed("/feeds/documents/private/full/-/%7Bhttp:%2F%2Fschemas.google.com%2Fdocs%2F2007%2Ffolders%2F#{@user_id}%7D#{folder}")
    spreadsheets = []
    spreadsheet_data = [] 
    if feed['totalResults'].to_i > 1 then
      spreadsheet_data = spreadsheet_data + feed['entry']
    else
      spreadsheet_data = spreadsheet_data << feed['entry']
    end  
    spreadsheet_data.each { |doc| 
      if doc['category'].include? SPREADSHEET_CATEGORY then
        ss = Spreadsheet.new
        ss.title = doc['title']['content']
        ss.author = doc['author']['name']
        ss.spreadsheet_id = doc['id']
        doc['link'].each { |link|
          case link['rel']
          when "http://schemas.google.com/spreadsheets/2006#worksheetsfeed"
            # hack to overcome bug in Google Spreadsheets http://code.google.com/p/gdata-issues/issues/detail?id=321 
            wks_link =  link['href'].sub "trix.", ""
            ss.worksheets = get_worksheets_from(wks_link)    
          when "alternate"
            ss.link = link['href']               
          when "edit" 
            ss.edit_link = link['href']
          end
        }
        ss.updated_on = doc['updated']      
        spreadsheets << ss
      end
    }
    return spreadsheets
  end

  # get a list of worksheets in this spreadsheet
  def get_worksheets_from(worksheetfeed)
    uri = URI.parse worksheetfeed      
    worksheets = []
    feed = gss_feed(uri.path)    
    if feed['totalResults'].to_i > 1 then
      feed['entry'].each {|ws|
        worksheets << populate_worksheet(ws)      
      }
    else      
      worksheets << populate_worksheet(feed['entry'])      
    end
    return worksheets
  end

  # populate a Worksheet object from a feed
  def populate_worksheet(data)
    ws = Worksheet.new
    ws.title = data['title']['content']
    ws.row_count = data['rowCount']
    ws.col_count = data['colCount']
    ws.worksheet_id = data['id']      
    data['link'].each { |link|
      case link['rel']        
      when "http://schemas.google.com/spreadsheets/2006#listfeed" 
        ws.rows = get_rows(link['href'])
      when "edit" 
        ws.edit_link = link['href']           
      end      
    }  
    return ws
  end

  # get row data from list feed
  def get_rows(listfeed)
    uri = URI.parse listfeed
    rows = []
    feed = gss_feed(uri.path)
    if feed['totalResults'].to_i > 1 then
      feed['entry'].each { |row|
        rows << Row.new.merge(row)
      }
    elsif  feed['totalResults'].to_i == 1 then
      rows << Row.new.merge(feed['entry'])
    end
    return rows
  end

  def get_pdf_document(docid)
    results = ''
    url = "http://docs.google.com/MiscCommands?command=saveasdoc&exportformat=pdf&docID=#{docid}"
    open(url, 'Authorization' => "GoogleLogin auth=#{@gdoc_token}") { |s| 
      results = s.read
    }
    return results
  end

  # delete the spreadsheet
  def delete(spreadsheet)
    url = URI.parse(spreadsheet.edit_link)
    res = Net::HTTP.new(url.host, url.port).start {|http| http.delete(url.path, 'Authorization' => "GoogleLogin auth=#{@gdoc_token}") }
    case res
    when Net::HTTPSuccess, Net::HTTPRedirection
      return true
    else
      return false
    end    
  end
end

# Models a Google spreadsheet 
class Spreadsheet
  attr_accessor :title, :author, :spreadsheet_id, 
                :worksheets, :link, :edit_link, 
                :updated_on
end

# Models a worksheet in a spreadsheet under Google Spreadsheets
class Worksheet
  attr_accessor :title, :row_count, :col_count, 
                :worksheet_id, :rows, :edit_link
end

# Models a row in a worksheet, in a spreadsheet under Google Spreadsheets
class Row < Hash
  def method_missing(m,*a)
    if m.to_s.upcase =~ /=$/
      self[$`] = a[0]
    elsif a.empty?
      self[m.to_s]
    else
      raise NoMethodError, "#{m}"
    end
  end
end

这个库是从Google电子表格中提取信息的核心代码,下面为你详细解释其工作原理。

4. Google API访问库工作原理
  • 登录方法 :通过ClientLogin API向Google账户认证API发送HTTP Post请求,获取认证令牌。由于不能对Google Docs和Google电子表格使用相同的令牌,所以需要登录两次。
def login(email, pwd)
  @user_id = email
  gdoc_params = { 'Email' => email, 
    'Passwd' => pwd, 
    'source' => 'saush-gdocs-01', 
    'accountType' => 'HOSTED_OR_GOOGLE', 
    'service' => 'writely'
  }    
  gss_params = { 'Email' => email, 
    'Passwd' => pwd, 
    'source' => 'saush-gss-01', 
    'accountType' => 'HOSTED_OR_GOOGLE', 
    'service' => 'wise'
  }
  gdoc_response = Net::HTTPS.post_form(URI.parse("https://#{GOOGLE_URL}/accounts/ClientLogin"), gdoc_params)
  gdoc_response.error! unless gdoc_response.kind_of? Net::HTTPSuccess
  @gdoc_token = gdoc_response.body.split(/=/).last
  gss_response = Net::HTTPS.post_form(URI.parse("https://#{GOOGLE_URL}/accounts/ClientLogin"), gss_params)
  gss_response.error! unless gss_response.kind_of? Net::HTTPSuccess
  @gss_token = gss_response.body.split(/=/).last
end
  • 获取提要方法 :使用Open URI获取Google Docs和Google电子表格的提要,并通过XmlSimple进行解析。
def gdoc_feed(feed)
  results = ''
  open("http://" + GOOGLE_DOCS_URL + feed, 'Authorization' => 
       "GoogleLogin auth=#{@gdoc_token}") { |s| 
    results = XmlSimple::xml_in(s.read, 'force_array' => false) 
  }
  return results
end

def gss_feed(feed)
  results = ''
  open("http://" + GOOGLE_SPREADSHEETS_URL + feed, 'Authorization' 
       => "GoogleLogin auth=#{@gss_token}") { |s| 
    results = XmlSimple::xml_in(s.read, 'force_array' => false) 
  }
  return results
end
  • 容器对象
    • Spreadsheet类 :用于建模Google电子表格文档,包含标题、作者、电子表格ID、工作表数组等属性。
    • Worksheet类 :建模电子表格中的工作表,包含标题、行数、列数、工作表ID、行数组等属性。
    • Row类 :继承自Hash,重写了 method_missing 方法,可通过列名访问行数据。
5. 数据解析流程

我们可以用下面的mermaid流程图来展示从Google电子表格中解析数据的流程:

graph TD;
    A[开始] --> B[登录Google API获取令牌];
    B --> C[获取指定文件夹中的电子表格];
    C --> D[遍历电子表格];
    D --> E{是否为电子表格};
    E -- 是 --> F[获取电子表格的工作表];
    F --> G[遍历工作表];
    G --> H[获取工作表的行数据];
    H --> I[处理行数据];
    E -- 否 --> D;
    I --> J[删除已处理的电子表格];
    J --> K[结束];

通过以上步骤,我们可以完成费用报销混搭应用的开发,实现从Google电子表格中提取费用报销数据并进行处理的功能。后续我们还会继续创建Manager类及其控制器和视图、费用报销解析rake脚本、批量支付rake脚本,并修改Payment和Claim Item控制器等。

费用报销混搭插件开发指南

6. 解析电子表格数据示例

为了更清晰地理解数据解析过程,我们来看一个具体的电子表格及其解析结果。假设我们有一个名为“Peter”的电子表格,其ATOM提要如下:

<entry>
  <id>http://docs.google.com/feeds/documents/private/full/spreadsheet%3ApafEr_vqlVZWG3p3RrZQUeQ</id>
  <updated>2008-02-02T00:54:37.383Z</updated>
  <category scheme="http://schemas.google.com/docs/2007/folders/somemail@gmail.com" term="approved-claims" label="approved-claims"/>
  <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/docs/2007#spreadsheet" label="spreadsheet"/>
  <title type="text">Peter</title>
  <content type="text/html" src="http://spreadsheets.google.com/fm?fmcmd=102&amp;key=pafEr_vqlVZWG3p3RrZQUeQ"/>
  <link rel="alternate" type="text/html" href="http://spreadsheets.google.com/ccc?key=pafEr_vqlVZWG3p3RrZQUeQ"/>
  <link rel="http://schemas.google.com/spreadsheets/2006#worksheetsfeed" type="application/atom+xml" href="http://spreadsheets.google.com/feeds/worksheets/pafEr_vqlVZWG3p3RrZQUeQ/private/full"/>
  <link rel="self" type="application/atom+xml" href="http://docs.google.com/feeds/documents/private/full/spreadsheet%3ApafEr_vqlVZWG3p3RrZQUeQ"/>
  <link rel="edit" type="application/atom+xml" href="http://docs.google.com/feeds/documents/private/full/spreadsheet%3ApafEr_vqlVZWG3p3RrZQUeQ/fc5gdcuf"/>
  <author>
    <name>sausheong</name>
    <email>username@gmail.com</email>
  </author>
</entry>

通过XmlSimple解析后,得到的哈希数据如下:

{
  "category" => [
    {
      "term" => "approved-claims",
      "scheme" => "http://schemas.google.com/docs/2007/folders/somemail@gmail.com",
      "label" => "approved-claims"
    },
    {
      "term" => "http://schemas.google.com/docs/2007#spreadsheet",
      "scheme" => "http://schemas.google.com/g/2005#kind",
      "label" => "spreadsheet"
    }
  ],
  "title" => {
    "type" => "text",
    "content" => "Peter"
  },
  "author" => {
    "name" => "sausheong",
    "email" => "somemail@gmail.com"
  },
  "id" => "http://docs.google.com/feeds/documents/private/full/spreadsheet%3ApafEr_vqlVZWG3p3RrZQUeQ",
  "content" => {
    "src" => "http://spreadsheets.google.com/fm?fmcmd=102&key=pafEr_vqlVZWG3p3RrZQUeQ",
    "type" => "text/html"
  },
  "link" => [
    {
      "href" => "http://spreadsheets.google.com/ccc?key=pafEr_vqlVZWG3p3RrZQUeQ",
      "rel" => "alternate",
      "type" => "text/html"
    },
    {
      "href" => "http://spreadsheets.google.com/feeds/worksheets/pafEr_vqlVZWG3p3RrZQUeQ/private/full",
      "rel" => "http://schemas.google.com/spreadsheets/2006#worksheetsfeed",
      "type" => "application/atom+xml"
    },
    {
      "href" => "http://docs.google.com/feeds/documents/private/full/spreadsheet%3ApafEr_vqlVZWG3p3RrZQUeQ",
      "rel" => "self",
      "type" => "application/atom+xml"
    },
    {
      "href" => "http://docs.google.com/feeds/documents/private/full/spreadsheet%3ApafEr_vqlVZWG3p3RrZQUeQ/fc5gdcuf",
      "rel" => "edit",
      "type" => "application/atom+xml"
    }
  ],
  "updated" => "2008-02-02T00:54:37.383Z"
}

从这个哈希数据中,我们可以获取电子表格的各种信息,如标题、作者、ID、链接等。其中,我们关注的工作表提要链接可以通过 hash['link'] rel http://schemas.google.com/spreadsheets/2006#worksheetsfeed 的元素获取。

接着,我们获取该电子表格的工作表提要,示例如下:

<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:openSearch="http://a9.com/-/spec/opensearchrss/1.0/" xmlns:gs="http://schemas.google.com/spreadsheets/2006">
  <id>http://spreadsheets.google.com/feeds/worksheets/pafEr_vqlVZWG3p3RrZQUeQ/private/full</id>
  <updated>2008-02-02T00:54:37.383Z</updated>
  <category scheme="http://schemas.google.com/spreadsheets/2006" term="http://schemas.google.com/spreadsheets/2006#worksheet"/>
  <title type="text">Peter</title>
  <link rel="alternate" type="text/html" href="http://spreadsheets.google.com/ccc?key=pafEr_vqlVZWG3p3RrZQUeQ"/>
  <link rel="http://schemas.google.com/g/2005#feed" type="application/atom+xml" href="http://spreadsheets.google.com/feeds/worksheets/pafEr_vqlVZWG3p3RrZQUeQ/private/full"/>
  <link rel="http://schemas.google.com/g/2005#post" type="application/atom+xml" href="http://spreadsheets.google.com/feeds/worksheets/pafEr_vqlVZWG3p3RrZQUeQ/private/full"/>
  <link rel="self" type="application/atom+xml" href="http://spreadsheets.google.com/feeds/worksheets/pafEr_vqlVZWG3p3RrZQUeQ/private/full"/>
  <author>
    <name>sausheong</name>
    <email>somemail@gmail.com</email>
  </author>
  <openSearch:totalResults>4</openSearch:totalResults>
  <openSearch:startIndex>1</openSearch:startIndex>
  <entry>
    <id>http://spreadsheets.google.com/feeds/worksheets/pafEr_vqlVZWG3p3RrZQUeQ/private/full/od6</id>
    <updated>2008-02-02T00:54:37.383Z</updated>
    <category scheme="http://schemas.google.com/spreadsheets/2006" term="http://schemas.google.com/spreadsheets/2006#worksheet"/>
    <title type="text">Items</title>
    <content type="text">Items</content>
    <link rel="http://schemas.google.com/spreadsheets/2006#listfeed" type="application/atom+xml" href="http://spreadsheets.google.com/feeds/list/pafEr_vqlVZWG3p3RrZQUeQ/od6/private/full"/>
    <link rel="http://schemas.google.com/spreadsheets/2006#cellsfeed" type="application/atom+xml" href="http://spreadsheets.google.com/feeds/cells/pafEr_vqlVZWG3p3RrZQUeQ/od6/private/full"/>
    <link rel="self" type="application/atom+xml" href="http://spreadsheets.google.com/feeds/worksheets/pafEr_vqlVZWG3p3RrZQUeQ/private/full/od6"/>
    <link rel="edit" type="application/atom+xml" href="http://spreadsheets.google.com/feeds/worksheets/pafEr_vqlVZWG3p3RrZQUeQ/private/full/od6/bu1jir0swo"/>
    <gs:rowCount>100</gs:rowCount>
    <gs:colCount>20</gs:colCount>
  </entry>
</feed>

我们关注的是列表提要链接,它指向工作表的行数据。获取列表提要中的一行数据示例如下:

<entry>
  <id>http://spreadsheets.google.com/feeds/list/pafEr_vqlVZWG3p3RrZQUeQ/od6/private/full/cokwr</id>
  <updated>2008-02-02T00:54:37.383Z</updated>
  <category scheme="http://schemas.google.com/spreadsheets/2006" term="http://schemas.google.com/spreadsheets/2006#list"/>
  <title type="text">3/10/2008</title>
  <content type="text">amount: $9.50, item: Taxi, project: Project A, remarks: Taxi to the hotel</content>
  <link rel="self" type="application/atom+xml" href="http://spreadsheets.google.com/feeds/list/pafEr_vqlVZWG3p3RrZQUeQ/od6/private/full/cokwr"/>
  <link rel="edit" type="application/atom+xml" href="http://spreadsheets.google.com/feeds/list/pafEr_vqlVZWG3p3RrZQUeQ/od6/private/full/cokwr/gf6ji7n3f2d53"/>
  <gsx:date>3/10/2008</gsx:date>
  <gsx:amount>$9.50</gsx:amount>
  <gsx:item>Taxi</gsx:item>
  <gsx:project>Project A</gsx:project>
  <gsx:remarks>Taxi to the hotel</gsx:remarks>
</entry>

通过XmlSimple解析后,得到的行数据哈希如下:

{
  "remarks" => "Taxi to the hotel",
  "category" => {
    "term" => "http://schemas.google.com/spreadsheets/2006#list",
    "scheme" => "http://schemas.google.com/spreadsheets/2006"
  },
  "title" => {
    "type" => "text",
    "content" => "3/10/2008"
  },
  "project" => "Project A",
  "date" => "3/10/2008",
  "id" => "http://spreadsheets.google.com/feeds/list/pafEr_vqlVZWG3p3RrZQUeQ/od6/private/full/cokwr",
  "amount" => "$9.50",
  "content" => {
    "type" => "text",
    "content" => "amount: $9.50, item: Taxi, project: Project A, remarks: Taxi to the hotel"
  },
  "item" => "Taxi",
  "link" => [
    {
      "href" => "http://spreadsheets.google.com/feeds/list/pafEr_vqlVZWG3p3RrZQUeQ/od6/private/full/cokwr",
      "rel" => "self",
      "type" => "application/atom+xml"
    },
    {
      "href" => "http://spreadsheets.google.com/feeds/list/pafEr_vqlVZWG3p3RrZQUeQ/od6/private/full/cokwr/gf6ji7n3f2d53",
      "rel" => "edit",
      "type" => "application/atom+xml"
    }
  ],
  "updated" => "2008-02-02T00:54:37.383Z"
}

我们可以看到,通过调用 hash['remarks'] 等方式可以方便地获取对应列的数据,这正是 Row 类重写 method_missing 方法的作用。

7. 后续开发计划

接下来,我们将按照以下步骤继续完成费用报销混搭应用的开发:
1. 创建Manager类及其控制器和视图 :Manager类将负责管理费用报销的流程,控制器和视图将提供用户交互界面,方便管理人员进行操作。
2. 创建费用报销解析rake脚本 :该脚本将定期运行,自动从Google电子表格中解析费用报销数据,并将其存储到数据库中。
3. 创建批量支付rake脚本 :使用Ruby - PayPal库,该脚本将实现批量支付功能,将报销款项支付给员工。
4. 修改Payment和Claim Item控制器 :对控制器进行修改,以确保数据的正确处理和显示,同时与其他模块进行良好的交互。

8. 总结

通过本文,我们详细介绍了费用报销混搭应用的开发过程,包括所需的工具和库、开发步骤、Google API访问库的创建和工作原理,以及数据解析流程。我们还通过具体的示例展示了如何解析电子表格数据。后续的开发工作将围绕Manager类、rake脚本和控制器的创建和修改展开,最终实现一个完整的费用报销管理系统。在开发过程中,我们需要注意Google API的使用规范和数据处理的准确性,确保系统的稳定性和可靠性。以下是整个开发过程的关键步骤总结表格:
| 步骤 | 操作内容 |
| ---- | ---- |
| 1 | 创建Rails应用 |
| 2 | 设置数据库 |
| 3 | 创建Payment和Claim Item支架 |
| 4 | 修改Payment并创建其子类 |
| 5 | 创建Google API访问库 |
| 6 | 创建Manager类及其控制器和视图 |
| 7 | 创建费用报销解析rake脚本 |
| 8 | 创建批量支付rake脚本 |
| 9 | 修改Payment和Claim Item控制器 |

希望本文能为你开发类似的应用提供有价值的参考,让你顺利完成费用报销混搭应用的开发。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值