Табличная часть документа

Продолжение цикла статей о создании мобильного приложения «Заказ покупателя», предыдущие части: 1, 2, 3, 4.

Одной из самых востребованных задач при разработке корпоративного мобильного приложения является организация механизма ввода данных в списках. Если это основной функционал, то при проектировании пользовательского интерфейса этому вопросу следует уделить особое внимание. Желательно вообще избежать дополнительных кликов и открытий вспомогательных диалогов ввода, идеальный вариант – редактирование данных непосредственно в списке.

В этой статье рассмотрен вариант редактирования/заполнения табличной части документа по прайс-листу продукции. То есть при открытии формы документа вместо срок табличной части документа отображается весь список номенклатуры, в котором пользователь отмечает заказываемый товар.

Изменим форму документа, созданную в предыдущей части, так чтобы у нас отображались две страницы. На первой странице с именем «Информация» будем  редактировать реквизиты шапки, а на странице «Товары» отображаем  соответствующую табличную часть документа.

Перелистывание станиц осуществляется жестами, отдельные ярлычки страниц  не выводятся, чтобы сэкономить место на экране.

    

Создание страниц

Страница «Информация» (класс  DocZakazFragment.java) практически полностью идентична, рассмотренной в предыдущем примере, «форме документа» только сделана на фрагментах. В качестве родителя используется вспомогательный класс  SimpleDocumentFragment который как раз и предназначен для редактирования реквизитов шапки документов.

Страница “Товары”  (класс DocZakazTPTovariFragment.java) сделана на базе FbaDBFragment и отображает список номенклатуры из регистра сведений “Цены” с отбором по типу цены. Инициализация данных этого фрагмента:

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
private void initData() throws SQLException{
 
        //ссылка на владелца
        owner = fromBundle(getArguments());
 
        DBOpenHelper helper = getHelper();
        tpDao = helper.getDao(DocumentZakazPokupatelyaTPTovari.class);
        exTbaleCeniDao = new ExTableCeniDao(getConnectionSource());
 
        ConstantsDao costDao = helper.getDao(Constants.class);
        Constants constants = costDao.read();
 
        //Тип цен возможен один, возьмём из констант
        defTipCen = constants.osnovnoiTipCenProdazhi;
 
        HashMap<String,Object> filter = new HashMap<String, Object>();
        filter.put(ExTableCeni.FIELD_NAME_TIP_CEN, defTipCen);
 
        //Внимание! Может быть большой объем данных
        List<ExTableCeni> data = exTbaleCeniDao.select(filter);
 
        //табличная часть сохраненного документа: обновить количество в списке
        if(owner!=null){
                List<DocumentZakazPokupatelyaTPTovari> lstTablePart = tpDao.getTablePart(owner);
                updateListOnTablePart(data,lstTablePart);
        }
 
        //Кастомный построитель /форматтер для элементов строки
        MetaAdapterViewBinder adapterBinder = new MetaAdapterViewBinder(
                        getActivity(), ExTableCeni.class,
                        new String[] {
                                        ExTableCeni.FIELD_NAME_NOMENKLATURA,
                                        ExTableCeni.FIELD_NAME_CENA,
                                        "kolvo" },
                        new int[] {     R.id.tvDescription, R.id.tvPrice, R.id.fetKolvo});
        adapterBinder.setViewBinder(priceViewBinder);
 
        //Нули в поле ввода количества не отображать
        adapterBinder.setFieldFormatter(new FieldFormatter.Builder()
                        .setZeroFormat("").create());
 
        maAdapter = new MetaArrayAdapter<ExTableCeni>(
                        data, R.layout.doc_zakaz_tovar_row, adapterBinder);
 
}
private void initData() throws SQLException{

    	//ссылка на владелца
    	owner = fromBundle(getArguments());

    	DBOpenHelper helper = getHelper();
    	tpDao = helper.getDao(DocumentZakazPokupatelyaTPTovari.class);
    	exTbaleCeniDao = new ExTableCeniDao(getConnectionSource());

    	ConstantsDao costDao = helper.getDao(Constants.class);
    	Constants constants = costDao.read();

    	//Тип цен возможен один, возьмём из констант
    	defTipCen = constants.osnovnoiTipCenProdazhi;

    	HashMap<String,Object> filter = new HashMap<String, Object>();
    	filter.put(ExTableCeni.FIELD_NAME_TIP_CEN, defTipCen);

    	//Внимание! Может быть большой объем данных
    	List<ExTableCeni> data = exTbaleCeniDao.select(filter);

    	//табличная часть сохраненного документа: обновить количество в списке
    	if(owner!=null){
            	List<DocumentZakazPokupatelyaTPTovari> lstTablePart = tpDao.getTablePart(owner);
            	updateListOnTablePart(data,lstTablePart);
    	}

    	//Кастомный построитель /форматтер для элементов строки
    	MetaAdapterViewBinder adapterBinder = new MetaAdapterViewBinder(
                    	getActivity(), ExTableCeni.class,
                    	new String[] {
                                    	ExTableCeni.FIELD_NAME_NOMENKLATURA,
                                    	ExTableCeni.FIELD_NAME_CENA,
                                    	"kolvo" },
                    	new int[] { 	R.id.tvDescription, R.id.tvPrice, R.id.fetKolvo});
    	adapterBinder.setViewBinder(priceViewBinder);

    	//Нули в поле ввода количества не отображать
    	adapterBinder.setFieldFormatter(new FieldFormatter.Builder()
                    	.setZeroFormat("").create());

    	maAdapter = new MetaArrayAdapter<ExTableCeni>(
                    	data, R.layout.doc_zakaz_tovar_row, adapterBinder);

}

