Android官方开发文档Training系列课程中文版:网络操作之XML解析

扩展标记语言(XML)是一系列有序编码的文档。它是一种很受欢迎的互联网数据传输格式。像需要频繁更新内容的网站来说,比如新闻站点或者博客,需要经常更新它们的XML源,以使外部程序可以保持内容的同步变化。对于含有网络连接态的APP应用来说,上传及解析XML数据是一个通用的任务。这节课将会学习如何解析XML文档及如何使用XML中的数据。

选择解析器

我们推荐使用XmlPullParser解析器,在Android上它是一种高效的可维护的解析器。Android中含有该接口的两个实现:
两个选择都可以。这里使用的是ExpatPullParser。

分析源

解析源的第一步就是判断哪种属性是你所关注的。解析器会将这些属性所对应的数据提取出,将剩余的部分忽略。
下面是示例APP中的部分摘录。每个实例都是以entry的形式出现在源里,并且entry会包含若干个嵌套标签。
<?xml version="1.0" encoding="utf-8"?> 
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:creativeCommons="http://backend.userland.com/creativeCommonsRssModule" ...">     
<title type="text">newest questions tagged android - Stack Overflow</title>
...
    <entry>
    ...
    </entry>
    <entry>
        <id>http://stackoverflow.com/q/9439999</id>
        <re:rank scheme="http://stackoverflow.com">0</re:rank>
        <title type="text">Where is my data file?</title>
        <category scheme="http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest/tags" term="android"/>
        <category scheme="http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest/tags" term="file"/>
        <author>
            <name>cliff2310</name>
            <uri>http://stackoverflow.com/users/1128925</uri>
        </author>
        <link rel="alternate" href="http://stackoverflow.com/questions/9439999/where-is-my-data-file" />
        <published>2012-02-25T00:30:54Z</published>
        <updated>2012-02-25T00:30:54Z</updated>
        <summary type="html">
            <p>I have an Application that requires a data file...</p>
        </summary>
    </entry>
    <entry>
    ...
    </entry>
...
</feed>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
示例APP中从entry中提出的数据包含title, link, summary等标签。

实例化解析器

接下来这一步就是实例化解析器并启动解析过程。在下面的代码段中,解析器被实例化并设置为不处理命名空间,并且将InputStream作为其输入来源。它通过调用nextTag()方法来启动解析过程,调用readFeed()方法来提取并处理APP所关心的数据。
public class StackOverflowXmlParser {
    // We don't use namespaces
    private static final String ns = null;

