каждый, я хочу сделать форму коммерческого предложения с функцией редактирования, поэтому, если пользователь допустит ошибку при вводе данных, он сможет редактировать, обновлять и добавлять другие элементы. И да, я все еще учусь и в этом разбираюсь ie.
Последняя информация, которую я получил от Google, использует топор ios .all, но я не знаю, как его использовать, уже ищу неделю и все еще застрял с этой проблемой.
Основная проблема в деталях. vue и SalesQuotationsController. php.
Я пытался дать вам как можно больше информации, надеюсь, она поможет Вы идентифицировали его.
Спасибо.
API
API. php
<?php
use Illuminate\Http\Request;
Route::post('/login', 'Auth\LoginController@login');
Route::group(['middleware' => 'auth:api'], function() {
Route::resource('/salesquotation', 'API\SalesQuotationsController')->except(['create', 'show', 'update']);
Route::post('/salesquotation/insertProduct', 'API\SalesQuotationsController@insertProduct');
Route::post('/salesquotation/{id}', 'API\SalesQuotationsController@update');
});
Javascript
store. js
import Vue from 'vue'
import Vuex from 'vuex'
import authVuex from './stores/auth.js'
import sqVuex from './stores/salesquotation.js'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
authVuex,
sqVuex
},
state: {
token: localStorage.getItem('token'),
errors: []
},
getters: {
isAuth: state => {
return state.token != "null" && state.token != null
}
},
mutations: {
SET_TOKEN(state, payload) {
state.token = payload
},
SET_ERRORS(state, payload) {
state.errors = payload
},
CLEAR_ERRORS(state) {
state.errors = []
}
}
})
export default store
router. js
import Vue from 'vue'
import Router from 'vue-router'
import store from './store.js'
import IndexSalesQuotation from './pages/transactions/salesquotation/Index.vue'
import ListSalesQuotation from './pages/transactions/salesquotation/List.vue'
import AddSalesQuotation from './pages/transactions/salesquotation/Add.vue'
import EditSalesQuotation from './pages/transactions/salesquotation/Edit.vue'
Vue.use(Router)
const router = new Router({
mode: 'history',
routes: [
{
path: '/salesquotation',
component: IndexSalesQuotation,
meta: { requiresAuth: true },
children: [
{
path: 'list',
name: 'salesQuotRoute.showList',
component: ListSalesQuotation,
meta: { title: 'Sales Quotation' }
},
{
path: 'add',
name: 'salesQuotRoute.add',
component: AddSalesQuotation,
meta: { title: 'Create New Sales Quotation' }
},
{
path: 'edit/:id',
name: 'salesQuotRoute.edit',
component: EditSalesQuotation,
meta: { title: 'Edit Sales Quotation' }
},
]
},
]
});
export default router
app. js
import Vue from 'vue'
import router from './router.js'
import store from './store.js'
import App from './App.vue'
import BootstrapVue from 'bootstrap-vue'
import VueSweetalert2 from 'vue-sweetalert2'
Vue.use(VueSweetalert2)
Vue.use(BootstrapVue)
import 'sweetalert2/dist/sweetalert2.min.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
import { mapActions, mapGetters, mapState } from 'vuex'
Vue.component('my-cur-input', {
props: ["value"],
template: `
<div>
<input type="text" v-model="displayValue" @blur="isInputActive = false" @focus="isInputActive = true" style="width:100px"/>
</div>`,
data: function() {
return {
// value = 0,
isInputActive: false
}
},
computed: {
displayValue: {
get: function() {
if (this.isInputActive) {
return this.value.toString()
} else {
var stro = this.value;
stro = parseFloat(stro).toFixed(2).split('.');
if (stro[0].length >= 4) {
stro[0] = stro[0].replace(/(\d)(?=(\d{3})+$)/g, '$1.');
}
return "Rp " + stro.join(',');
}
},
set: function(modifiedValue) {
let newValue = parseFloat(modifiedValue.replace(/[^\d\.]/g, ""))
let value = this.value
if (isNaN(newValue)) {
newValue = 0
}
this.$emit('input', newValue)
this.$emit('blur', newValue)
}
}
}
});
new Vue({
el: '#raz',
router,
store,
components: {
App
},
computed: {
...mapGetters(['isAuth']),
...mapState(['token']),
...mapState('userVuex', {
user_authenticated: state => state.authenticated
})
},
methods: {
...mapActions('userVuex', ['getUserLogin']),
watch: {
token() {
},
user_authenticated() {
}
},
created() {
if (this.isAuth) {
this.getUserLogin()
}
}
})
API. js
import axios from 'axios';
import store from './store.js'
const $axios = axios.create({
baseURL: '/api',
headers: {
'Content-Type': 'application/json'
}
});
$axios.interceptors.request.use (
function (config) {
const token = store.state.token
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
},
function (error) {
return Promise.reject (error);
}
);
export default $axios;
биржа продаж. js
import $axios from '../api.js'
const state = () => ({
custitems: [],
items: [],
units: [],
custs: [],
list_sq: [],
detail_sq: [],
page: 1,
idForEdit: ''
})
const mutations = {
ASSIGN_CUSTEM(state, payload) {
state.custitems = payload
},
ASSIGN_ITEM(state, payload) {
state.items = payload
},
ASSIGN_UNITS(state, payload) {
state.units = payload
},
ASSIGN_CUST(state, payload) {
state.custs = payload
},
ASSIGN_DETAIL(state, payload) {
state.detail_sq = payload
},
ASSIGN_LIST(state, payload) {
state.list_sq = payload
},
SET_PAGE(state, payload) {
state.page = payload
},
SET_ID_UPDATE(state, payload) {
state.idForEdit = payload
}
}
const actions = {
getUnit({ commit }) {
return new Promise((resolve, reject) => {
$axios.get(`/unit`)
.then((response) => {
commit('ASSIGN_UNITS', response.data.data)
resolve(response.data)
})
})
},
getProd({ commit, state }, payload) {
let search = payload.search
payload.loading(true)
return new Promise((resolve, reject) => {
$axios.get(`/custem?page=${state.page}&q=${search}`)
.then((response) => {
commit('ASSIGN_CUSTEM', response.data)
payload.loading(false)
resolve(response.data)
})
})
},
getCust({ commit, state }, payload) {
let search = payload.search
payload.loading(true)
return new Promise((resolve, reject) => {
$axios.get(`/cust?page=${state.page}&q=${search}`)
.then((response) => {
commit('ASSIGN_CUST', response.data)
payload.loading(false)
resolve(response.data)
})
})
},
createSalesQuot({commit}, payload) {
return new Promise((resolve, reject) => {
$axios.post(`/salesquotation`, payload)
.then((response) => {
resolve(response.data)
})
})
},
insertProduct({commit}, payload) {
return new Promise((resolve, reject) => {
$axios.post(`/salesquotation/insertProduct`, payload)
.then((response) => {
resolve(response.data)
})
})
},
getListSalesQuot({ commit, state }, payload) {
let search = typeof payload.search != 'undefined' ? payload.search:''
let status = typeof payload.status != 'undefined' ? payload.status:''
return new Promise((resolve, reject) => {
$axios.get(`/salesquotation?page=${state.page}&q=${search}&status=${status}`)
.then((response) => {
commit('ASSIGN_LIST', response.data)
resolve(response.data)
})
})
},
getDetailSalesQuot({ commit }, payload) {
return new Promise((resolve, reject) => {
$axios.get(`/salesquotation/${payload}/edit`)
.then((response) => {
commit('ASSIGN_DETAIL', response.data.data)
resolve(response.data)
})
})
},
editSalesQuot({ commit }, payload) {
return new Promise((resolve, reject) => {
$axios.get(`/salesquotation/${payload}/edit`)
.then((response) => {
resolve(response.data)
})
})
},
updateSalesQuot({ commit }, payload) {
return new Promise((resolve, reject) => {
$axios.put(`/salesquotation/${payload.id}`, payload)
.then((response) => {
resolve(response.data)
})
.catch((error) => {
if (error.response.status == 422) {
commit('SET_ERRORS', error.response.data.errors, { root: true })
}
})
})
},
}
export default {
namespaced: true,
state,
actions,
mutations
}
Vue
Список. vue
<template>
<div class="col-md-12">
<div class="panel">
<div class="panel-heading">
<router-link :to="{ name: 'salesQuotRoute.add' }" class="btn btn-primary btn-sm btn-flat">Create Sales Quotation</router-link>
<div class="pull-right">
<div class="row">
<div class="col-md-6">
<select v-model="filter_status" class="form-control">
<option value="3">All</option>
<option value="cancel">Canceled</option>
<option value="done">Done</option>
<option value="open">Open</option>
</select>
</div>
<div class="col-md-6">
<input type="text" class="form-control" placeholder="Search..." v-model="search">
</div>
</div>
</div>
</div>
<hr>
<div class="panel-body">
<b-table striped hover bordered responsive :items="listSQForm.data" :fields="fields" show-empty>
<template v-slot:cell(master_cust_id)="row">
<p>Name: <strong>{{ row.item.customer ? row.item.customer.cust_name:'' }}</strong></p>
<p>Telp: {{ row.item.customer.phone }}</p>
<p>Add: {{ row.item.customer.address1 }}</p>
</template>
<template v-slot:cell(amount)="row">
<p>{{ row.item.amount | currency }}</p>
</template>
<template v-slot:cell(status)="row">
<p v-html="row.item.status_label"></p>
</template>
<template v-slot:cell(actions)="row">
<router-link :to="{ name: 'salesQuotRoute.edit', params: {id: row.item.id} }" class="btn btn-warning btn-sm"><i class="fa fa-pencil"></i></router-link>
</template>
</b-table>
<div class="row">
<div class="col-md-6">
<p v-if="listSQForm.data"><i class="fa fa-bars"></i> {{ listSQForm.data.length }} item dari {{ listSQForm.meta.total }} total data</p>
</div>
<div class="col-md-6">
<div class="pull-right">
<b-pagination
v-model="page"
:total-rows="listSQForm.meta.total"
:per-page="listSQForm.meta.per_page"
aria-controls="listSQForm"
v-if="listSQForm.data && listSQForm.data.length > 0"
></b-pagination>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapActions, mapState } from 'vuex'
export default {
name: 'DataSalesQuotation',
created() {
this.getListSalesQuot({
status: this.filter_status,
search: this.search
})
},
data() {
return {
fields: [
{ key: 'sq_number', label: 'Quotation'},
{ key: 'master_cust_id', label: 'Customer' },
{ key: 'amount', label: 'Amount' },
{ key: 'description', label: 'Description' },
{ key: 'proforma_invoice_number', label: 'Proforma Invoice' },
{ key: 'status', label: 'Status' },
{ key: 'sales_order_number', label: 'SO Number' },
{ key: 'actions', label: 'Action' }
],
search: '',
filter_status: 3
}
},
computed: {
...mapState('sqVuex', {
listSQForm: state => state.list_sq
}),
page: {
get() {
return this.$store.state.sqVuex.page
},
set(val) {
this.$store.commit('sqVuex/SET_PAGE', val)
}
}
},
filters: {
currency: function(money) {
return accounting.formatMoney(money, "Rp ", 2, ".", ",")
}
},
watch: {
page() {
this.getListSalesQuot({
status: this.filter_status,
search: this.search
})
},
search() {
this.getListSalesQuot({
status: this.filter_status,
search: this.search
})
},
filter_status() {
this.getListSalesQuot({
status: this.filter_status,
search: this.search
})
}
},
methods: {
...mapActions('sqVuex', ['getListSalesQuot'])
}
}
</script>
Редактировать. vue
<template>
<div class="col-md-12">
<div class="panel">
<div class="panel-heading">
<h3 class="panel-title">Add Sales Quotation</h3>
</div>
<div class="panel-body">
<salesQuot-form ref="salesQuotForm"></salesQuot-form>
<div class="form-group">
<button class="btn btn-primary btn-sm btn-flat" @click.prevent="submit">
<i class="fa fa-save"></i> Save
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapActions, mapState, mapMutations } from 'vuex'
import FormSalesQuot from './Form.vue'
export default {
name: 'AddSalesQuotation',
methods: {
submit() {
this.$refs.salesQuotForm.submit()
}
},
components: {
'salesQuot-form': FormSalesQuot
}
}
</script>
Detail. vue
<template>
<div class="col-md-12">
<div class="panel">
<div class="panel-body">
<div class="row">
<div class="col-md-6 table table-responsive-xl" >
<h4> <strong>Customer</strong> </h4>
<hr>
<v-select :options="customerData.data"
v-model="sq.customer"
@search="onSearch"
label="cust_name"
placeholder="Search..."
:filterable="false">
<template slot="no-options">
Search Customer
</template>
<template slot="option" slot-scope="option">
{{ option.cust_number }}: {{ option.cust_name }}
</template>
</v-select>
</div>
<div class="col-md-6 table table-responsive-xl" v-if="sq.customer">
<h4> <strong>Customer Info</strong> </h4>
<hr>
<table class="table table-hover table-responsive-xl">
<tr>
<th width="15%">Customer Number </th>
<td width="2%">:</td>
<td>{{ sq.customer.cust_number }}</td>
</tr>
<tr>
<th>Name </th>
<td>:</td>
<td>{{ sq.customer.cust_name }}</td>
</tr>
<tr>
<th>Main Address </th>
<td>:</td>
<td>{{ sq.customer.address1 }}</td>
</tr>
<tr>
<th>Alt. Address </th>
<td>:</td>
<td>{{ sq.customer.address2 }}</td>
</tr>
<tr>
<th>Phone </th>
<td>:</td>
<td>{{ sq.customer.phone }}</td>
</tr>
<tr>
<th>Contact Person </th>
<td>:</td>
<td>{{ sq.customer.contact }}</td>
</tr>
</table>
</div>
<div class="col-md-6 form-group" :class="{ 'has-error': errors.desc }">
<textarea cols="3" rows="3" class="form-control" v-model="sq.desc"></textarea>
<p class="text-danger" v-if="errors.desc">{{ errors.desc[0] }}</p>
</div>
<div class="col-md-12" style="padding-top: 20px">
<h4><strong>Detail Transaction</strong> </h4>
<hr>
<button class="btn btn-warning btn-sm" style="margin-bottom: 10px" @click="addProduct">Tambah</button>
<div class="text-uppercase text-bold">id selected: {{ selected }}</div>
<div class="table table-responsive-xl">
<b-table-simple striped hover bordered responsive show-empty style="height:500px">
<b-thead>
<b-tr>
<b-th>
<label class="form-checkbox">
<input type="checkbox" v-model="selectAll" @click="select">
<i class="form-icon"></i>
</label>
</b-th>
<b-th>ID</b-th>
<b-th width="35%" id="min-width">Product</b-th>
<b-th width="5%" id="min-width">Quantity</b-th>
<b-th id="min-width">Price</b-th>
<b-th id="min-width">Subtotal</b-th>
<b-th id="min-width">Actions</b-th>
</b-tr>
</b-thead>
<b-tbody>
<b-tr v-for="(row, index) in sq.detailSQ" :key="index">
<b-td>
<label class="form-checkbox">
<input type="checkbox" :value="row.id" v-model="selected" @change="unCheckAll">
<i class="form-icon"></i>
</label>
</b-td>
<b-td>{{row.id}}</b-td>
<b-td>
<v-select :options="productData.data"
v-model="row.product"
@search="onSearchProduct"
label="item_name_alt"
placeholder="Search..."
:filterable="false"
style="width:300px">
<template slot="no-options">
Search Product
</template>
<template slot="option" slot-scope="option">
{{ option.peloduk.item_number }}: {{ option.item_name_alt }}
</template>
</v-select>
</b-td>
<b-td>
<div class="input-group">
<input type="number" v-model="row.quantity" class="form-control" @blur="calculate(index)" style="width:70px">
<span class="input-group-addon">{{ row.product != null && row.product.satuanunit.unit == row.product.satuanunit.unit ? row.product.satuanunit.unit:'Unit' }}</span>
</div>
</b-td>
<b-td>
<div class='input-group'>
<my-cur-input v-model="row.price" @blur="calculate(index)"></my-cur-input>
</div>
</b-td>
<b-td> {{ row.subtotal | currency }}</b-td>
<b-td>
<button class="btn btn-success btn-sm btn-block" @click="isDone(row.id)">
<i class="fa fa-paper-plane-o"></i>
</button>
</b-td>
</b-tr>
</b-tbody>
<b-tfoot>
<b-tr>
<b-th id="total" colspan="5" class="text-right">Total :</b-th>
<b-td>{{ total | currency }}</b-td>
<b-td> <button class="btn btn-success btn-sm btn-block" @click="isDoneAll(selected)">
</button>
</b-td>
</b-tr>
</b-tfoot>
</b-table-simple>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapActions, mapState, mapMutations } from 'vuex'
import vSelect from 'vue-select'
import 'vue-select/dist/vue-select.css'
import {Money} from 'v-money'
import _ from 'lodash'
import axios from 'axios';
export default {
name: 'DetailSalesQuotation',
created() {
this.getDetailSalesQuot(this.$route.params.id).then((res) => {
this.sq = {
customer: res.data.customer,
desc: res.data.description,
detailSQ: res.data.detail
}
})
},
data() {
return {
selectAll: false,
selected: [],
amount: 0,
customer_change: false,
sq: {
customer_id: '',
detailSQ: [
{ product: null, quantity: 1, price: 0, subtotal: 0 }
]
},
}
},
watch: {
},
filters: {
currency: function(money) {
return accounting.formatMoney(money, "Rp ", 2, ".", ",")
}
},
computed: {
...mapState(['errors']),
...mapState('sqVuex', {
detailSQForm: state => state.detail_sq,
customerData: state => state.custs,
productData: state => state.custitems,
}),
total() {
return _.sumBy(this.sq.detailSQ, function(o) {
return parseFloat(o.subtotal)
})
}
},
methods: {
...mapMutations('sqVuex', ['SET_ID_UPDATE']),
...mapActions('sqVuex', ['getDetailSalesQuot','getCust', 'getProd', 'updateSalesQuot', 'insertProduct']),
onSearch(search, loading) {
this.getCust({
search: search,
loading: loading
})
},
onSearchProduct(search, loading) {
this.getProd({
search: search,
loading: loading
})
},
addProduct() {
this.sq.detailSQ.push({ product: null, quantity: null, price: 0, subtotal: 0 })
},
calculate(index) {
let data = this.sq.detailSQ[index]
if (data.product != null) {
data.price = data.price
data.subtotal = (parseFloat(data.price) * (parseFloat(data.quantity))).toFixed(2)
}
},
select() {
this.selected = [];
if (!this.selectAll) {
for (let i in this.sq.detailSQ) {
this.selected.push(this.sq.detailSQ[i].id);
}
}
},
unCheckAll(){
if(this.selected.length == this.sq.detailSQ.length){
this.selectAll = true;
}
else{
this.selectAll = false;
}
},
//this is the problem
submit() {
this.isSuccess = false
let filter = _.filter(this.sq.detailSQ, function(item) {
return item.product != null
})
if (filter.length > 0) {
Object.assign(this.sq, { id: this.$route.params.id })
axios.all([
this.updateSalesQuot(this.sq)
])
}
},
//this is the problem
},
components:
{
vSelect,
Money
}
}
</script>
Backend Laravel
Из-за ограничения символов, модель и коллекция здесь не включены
SalesQuotationsController
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\SalesQuotation;
use App\SalesQuotationDetail;
use DB;
use App\Http\Resources\SalesQuotationCollection;
class SalesQuotationsController extends Controller
{
public function index()
{
$search = request()->q;
$user = request()->user();
$salesquot = SalesQuotation::with(['user', 'detail', 'customer'])->orderBy('created_at', 'DESC')
->whereHas('customer', function($q) use($search) {
$q->where('cust_name', 'LIKE', '%' . $search . '%');
});
if (in_array(request()->status, [0,1])) {
$salesquot = $salesquot->where('status', request()->status);
}
$salesquot = $salesquot->paginate(10);
return new SalesQuotationCollection($salesquot);
}
public function store(Request $request)
{
DB::beginTransaction();
try{
$sqend = SalesQuotation::orderBy('created_at', 'DESC')->first();
if ( ! $sqend )
$sqnum = 0;
else
$sqnum = substr($sqend->sq_number, 2);
$urutansqnum = 'SQ' . sprintf('%04d', intval($sqnum) + 1);
$piend = SalesQuotation::orderBy('created_at', 'DESC')->first();
if (! $piend)
$pinum = 0;
else
$pinum = substr($piend->proforma_invoice_number, 2);
$urutanpinum = 'PI' . sprintf('%04d', intval($pinum) + 1);
$user = $request->user();
$salesquot = SalesQuotation::create([
'sq_number' => $urutansqnum,
'master_cust_id' => $request->customer_id['id'],
'user_id' => $user->id,
'amount' => 0,
'description' => $request->desc,
'proforma_invoice_number' => $urutanpinum,
'status' => 'open'
]);
$amount = 0;
foreach ($request->detailSQ as $row) {
if (!is_null($row['product'])) {
$subtotal = $row['price'] * $row['quantity'];
try{
SalesQuotationDetail::create([
'sales_quotation_id' => $salesquot->id,
'master_cust_id' => $request->customer_id['id'],
'master_cust_item_id' => $row['product']['id'],
'master_unit_id' => $row['product']['master_unit_id'],
'quantity' => $row['quantity'],
'price' => $row['price'],
'subtotal' => $subtotal,
'description' => $request->desc
]);
$amount += $subtotal;
}
}
$salesquot->update(['amount' => $amount]);
DB::commit();
return response()->json(['status' => 'success', 'data' => $salesquot]);
}
catch (\Exception $e){
DB::rollback();
return response()->json(['status' => 'error', 'data' => $e->getMessage()]);
}
}
public function update(Request $request, $id)
{
DB::beginTransaction();
try{
$user = $request->user();
$slsqt = SalesQuotation::find($id);
$slsqt->update([
'master_cust_id' => $request->customer['id'],
'description' => $request->desc
]);
$amount = 0;
foreach ($request->detailSQ as $row) {
if (!is_null($row['product'])) {
$subtotal = $row['price'] * $row['quantity'];
try{
$sld = SalesQuotationDetail::find($row['id']);
$sld->update([
'master_cust_id' => $request->customer['id'],
'master_cust_item_id' => $row['product']['id'],
'master_unit_id' => $row['product']['master_unit_id'],
'quantity' => $row['quantity'],
'price' => $row['price'],
'subtotal' => $subtotal,
'description' => $request->desc
]);
$amount += $subtotal;
}
}
$slsqt->update(['amount' => $amount]);
DB::commit();
return response()->json(['status' => 'success', 'data' => $slsqt]);
}
catch (\Exception $e){
DB::rollback();
return response()->json(['status' => 'error', 'data' => $e->getMessage(), $sld]);
// return response()->json(['status' => 'error', 'data' => $slsqt]);
}
}
public function insertProduct(request $request)
{
DB::beginTransaction();
try{
$amount = 0;
foreach ($request->detailSQ as $row) {
if (!is_null($row['product'])) {
$subtotal = $row['price'] * $row['quantity'];
try{
$sqd = SalesQuotationDetail::create([
'sales_quotation_id' => $request->id,
'master_cust_id' => $request->customer_id['id'],
'master_cust_item_id' => $row['product']['id'],
'master_unit_id' => $row['product']['master_unit_id'],
'quantity' => $row['quantity'],
'price' => $row['price'],
'subtotal' => $subtotal,
'description' => $request->desc
]);
$amount += $subtotal;
}
}
$salesquot->update(['amount' => $amount]);
DB::commit();
return response()->json(['status' => 'success', 'data' => $sqd]);
}
catch (\Exception $e){
DB::rollback();
return response()->json(['status' => 'error', 'data' => $e->getMessage()]);
}
}
public function edit($id)
{
$sq = SalesQuotation::with(['customer', 'detail', 'detail.product', 'detail.product.satuanunit', 'detail.satuanunit'])->find($id);
return response()->json(['status' => 'success', 'data' => $sq]);
}
}
Скриншот базы данных
- SalesQuotations
Снимок экрана структуры данных SalesQuotations
Снимок экрана данных SalesQuotations
SalesQuotationsDetail
Снимок экрана структуры данных SalesQuotationsDetail
Снимок экрана SalesQuotationsDetail
Снимок экрана программы
Форма списка Форма списка
Форма транзакции Форма транзакции
Продолжение формы транзакции Форма транзакции