Если открывается форма существующего документа – количество в  списке заполняется по данным документа :

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
/*
 * Обновить список по табличной части сохраненного документа
 */
private void updateListOnTablePart(List<ExTableCeni> lst,
                List<DocumentZakazPokupatelyaTPTovari> lstTablePart) {
        for ( DocumentZakazPokupatelyaTPTovari tpRow : lstTablePart) {
                ExTableCeni row = findByGoog(lst, tpRow.nomenklatura, tpRow.harakteristika);
                if (row != null) {
                        row.kolvo = (int) tpRow.kolichestvo;
                        row.setModified(true);
                }
        }
 
}
 
/*
 * Найти в списке элемент по номенклатуре и характеристике
 */
private ExTableCeni findByGoog(List<ExTableCeni> lst,
                CatalogNomenklatura nomenklatura,
                CatalogHarakteristikiNomenklaturi harakteristika) {
 
        ExTableCeni row = null;
        for (ExTableCeni rw : lst) {
                if (rw.nomenklatura.equals(nomenklatura)
                                && rw.harakteristikaNomenklaturi.equals(harakteristika)) {
                        row = rw;
                        break;
                }
        }
        return row;
}
/*
 * Обновить список по табличной части сохраненного документа
 */
private void updateListOnTablePart(List<ExTableCeni> lst,
            	List<DocumentZakazPokupatelyaTPTovari> lstTablePart) {
    	for ( DocumentZakazPokupatelyaTPTovari tpRow : lstTablePart) {
            	ExTableCeni row = findByGoog(lst, tpRow.nomenklatura, tpRow.harakteristika);
            	if (row != null) {
                    	row.kolvo = (int) tpRow.kolichestvo;
                    	row.setModified(true);
            	}
    	}

}

/*
 * Найти в списке элемент по номенклатуре и характеристике
 */
private ExTableCeni findByGoog(List<ExTableCeni> lst,
            	CatalogNomenklatura nomenklatura,
            	CatalogHarakteristikiNomenklaturi harakteristika) {

    	ExTableCeni row = null;
    	for (ExTableCeni rw : lst) {
            	if (rw.nomenklatura.equals(nomenklatura)
                            	&& rw.harakteristikaNomenklaturi.equals(harakteristika)) {
                    	row = rw;
                    	break;
            	}
    	}
    	return row;
}