    public List parse(InputStream in) throws XmlPullParserException, IOException {
        try {
            XmlPullParser parser = Xml.newPullParser();
            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
            parser.setInput(in, null);
            parser.nextTag();
            return readFeed(parser);
        } finally {
            in.close();
        }
    }
 ... 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

读取源

readFeed()方法开始对源进行实际操作。它会寻找元素标签”entry”并将其作为递归处理的起点。如果标签不是entry,则跳过。一旦整个源完成了递归处理,那么readFeed()则会返回一个List,它内部包含了entry对应的数据及内嵌的数据成员。
private List readFeed(XmlPullParser parser) throws XmlPullParserException, IOException {
    List entries = new ArrayList();
    parser.require(XmlPullParser.START_TAG, ns, "feed");
    while (parser.next() != XmlPullParser.END_TAG) {
        if (parser.getEventType() != XmlPullParser.START_TAG) {
            continue;
        }
        String name = parser.getName();
        // Starts by looking for the entry tag
        if (name.equals("entry")) {
            entries.add(readEntry(parser));
        } else {
            skip(parser);
        }
    }  
    return entries;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

解析XML

XML的解析包含以下步骤:
1.像Analyze the Feed中所描述的那样,需要确定你所关注的标签。这个示例从entry及其内嵌标签title, link, 和 summary中提取数据。
2.创建以下方法:
  • 对每个标签创建一个read方法,用于读取你所关注的标签。 比如readEntry(), readTitle(), 等等等等。解析器会从输入流中读取标签。当它探测到标签名为entry, title, link或summary时,它就会调用该标签所对应的方法。否则会跳过该标签。
  • 每个提取数据的方法会将解析器移动至下一个标签。比如:
    • 对于title及summary标签,解析器会调用readText()方法。该方法通过parser.getText()方法提取相应标签所对应的数据。
    • 对于link标签,解析器首先检查该link是否是我们所关注的,然后才会通过parser.getAttributeValue()方法将link的值提取出。
    • 对于entry标签,解析器会调用readEntry()方法。这个方法会解析entry的内部标签,并返回一个含有数据成员title, link,及summary的对象Entry。
  • 辅助方法skip()是个递归方法。有关更多它的信息,请继续往下。
下面的代码展示了如何解析部分entry,title,link及summary标签。
public static class Entry {
    public final String title;
    public final String link;
    public final String summary;
    private Entry(String title, String summary, String link) {
        this.title = title;
        this.summary = summary;
        this.link = link;
    }
}

// Parses the contents of an entry. If it encounters a title, summary, or link tag, hands them off
// to their respective "read" methods for processing. Otherwise, skips the tag.
private Entry readEntry(XmlPullParser parser) throws XmlPullParserException, IOException {
    parser.require(XmlPullParser.START_TAG, ns, "entry");
    String title = null;
    String summary = null;
    String link = null;
    while (parser.next() != XmlPullParser.END_TAG) {
        if (parser.getEventType() != XmlPullParser.START_TAG) {
            continue;
        }
        String name = parser.getName();
        if (name.equals("title")) {
            title = readTitle(parser);
        } else if (name.equals("summary")) {
            summary = readSummary(parser);
        } else if (name.equals("link")) {
            link = readLink(parser);
        } else {
            skip(parser);
        }
    }
    return new Entry(title, summary, link);
}
// Processes title tags in the feed.
private String readTitle(XmlPullParser parser) throws IOException, XmlPullParserException {
    parser.require(XmlPullParser.START_TAG, ns, "title");
    String title = readText(parser);
    parser.require(XmlPullParser.END_TAG, ns, "title");
    return title;
}

// Processes link tags in the feed.
private String readLink(XmlPullParser parser) throws IOException, XmlPullParserException {
    String link = "";
    parser.require(XmlPullParser.START_TAG, ns, "link");
    String tag = parser.getName();
    String relType = parser.getAttributeValue(null, "rel");  
    if (tag.equals("link")) {
        if (relType.equals("alternate")){
            link = parser.getAttributeValue(null, "href");
            parser.nextTag();
        } 
    }
    parser.require(XmlPullParser.END_TAG, ns, "link");
    return link;
}
// Processes summary tags in the feed.
private String readSummary(XmlPullParser parser) throws IOException, XmlPullParserException {
    parser.require(XmlPullParser.START_TAG, ns, "summary");
    String summary = readText(parser);
    parser.require(XmlPullParser.END_TAG, ns, "summary");
    return summary;
}
// For the tags title and summary, extracts their text values.
private String readText(XmlPullParser parser) throws IOException, XmlPullParserException {
    String result = "";
    if (parser.next() == XmlPullParser.TEXT) {
        result = parser.getText();
        parser.nextTag();
    }
    return result;
}
  ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76

跳过不关心的标签

解析XML数据的其中一个步骤就是需要忽略那些不需要关注的标签。下面是解析器的skip()方法。
private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
    if (parser.getEventType() != XmlPullParser.START_TAG) {
        throw new IllegalStateException();
    }
    int depth = 1;
    while (depth != 0) {
        switch (parser.next()) {
        case XmlPullParser.END_TAG:
            depth--;
            break;
        case XmlPullParser.START_TAG:
            depth++;
            break;
        }
    }
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
它的工作过程如下:
  • 如果当前的项目不是START_TAG的话则抛出异常。
  • 它会消费掉START_TAG标签,直到遇到与之相匹配的END_TAG时结束。
  • 为了确保在正确的END_TAG时结束,这里使用了深度追踪的方式。
所以如果当前的标签含有内嵌标签,depth的值就不会是0,直到解析器消费完了改标签内的所有项目。试想一下解析器如何跳过<author>标签,它含有两个内嵌标签<name>和<uri>:
  • 第一次while循环,解析器所遇到的<author>标签的下一个标签是<name>的START_TAG项目,这时depth的值被自增到2.
  • 第二次while循环,解析器所遇到的下一个标签是</name>的END_TAG项目。这时depth的值被自减到1.
  • 第三次while循环,解析器所遇到的下一个标签是<uri>的START_TAG项目。这时depth的值被自增到2.
  • 第四次while循环,解析器所遇到的下一个标签是</uri>的END_TAG项目。这时depth的值被自减到1.
  • 第四次while循环,解析器所遇到的下一个标签是</author>的END_TAG项目。这时depth的值被自减到0.这表示<author>标签被成功跳过。

消费XML数据

示例应用中将获取解析XML源的过程放在了AsyncTask中。
loadPage()方法执行了以下过程:
  • 以字符串的形式实例化了XML源的URL地址。
  • 如果用户设置了网络连接并且网络连接状况良好,则调用new DownloadXmlTask().execute(url)。这里实例化了一个新的 DownloadXmlTask 对象,并执行了 execute() 方法,该方法会下载并解析源,最后将返回一个字符串形式的用于显示在UI界面上的结果。
public class NetworkActivity extends Activity {
    public static final String WIFI = "Wi-Fi";
    public static final String ANY = "Any";
    private static final String URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest";

    // Whether there is a Wi-Fi connection.
    private static boolean wifiConnected = false; 
    // Whether there is a mobile connection.
    private static boolean mobileConnected = false;
    // Whether the display should be refreshed.
    public static boolean refreshDisplay = true; 
    public static String sPref = null;
    ...

    // Uses AsyncTask to download the XML feed from stackoverflow.com.
    public void loadPage() {  

        if((sPref.equals(ANY)) && (wifiConnected || mobileConnected)) {
            new DownloadXmlTask().execute(URL);
        }
        else if ((sPref.equals(WIFI)) && (wifiConnected)) {
            new DownloadXmlTask().execute(URL);
        } else {
            // show error
        }  
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
DownloadXmlTask实现了AsyncTask的下面一些方法:
  • doInBackground() 执行了loadXmlFromNetwork()方法,它将源的URL地址作为参数传递给loadXmlFromNetwork(),当loadXmlFromNetwork()处理完成之后,将处理后的字符串返回。
  • onPostExecute() 获得刚刚返回的字符串将其显示在UI界面上。
// Implementation of AsyncTask used to download XML feed from stackoverflow.com.
private class DownloadXmlTask extends AsyncTask<String, Void, String> {
    @Override
    protected String doInBackground(String... urls) {
        try {
            return loadXmlFromNetwork(urls[0]);
        } catch (IOException e) {
            return getResources().getString(R.string.connection_error);
        } catch (XmlPullParserException e) {
            return getResources().getString(R.string.xml_error);
        }
    }
    @Override
    protected void onPostExecute(String result) {  
        setContentView(R.layout.main);
        // Displays the HTML string in the UI via a WebView
        WebView myWebView = (WebView) findViewById(R.id.webview);
        myWebView.loadData(result, "text/html", null);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
下面是loadXmlFromNetwork()方法的实现,它执行了以下过程:
1.实例化了StackOverflowXmlParser。它还创建了一个List的变量及title, url, summary变量,这些变量用于持有从XML中读取的值。 
2.调用downloadUrl(),它用于获取源,并返回一个InputStream对象。 
3.使用StackOverflowXmlParser 来解析刚刚的InputStream对象。 StackOverflowXmlParser将提取出的数据放入到List中。 
4.处理List,将其中的数据与HTML标签整合。 
5.最后返回一个HTML形式的字符串,字符串用于显示在主界面上。
// Uploads XML from stackoverflow.com, parses it, and combines it with
// HTML markup. Returns HTML string.
private String loadXmlFromNetwork(String urlString) throws XmlPullParserException, IOException {
    InputStream stream = null;
    // Instantiate the parser
    StackOverflowXmlParser stackOverflowXmlParser = new StackOverflowXmlParser();
    List<Entry> entries = null;
    String title = null;
    String url = null;
    String summary = null;
    Calendar rightNow = Calendar.getInstance(); 
    DateFormat formatter = new SimpleDateFormat("MMM dd h:mmaa");

    // Checks whether the user set the preference to include summary text
    SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
    boolean pref = sharedPrefs.getBoolean("summaryPref", false);

    StringBuilder htmlString = new StringBuilder();
    htmlString.append("<h3>" + getResources().getString(R.string.page_title) + "</h3>");
    htmlString.append("<em>" + getResources().getString(R.string.updated) + " " + 
            formatter.format(rightNow.getTime()) + "</em>");

    try {
        stream = downloadUrl(urlString);        
        entries = stackOverflowXmlParser.parse(stream);
    // Makes sure that the InputStream is closed after the app is
    // finished using it.
    } finally {
        if (stream != null) {
            stream.close();
        } 
     }

    // StackOverflowXmlParser returns a List (called "entries") of Entry objects.
    // Each Entry object represents a single post in the XML feed.
    // This section processes the entries list to combine each entry with HTML markup.
    // Each entry is displayed in the UI as a link that optionally includes
    // a text summary.
    for (Entry entry : entries) {       
        htmlString.append("<p><a href='");
        htmlString.append(entry.link);
        htmlString.append("'>" + entry.title + "</a></p>");
        // If the user set the preference to include summary text,
        // adds it to the display.
        if (pref) {
            htmlString.append(entry.summary);
        }
    }
    return htmlString.toString();
}
// Given a string representation of a URL, sets up a connection and gets
// an input stream.
private InputStream downloadUrl(String urlString) throws IOException {
    URL url = new URL(urlString);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    conn.setReadTimeout(10000 /* milliseconds */);
    conn.setConnectTimeout(15000 /* milliseconds */);
    conn.setRequestMethod("GET");
    conn.setDoInput(true);
    // Starts the query
    conn.connect();
    return conn.getInputStream();
}

SAP学习笔记(MM的基本流程)

    MM模块这个领域对我来说一直基本算是一片空白。但是丑媳妇总要见公婆的,今天把SAP的MM里面的采购流程大致看了一下,挺简单的。
        最重要的当然是组织架构,MM模块里面主要涉及到公司代码、工厂、采购组织、采购组。公司代码下面有多个工厂。采购组织可以负责多个工厂的采购,这是IDES里的例子的典型架构。根据每个公司的实际情况,当然也设置得非常灵活,比如一个采购组织对应一个工厂,或者跨公司代码的采购组织。
        物料主数据和供应商主数据比较简单,这里就不多说了。然后就是信息记录和框架协议,信息记录的概念和SD里的信息记录差不多,只不过把客户换成了供应商。框架协议有两大类:分别是合同(Contract)和计划协议(Schedule agreement)。框架协议是指和供应商在货物的数量或金额上在一个比较长的时期内达成的协议,协议里的货物价格可以在采购订单里使用。
然后就是Source List,这个应该叫货源清单。可以在系统里面维护对物料+工厂来维护一个货源清单,里面可以包含多个供应商在此工厂对该物料的框架协议。这样在采购订单里可以方便的选择货源清单里的任何一个供应商。如果在货源清单里将某一个供应商标记为默认的,那么采购订单每次会默认选择这个供应商。
        首先我用工厂1000下的物料100-120测试,分别对3个供应商1000,1004,1005创建了3个框架协议,价格分别是18,19,20。然后创建了一个货源清单,将1005的19rmb/个设置为默认优先选择。然后用TCODE ME51N创建一个采购申请(Purchase Requisition),在采购申请上选择Source determination,这样只用输入物料100-120和数量,系统就自动显示了供应商1005的价格19/个,并且还显示出了用到的框架协议号码。
        然后来简单看看采购的流程,和销售流程一样,采购的流程也比较容易理解。总的来说就是向供应商下采购订单-->收货--> 检验供应商发票 --> 付款。采购订单是对供应商要求货物提供的正式文档。
        首先用TCODE MMBE来查看库存物料100-500有2388个。我要再采购20个。
用TCODE ME21N来创建采购订单,供应商选1005,需要采购的物料号码是100-500,订单数量是20个,单价是15。
        然后用Logisitics -> MM -> Purchasing -> Purchase Order -> List Displays -> By Vendor来查看采购订单。可以看到报表清楚显示该订单还有20个没有运送;还有20个没有发票。
        假设供应商及时送货,然后我用TCODE MIGO来做收货。做完收货后,系统会生成物料凭证和会计凭证。MMBE可以查看到库存物料100-500已经变成2408了。还是通过List Diplays(ME2L)找到采购订单后,可以看到还有0个没有运送了。然后查看PO history可以找到收货生成的物料凭证,然后通过物料凭证可以查看会计凭证。凭证类型是WE,表示是收货。unfinished products借206.4,价格差异借93.6,应付暂估贷300。这里的价格差异是这样引起的:由于物料100-500的主数据中的财务信息中,该物料使用的是标准价格,而不是移动平均价。按照标准价格算得价格和实际采购价格的差值就借价格差异科目。
        接下来就是供应商提供发票过来后,做发票校验。TCODE是MIRO。做完以后会产生新的会计凭证。重新用ME2L来查看采购订单。可以看到0个没有运送,0个没有发票了。然后在PO history里可以找到会计凭证,应付账款--1005 贷348,应付暂估借300,应交税款-进项税 借48。通过发票校验,应付暂估清掉了,接下来只剩向供应商付款了。付款我就不做了,和销售里的收款差不多。
        今天主要看的是MM的采购流程,和SD的销售流程实际上思路差不多。而且主要步骤都和FI有着紧密的联系。只要有一般的常识和一点会计基础,对每一步理解起来都不难。下次我准备再研究一下MM里的库存管理。

SAP学习笔记(MM的库存管理)

继续学习SAP的MM模块。今天看了MM中的Inventory Management。对库存管理有了一定的了解。
        首先大家知道像沃尔玛这样的零售业的利润表里,利润 = 销售收入 - 采购成本。 采购成本如何计算呢,很简单,初始库存总共采购成本100块,结算时剩余库存成本30块,那么销售出去的采购成本就是100 - 30 = 70块。在很久很久以前,那是计算机技术还不是很发达。一般的制造业公司在月底需要做月结出报表时,需要人工来盘点剩余的库存一共有多少。可想而知多么费时费力。而随着计算机技术的发展,像SAP这样的计算机系统便为财务会计提供了极大的便利,由于库存的每次变化都会记录进系统,所以能够随时知道库存的情况。这样的方便都归功于MM模块的库存管理了。
        移动类型是比较重要的一个概念,每次货物的发货、收货、转移,都会在系统中产生一个物料凭证。而每个物料凭证行项目都会有一个移动类型,移动类型表示了货物的具体移动内容,而且还可以控制屏幕的字段。比如101就表示是对采购订单的收货。201表示对成本中心发货。311表示工厂内storage location之间的货物的传送。561表示货物的初始化。
        用TCODE MIGO可以对采购订单来收货,产生的物料凭证移动类型是101。用TCODE MB21可以创建预留,预留并不影响库存值。但如果库存值小于预留,那么MRP就会产生采购申请。用MB1A可以对预留发货。用MIGO也可以为预留收货。MBRL是退货,及时在已经对采购订单做完收货了,还可以通过原物料凭证退货。MB1B是货物传输,可以在工厂之间传输货物(移动类型是301),在工厂下的storage location来传输货物(311)。由于像这类传输都涉及一个发货方和一个收货方,所以物料凭证都有2个行项目。不像对采购订单的收货产生的物料凭证,只有1个行项目。以上都是通过1步操作来收货发货,还可以通过2步来收货发货,发货:MB1B,移动类型是313,产生2个行项目。收货:MBSU,移动类型是315,只有一个行项目,记录接收方。最后还有两个TCODE比较有用就是:Stock for Posing Date: MB5B和物料凭证清单: MB51。这两个都可以用来查询显示相应的物料凭证。MB5B主要以工厂为主来查询。MB51则以物料凭证上的关键字段来查询。
        通过我对这些操作的测试学习,我发现尽管SAP物料凭证的概念比较简单,无非就是一个移动类型,但是系统提供了非常多非常灵活的操作来满足各种实际需求。
        当然,即使用了SAP,系统上的数字还是可能会和实际库存有误差的,这就需要定期来对物料做盘点(Physical Inventory),盘点很简单,先创建一个Inventory document,然后输入数字,最后在财务上过账。首先通过TCODE MI01来创建document,比如我需要盘点工厂1000库存点0001下的物料100500,在人工盘点完后然后用MI04来输入库存数字,比如实际有1280个,和现在系统中的库存数(1290)有一点点差异。确认后,就可以用MI07来对差异过账了。产生的会计凭证类型是WI,借库存损失,贷商品库存。最后通过MMBE来查看该物料的数量,已经更新为盘点后的1280个了。
        另外有一个概念就是Valuation Area。在SAP中可以将Valuation area设置为公司代码或工厂,如果选公司代码,那么某物料在所有工厂的价格都是一样的。如果选择工厂,那么某物料在不同的工厂可以有不同的价格。然后就是Valuation Class,这是物料主数据里的财务主数据,它定义了该物料在财务记账时使用的总帐科目号。它由物料类型决定,然后在后台配置TCODE OBYC中定义了每个valuation class对应的总帐科目。
        最后我还想复习一下物料的价格类型。在物料主数据里,可以定义物料是使用标准价格(S)还是移动平均价(V)。不同的定价类型,会有不同的记账方式。如果使用标准价格,那么系统中还是对该物料计算移动平均价,可以做为参考。比如某物料A,我在主数据中指定用标准价格,然后初始的标准价格和移动平均价都是2块。然后初始化库存100个。那么总价值就是2×100 = 200块。然后我以每个2.4的价格向供应商采购了100个。在收货后,系统对收货的记账:借库存200,借价格差异费用40,贷应付暂估240。移动平均价变成了( 2.4×100 + 200 )/200 = 2.2,标准价格不变,库存价值变成了2×200 = 400。然后做发票校验,最后供应商的发票显示是每个2.2,那么发票价格就是2.2×100 = 220(这里不考虑增值税),过账后,系统重新加算了该物料的移动平均价:( 220 + 200 )/ 200 = 2.1。系统对发票的记账:借应付暂估240,贷应付账款 220,贷价格差异收入20。
        如果物料B使用了移动平均价,那么如果初始移动平均价为2块,初始库存是100,那么如果以2.4采购100个以后,移动平均价变成了2.2,记账:借库存240,贷应付暂估240。如果发票上是单价2.2,移动平均价变为2.1,记账:借应付暂估240,贷应付账款220,贷库存20。
        MM就先了解到这里了,都是一些非常基础的东西,也蛮有意思的,可以说MM还是SAP里面最简单的一个业务模块了。接下来我要学习传说中SAP里最灵活也是最具挑战性的模块--不错,就是CO模块了:-)