Обратите внимание на поле «kolvo» в MetaAdapterViewBinder и связанный с ним контрол FieldEditText. Это вспомогательное поле добавлено в класс  ExTableCeni (таблицы цен) и используется исключительно для редактирования значения введенного пользователем и в базе не сохраняется. А так как это поле связано в контролом FieldEditText, дополнительных обработчиков при изменении данных в поле ввода не требуется.

Кастомный построитель,  аналогичный priceViewBinder, уже рассматривался ране.  В основном он используется, когда  не устраивает вывод значений по умолчанию. Например, здесь с номенклатурой выводится так же характеристика, а цена выводится за единицу измерения.

Обратите внимание, как  можно отменить вывод нулей для пустого количества с помощью FieldFormatter.

Процедура сохранения табличной части документа:

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
/**
 * Сохранить в локальной базе табличную часть документа
 * @throws SQLException
 */
public void save(Ref doc) throws SQLException{
        owner = doc;
 
        //удалить все стары записи, если есть
        tpDao.clearTable(owner);
 
        final int count = maAdapter.getCount();
        int lineNumber = 1;
        for(int i = 0; i< count;i++){
                ExTableCeni row = (ExTableCeni) maAdapter.getItem(i);
                if(row.kolvo > 0){
                        DocumentZakazPokupatelyaTPTovari tpRow = tpDao.newItem(owner, lineNumber++);
                        tpRow.nomenklatura = row.nomenklatura;
                        tpRow.harakteristika = row.harakteristikaNomenklaturi;
                        tpRow.edinicaIzmereniya = row.edinicaIzmereniya;
                        tpRow.kolichestvo = row.kolvo;
                        tpRow.cena = row.cena;
                        tpRow.summa = row.kolvo * row.cena;
 
                        tpDao.create(tpRow);
                }
        }
}
/**
 * Сохранить в локальной базе табличную часть документа
 * @throws SQLException
 */
public void save(Ref doc) throws SQLException{
    	owner = doc;

    	//удалить все стары записи, если есть
    	tpDao.clearTable(owner);

    	final int count = maAdapter.getCount();
    	int lineNumber = 1;
    	for(int i = 0; i< count;i++){
            	ExTableCeni row = (ExTableCeni) maAdapter.getItem(i);
            	if(row.kolvo > 0){
                    	DocumentZakazPokupatelyaTPTovari tpRow = tpDao.newItem(owner, lineNumber++);
                    	tpRow.nomenklatura = row.nomenklatura;
                    	tpRow.harakteristika = row.harakteristikaNomenklaturi;
                    	tpRow.edinicaIzmereniya = row.edinicaIzmereniya;
                    	tpRow.kolichestvo = row.kolvo;
                    	tpRow.cena = row.cena;
                    	tpRow.summa = row.kolvo * row.cena;

                    	tpDao.create(tpRow);
            	}
    	}
}

Здесь все предельно просто. Сначала удаляются ранее сохраненные данные, затем перебираются все строки в списке и сохраняются только те, где пользователем введено количество.

Адаптер страниц

Со страницами разобрались. Адаптер для них может быть, например таким:

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
/*
 * Адаптер с двумя страницами отображающими реквизиты документа и его
 * табличную часть «Товары». Для упрощения примера и фрагменты создаются
 * один раз и хранятся в памяти.
 */
private static class DocPagerAdapter extends FragmentPagerAdapter {
 
        static final String[] titles = new String[] { "Информация", "Товары" };
        private Ref ref;
 
        private DocZakazFragment docFragment;
        private DocZakazTPTovariFragment tpFragment;
 
        public DocPagerAdapter(FragmentManager fm, Ref ref) {
                super(fm);
                this.ref = ref;
        }
 
        @Override
        public Fragment getItem(int position) {
                Fragment fragment = null;
                switch (position) {
                case 0:
                        if (docFragment == null)
                                docFragment = DocZakazFragment.newInstance(ref);
                        return docFragment;
                case 1:
                        if (tpFragment == null)
                                tpFragment = DocZakazTPTovariFragment.newInstance(ref);
                        return tpFragment;
                }
                return fragment;
        }
 
        @Override
        public int getCount() {
                return titles.length;
        }
 
        @Override
        public CharSequence getPageTitle(int position) {
                return titles[position];
        }
 
        /**
         * Сохранить (обновить ) документ
         *
         * @return Возвращает ссылку на обновленный документ или null в случае
         *      ошибки
         */
        public Ref save() {
 
                Ref doc = null;
                try {
                        // Сначала сам документ, потом табличную часть
                        // т.к таблицы связаны по внешнему ключу
                        docFragment.getObject().summa = tpFragment.getSumTotal();
                        docFragment.save();
 
                        tpFragment.save(docFragment.getObject());
 
                        doc = docFragment.getObject();
                } catch (SQLException e) {
                        e.printStackTrace();
                }
                return doc;
        }
}
/*
 * Адаптер с двумя страницами отображающими реквизиты документа и его
 * табличную часть «Товары». Для упрощения примера и фрагменты создаются
 * один раз и хранятся в памяти.
 */
private static class DocPagerAdapter extends FragmentPagerAdapter {

    	static final String[] titles = new String[] { "Информация", "Товары" };
    	private Ref ref;

    	private DocZakazFragment docFragment;
    	private DocZakazTPTovariFragment tpFragment;

    	public DocPagerAdapter(FragmentManager fm, Ref ref) {
            	super(fm);
            	this.ref = ref;
    	}

    	@Override
    	public Fragment getItem(int position) {
            	Fragment fragment = null;
            	switch (position) {
            	case 0:
                    	if (docFragment == null)
                            	docFragment = DocZakazFragment.newInstance(ref);
                    	return docFragment;
            	case 1:
                    	if (tpFragment == null)
                            	tpFragment = DocZakazTPTovariFragment.newInstance(ref);
                    	return tpFragment;
            	}
            	return fragment;
    	}

    	@Override
    	public int getCount() {
            	return titles.length;
    	}

    	@Override
    	public CharSequence getPageTitle(int position) {
            	return titles[position];
    	}

    	/**
    	 * Сохранить (обновить ) документ
    	 *
    	 * @return Возвращает ссылку на обновленный документ или null в случае
    	 *     	ошибки
    	 */
    	public Ref save() {

            	Ref doc = null;
            	try {
                    	// Сначала сам документ, потом табличную часть
                    	// т.к таблицы связаны по внешнему ключу
                    	docFragment.getObject().summa = tpFragment.getSumTotal();
                    	docFragment.save();

                    	tpFragment.save(docFragment.getObject());

                    	doc = docFragment.getObject();
            	} catch (SQLException e) {
                    	e.printStackTrace();
            	}
            	return doc;
    	}
}

При создании ему передается ссылка на редактируемый  документ  или null, если требуется создать новый. По запросу из вне,  адаптер сохраняет документ и возвращает обновлённую ссылку  на него.

Форма документа

Класс DocZakazItemActivity (наша форма документа созданная в предыдущей части) изменяется. Теперь она содержит только адаптер и кнопку сохранения документа в меню.

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
 
    <android.support.v4.view.ViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </android.support.v4.view.ViewPager>
</RelativeLayout>
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	android:orientation="vertical" >

	<android.support.v4.view.ViewPager
    	android:id="@+id/pager"
    	android:layout_width="match_parent"
    	android:layout_height="match_parent" >
	</android.support.v4.view.ViewPager>
</RelativeLayout>

Меню menu/activity_zakaz_item.xml:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
    <item
        android:id="@+id/menu_save"
        android:icon="@android:drawable/ic_menu_save"
        android:showAsAction="ifRoom|withText"
        android:title="@string/menu_save"/>
</menu>
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
	<item
    	android:id="@+id/menu_save"
    	android:icon="@android:drawable/ic_menu_save"
    	android:showAsAction="ifRoom|withText"
    	android:title="@string/menu_save"/>
</menu>

Инициализация данных – получить переданную при открытии ссылку и создать адаптер:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void init() {
 
    Ref ref = null;
 
    Bundle extras = getIntent().getExtras();
    if (extras != null) {
            String uuid = extras.getString(EXTRA_REF);
            ref = new DocumentZakazPokupatelya();
            ref.setRef(UUID.fromString(uuid));
    }
    //Адаптер для страниц
    docAdapter = new DocPagerAdapter(getSupportFragmentManager(), ref);
    pager = (ViewPager) findViewById(R.id.pager);
    pager.setAdapter(docAdapter);
}
private void init() {

   	Ref ref = null;

   	Bundle extras = getIntent().getExtras();
   	if (extras != null) {
          	String uuid = extras.getString(EXTRA_REF);
          	ref = new DocumentZakazPokupatelya();
          	ref.setRef(UUID.fromString(uuid));
   	}
   	//Адаптер для страниц
   	docAdapter = new DocPagerAdapter(getSupportFragmentManager(), ref);
   	pager = (ViewPager) findViewById(R.id.pager);
   	pager.setAdapter(docAdapter);
}

Обработчик сохранения документа при нажатии на одноименную кнопку в меню:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
 * Сохранить документ в локальной базе
 */
private void onSaveDoc() {
 
        Ref doc = docAdapter.save();
        if (doc != null) {
 
                // Уведомить об изменении
                Intent i = new Intent(Const.ACTION_UPDATE_ITEM);
                i.addCategory(Const.CATEGORY_CHANGED_DOC_ZAKAZ);
                i.putExtra(DocZakazItemActivity.EXTRA_REF, doc.toString());
                sendBroadcast(i);
 
                finish();
        }
}
/*
 * Сохранить документ в локальной базе
 */
private void onSaveDoc() {

    	Ref doc = docAdapter.save();
    	if (doc != null) {

            	// Уведомить об изменении
            	Intent i = new Intent(Const.ACTION_UPDATE_ITEM);
            	i.addCategory(Const.CATEGORY_CHANGED_DOC_ZAKAZ);
            	i.putExtra(DocZakazItemActivity.EXTRA_REF, doc.toString());
            	sendBroadcast(i);

            	finish();
        }
}

Вызов этой формы (Activity) из списка документов не изменился:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
 * Изменить документ
 */
protected void editDoc(DocumentZakazPokupatelya doc) {
    Intent i = new Intent(this,DocZakazItemActivity.class);
    i.putExtra(DocZakazItemActivity.EXTRA_REF, doc.getRef().toString());
    startActivity(i);
}
 
/*
 * Новый документ
 */
protected void newDoc() {
    Intent i = new Intent(this,DocZakazItemActivity.class);
    startActivity(i);
}
/*
 * Изменить документ
 */
protected void editDoc(DocumentZakazPokupatelya doc) {
   	Intent i = new Intent(this,DocZakazItemActivity.class);
   	i.putExtra(DocZakazItemActivity.EXTRA_REF, doc.getRef().toString());
   	startActivity(i);
}

/*
 * Новый документ
 */
protected void newDoc() {
   	Intent i = new Intent(this,DocZakazItemActivity.class);
   	startActivity(i);
}

Полный код примера как обычно  доступен в нашем SVN-репоритории по адресу https://xp-dev.com/svn/fba_toolkit_public/samples/fbaSample3Order/. Переключитесь на соответствующую ревизию (см. по комментарию).

Движок FBA (проект ru_profi1c_fba) также должен быть обновлен, рекомендуется версия не ниже 1.0.4.002.

Удачи в разработке. Если у вас есть вопросы или комментарии, используйте форму «Контакты» для связи.

Дополнительные материалы по теме

Похожие записи: