bug fix in sell invoices

This commit is contained in:
Hesabix 2025-05-29 16:30:54 +00:00
parent 69c27c0109
commit b9c3201b9f
7 changed files with 290 additions and 132 deletions

View file

@ -95,6 +95,7 @@ class PersonsController extends AbstractController
$response['bs'] = $bs;
$response['bd'] = $bd;
$response['balance'] = $bs - $bd;
return $this->json($response);
}
@ -499,6 +500,7 @@ class PersonsController extends AbstractController
): JsonResponse {
$acc = $access->hasRole('person');
if (!$acc) {
var_dump($acc);
throw $this->createAccessDeniedException();
}

View file

@ -130,50 +130,50 @@ class PrintersController extends AbstractController
$params = json_decode($content, true);
}
$settings->setSellBidInfo($params['sell']['bidInfo']);
$settings->setSellTaxInfo($params['sell']['taxInfo']);
$settings->setSellDiscountInfo($params['sell']['discountInfo']);
$settings->setSellNote($params['sell']['note']);
$settings->setSellBidInfo($params['sell']['bidInfo'] ?? false);
$settings->setSellTaxInfo($params['sell']['taxInfo'] ?? false);
$settings->setSellDiscountInfo($params['sell']['discountInfo'] ?? false);
$settings->setSellNote($params['sell']['note'] ?? false);
$settings->setSellNoteString($params['sell']['noteString']);
$settings->setSellPays($params['sell']['pays']);
$settings->setSellPays($params['sell']['pays'] ?? false);
$settings->setSellPaper($params['sell']['paper']);
$settings->setSellBusinessStamp($params['sell']['businessStamp']);
$settings->setSellInvoiceIndex($params['sell']['invoiceIndex']);
$settings->setSellBusinessStamp($params['sell']['businessStamp'] ?? false);
$settings->setSellInvoiceIndex($params['sell']['invoiceIndex'] ?? false);
if ($params['buy']['bidInfo'] == null) {
$settings->setBuyBidInfo(false);
} else {
$settings->setBuyBidInfo(true);
}
$settings->setBuyTaxInfo($params['buy']['taxInfo']);
$settings->setBuyDiscountInfo($params['buy']['discountInfo']);
$settings->setBuyNote($params['buy']['note']);
$settings->setBuyTaxInfo($params['buy']['taxInfo'] ?? false);
$settings->setBuyDiscountInfo($params['buy']['discountInfo'] ?? false);
$settings->setBuyNote($params['buy']['note'] ?? false);
$settings->setBuyNoteString($params['buy']['noteString']);
$settings->setBuyPays($params['buy']['pays']);
$settings->setBuyPays($params['buy']['pays'] ?? false);
$settings->setBuyPaper($params['buy']['paper']);
$settings->setRfbuyBidInfo($params['rfbuy']['bidInfo']);
$settings->setRfbuyTaxInfo($params['rfbuy']['taxInfo']);
$settings->setRfbuyDiscountInfo($params['rfbuy']['discountInfo']);
$settings->setRfbuyNote($params['rfbuy']['note']);
$settings->setRfbuyBidInfo($params['rfbuy']['bidInfo'] ?? false);
$settings->setRfbuyTaxInfo($params['rfbuy']['taxInfo'] ?? false);
$settings->setRfbuyDiscountInfo($params['rfbuy']['discountInfo'] ?? false);
$settings->setRfbuyNote($params['rfbuy']['note'] ?? false);
$settings->setRfbuyNoteString($params['rfbuy']['noteString']);
$settings->setRfbuyPays($params['rfbuy']['pays']);
$settings->setRfbuyPays($params['rfbuy']['pays'] ?? false);
$settings->setRfbuyPaper($params['rfbuy']['paper']);
$settings->setRfsellBidInfo($params['rfsell']['bidInfo']);
$settings->setRfsellTaxInfo($params['rfsell']['taxInfo']);
$settings->setRfsellDiscountInfo($params['rfsell']['discountInfo']);
$settings->setRfsellNote($params['rfsell']['note']);
$settings->setRfsellBidInfo($params['rfsell']['bidInfo'] ?? false);
$settings->setRfsellTaxInfo($params['rfsell']['taxInfo'] ?? false);
$settings->setRfsellDiscountInfo($params['rfsell']['discountInfo'] ?? false);
$settings->setRfsellNote($params['rfsell']['note'] ?? false);
$settings->setRfsellNoteString($params['rfsell']['noteString']);
$settings->setRfsellPays($params['rfsell']['pays']);
$settings->setRfsellPays($params['rfsell']['pays'] ?? false);
$settings->setRfSellPaper($params['rfsell']['paper']);
$settings->setRepserviceNoteString($params['repservice']['noteString']);
$settings->setRepServicePaper($params['repservice']['paper']);
$settings->setFastsellCashdeskTicket($params['fastsell']['cashdeskTicket']);
$settings->setFastsellInvoice($params['fastsell']['invoice']);
$settings->setFastsellPdf($params['fastsell']['pdf']);
$settings->setFastsellCashdeskTicket($params['fastsell']['cashdeskTicket'] ?? false);
$settings->setFastsellInvoice($params['fastsell']['invoice'] ?? false);
$settings->setFastsellPdf($params['fastsell']['pdf'] ?? false);
$settings->setLeftFooter($params['global']['leftFooter']);
$settings->setRightFooter($params['global']['rightFooter']);

View file

@ -926,18 +926,20 @@ class SellController extends AbstractController
$sumTax = 0;
$sumTotal = 0;
foreach ($params['items'] as $item) {
$sumTax += $item['tax'] ?? 0;
$sumTotal += $item['total'] ?? 0;
$itemTotal = $item['total'] ?? 0;
$itemTax = $item['tax'] ?? 0;
$sumTotal += $itemTotal;
$sumTax += $itemTax;
$hesabdariRow = new HesabdariRow();
$hesabdariRow->setDes($item['description'] ?? '');
$hesabdariRow->setBid($acc['bid']);
$hesabdariRow->setYear($acc['year']);
$hesabdariRow->setDoc($doc);
$hesabdariRow->setBs($item['total'] + ($item['tax'] ?? 0));
$hesabdariRow->setBs($itemTotal); // فقط مبلغ کالا بدون مالیات
$hesabdariRow->setBd(0);
$hesabdariRow->setDiscount($item['discountAmount'] ?? 0);
$hesabdariRow->setTax($item['tax'] ?? 0);
$hesabdariRow->setTax($itemTax);
$hesabdariRow->setDiscountType($item['showPercentDiscount'] ? 'percent' : 'fixed');
$hesabdariRow->setDiscountPercent($item['discountPercent'] ?? 0);
@ -1221,13 +1223,21 @@ class SellController extends AbstractController
$itemDiscountPercent = $row->getDiscountPercent() ?? 0;
$itemTax = $row->getTax() ?? 0;
// محاسبه تخفیف سطری
if ($itemDiscountType === 'percent') {
$itemDiscount = round(($basePrice * $itemDiscountPercent) / 100);
// محاسبه قیمت واحد و تخفیف
if ($itemDiscountType === 'percent' && $itemDiscountPercent > 0) {
// محاسبه قیمت اصلی در حالت تخفیف درصدی
$originalPrice = $basePrice / (1 - ($itemDiscountPercent / 100));
$itemDiscount = round(($originalPrice * $itemDiscountPercent) / 100);
} else {
// محاسبه قیمت اصلی در حالت تخفیف مقداری
$originalPrice = $basePrice + $itemDiscount;
}
// محاسبه قیمت واحد
$unitPrice = $row->getCommdityCount() > 0 ? $originalPrice / $row->getCommdityCount() : 0;
// محاسبه قیمت خالص (بدون مالیات)
$netPrice = $basePrice - $itemDiscount;
$netPrice = $basePrice;
$totalInvoice += $netPrice;
$items[] = [
@ -1237,7 +1247,7 @@ class SellController extends AbstractController
'code' => $row->getCommodity()->getCode()
],
'count' => $row->getCommdityCount(),
'price' => $row->getCommdityCount() > 0 ? $netPrice / $row->getCommdityCount() : 0,
'price' => $unitPrice,
'discountPercent' => $itemDiscountPercent,
'discountAmount' => $itemDiscount,
'total' => $netPrice,
@ -1256,6 +1266,14 @@ class SellController extends AbstractController
$totalDiscount = $discountAll;
}
// محاسبه مبلغ نهایی با در نظر گرفتن تخفیف کلی و مالیات
$finalTotal = $totalInvoice - $totalDiscount + $transferCost;
$totalTax = 0;
foreach ($items as $item) {
$totalTax += $item['tax'];
}
$finalTotal += $totalTax;
return $this->json([
'result' => 1,
'data' => [
@ -1275,7 +1293,7 @@ class SellController extends AbstractController
'shippingCost' => $transferCost,
'showTotalPercentDiscount' => $discountType === 'percent',
'items' => $items,
'finalTotal' => $doc->getAmount(),
'finalTotal' => $finalTotal,
'payments' => $payments
]
]);

View file

@ -69,6 +69,13 @@ class Access
'bid'=>$bid
]);
}
else {
$year = $this->em->getRepository(Year::class)->findOneBy([
'head' => true,
'bid'=>$bid
]);
if (!$year) { return false; }
}
if ($this->request->headers->get('activeMoney')) {
$money = $this->em->getRepository(Money::class)->findOneBy([
@ -78,6 +85,7 @@ class Access
}
else{
$money = $bid->getMoney();
if (!$money) { return false; }
}
$accessArray = [

View file

@ -3,11 +3,11 @@
v-model="inputValue"
v-bind="$attrs"
:class="$attrs.class"
type="number"
type="text"
:rules="combinedRules"
:error-messages="errorMessages"
@keydown="restrictToNumbers"
@input="handleInput"
@keydown="restrictInput"
dir="ltr"
dense
:hide-details="$attrs['hide-details'] || 'auto'"
@ -19,6 +19,8 @@
</template>
<script>
import { debounce } from 'lodash'
export default {
name: 'HNumberInput',
inheritAttrs: false,
@ -34,18 +36,29 @@ export default {
},
allowDecimal: {
type: Boolean,
default: false
default: true
},
allowNegative: {
type: Boolean,
default: false
},
maxDecimals: {
type: Number,
default: 2
},
useThousandSeparator: {
type: Boolean,
default: true
}
},
data() {
return {
inputValue: '',
errorMessages: []
errorMessages: [],
integerPart: '',
decimalPart: '',
isProcessing: false
}
},
@ -53,15 +66,16 @@ export default {
combinedRules() {
return [
v => {
if (!v && v !== '0') return true // اجازه خالی بودن
const pattern = this.allowDecimal
if (!v && v !== '0') return true
const cleaned = v.replace(/,/g, '')
const regex = this.allowDecimal
? this.allowNegative
? /^-?\d*\.?\d*$/
: /^\d*\.?\d*$/
? new RegExp(`^-?\\d*\\.?\\d{0,${this.maxDecimals}}$`)
: new RegExp(`^\\d*\\.?\\d{0,${this.maxDecimals}}$`)
: this.allowNegative
? /^-?\d+$/
: /^\d+$/
return pattern.test(v) || this.$t('numberinput.invalid_number')
return regex.test(cleaned) || 'فقط عدد با ممیز اعشاری (.) مجاز است'
},
...this.rules
]
@ -74,97 +88,198 @@ export default {
handler(newVal) {
if (newVal === null || newVal === undefined) {
this.inputValue = ''
this.integerPart = ''
this.decimalPart = ''
} else {
const cleaned = String(newVal).replace(this.allowDecimal ? /[^0-9.-]/g : /[^0-9-]/g, '')
this.inputValue = cleaned
const num = Number(newVal)
if (!isNaN(num)) {
this.setPartsFromNumber(num)
this.inputValue = this.formatNumber()
} else {
this.setPartsFromString(String(newVal))
this.inputValue = this.formatNumber()
}
}
}
},
inputValue(newVal) {
if (newVal === '' || newVal === null || newVal === undefined) {
this.$emit('update:modelValue', null)
this.errorMessages = []
return
}
inputValue: {
immediate: true,
handler: debounce(function (newVal) {
if (this.isProcessing) return
this.isProcessing = true
const cleaned = String(newVal).replace(this.allowDecimal ? /[^0-9.-]/g : /[^0-9-]/g, '')
const pattern = this.allowDecimal
? this.allowNegative
? /^-?\d*\.?\d*$/
: /^\d*\.?\d*$/
: this.allowNegative
? /^-?\d+$/
: /^\d+$/
if (pattern.test(cleaned)) {
let numericValue
if (this.allowDecimal) {
numericValue = cleaned === '' || cleaned === '-' ? null : parseFloat(cleaned)
} else {
numericValue = cleaned === '' || cleaned === '-' ? null : parseInt(cleaned, 10)
if (!newVal) {
this.integerPart = ''
this.decimalPart = ''
this.$emit('update:modelValue', null)
this.errorMessages = []
this.isProcessing = false
return
}
this.$emit('update:modelValue', isNaN(numericValue) ? null : numericValue)
this.errorMessages = []
} else {
this.errorMessages = [this.$t('numberinput.invalid_number')]
}
const cleaned = newVal.replace(/,/g, '').trim()
const regex = this.allowDecimal
? this.allowNegative
? new RegExp(`^-?\\d*\\.?\\d{0,${this.maxDecimals}}$`)
: new RegExp(`^\\d*\\.?\\d{0,${this.maxDecimals}}$`)
: this.allowNegative
? /^-?\d+$/
: /^\d+$/
if (regex.test(cleaned)) {
this.setPartsFromString(cleaned)
const formatted = this.formatNumber()
if (this.inputValue !== formatted) {
this.inputValue = formatted
}
const numericValue = this.getNumericValue()
this.$emit('update:modelValue', numericValue)
this.errorMessages = []
} else {
this.errorMessages = ['فقط عدد با ممیز اعشاری (.) مجاز است']
this.inputValue = this.formatNumber()
}
this.isProcessing = false
}, 150)
}
},
methods: {
restrictToNumbers(event) {
convertPersianToEnglish(str) {
const persianNumbers = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹']
const englishNumbers = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
let result = str || ''
persianNumbers.forEach((num, index) => {
result = result.replace(new RegExp(num, 'g'), englishNumbers[index])
})
return result
},
setPartsFromNumber(num) {
const isNegative = num < 0
const absValue = Math.abs(num)
const strValue = this.allowDecimal
? absValue.toString()
: Math.floor(absValue).toString()
const parts = strValue.split('.')
this.integerPart = isNegative ? `-${parts[0]}` : parts[0]
this.decimalPart = parts[1] || ''
},
setPartsFromString(str) {
const cleaned = this.convertPersianToEnglish(str).replace(/,/g, '')
const isNegative = cleaned.startsWith('-')
const absValue = cleaned.replace(/^-/, '')
const parts = absValue.split('.')
this.integerPart = parts[0] || ''
this.integerPart = isNegative && this.integerPart ? `-${this.integerPart}` : this.integerPart
this.decimalPart = this.allowDecimal && parts[1] ? parts[1].slice(0, this.maxDecimals) : ''
},
formatNumber() {
if (!this.integerPart && !this.decimalPart) return ''
let integer = this.integerPart.replace(/^-/, '')
if (this.useThousandSeparator) {
integer = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
const isNegative = this.integerPart.startsWith('-')
let result = isNegative ? `-${integer}` : integer
if (this.allowDecimal && this.decimalPart) {
result += '.' + this.decimalPart
} else if (this.allowDecimal && this.inputValue.endsWith('.')) {
result += '.'
}
return result
},
getNumericValue() {
const cleaned = `${this.integerPart}.${this.decimalPart || '0'}`
const num = this.allowDecimal ? parseFloat(cleaned) : parseInt(cleaned, 10)
return isNaN(num) ? null : num
},
restrictInput(event) {
const key = event.key
const input = this.inputValue || ''
// اجازه دادن به کلیدهای کنترلی
if (
['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter'].includes(key) ||
(event.ctrlKey || event.metaKey)
) {
if (['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter'].includes(key) ||
(event.ctrlKey || event.metaKey)) {
return
}
if (this.allowDecimal) {
// اجازه ورود اعداد، ممیز، کاما (برای کیبوردهای محلی) و (در صورت اجازه) منفی
if (!/[0-9.,]/.test(key) && (!this.allowNegative || key !== '-')) {
event.preventDefault()
}
// جلوگیری از بیش از یک ممیز
if ((key === '.' || key === ',') && input.includes('.')) {
event.preventDefault()
}
// جلوگیری از ممیز در ابتدا یا بعد از منفی
if ((key === '.' || key === ',') && (input === '' || input === '-')) {
event.preventDefault()
}
// جلوگیری از بیش از یک منفی
if (key === '-' && (input.includes('-') || !this.allowNegative)) {
event.preventDefault()
}
// منفی فقط در ابتدا
if (key === '-' && input !== '') {
if (!/[0-9.]/.test(key)) {
event.preventDefault()
return
}
} else {
// فقط اعداد و (در صورت اجازه) منفی
if (!/[0-9]/.test(key) && (!this.allowNegative || key !== '-')) {
if (!/[0-9-]/.test(key)) {
event.preventDefault()
return
}
// جلوگیری از بیش از یک منفی
if (key === '-' && (input.includes('-') || !this.allowNegative)) {
}
if (key === '.') {
if (!this.allowDecimal) {
event.preventDefault()
return
}
// منفی فقط در ابتدا
if (key === '-' && input !== '') {
if (input.includes('.') || this.decimalPart) {
event.preventDefault()
return
}
if (!this.integerPart) {
event.preventDefault()
return
}
}
if (key === '-') {
if (!this.allowNegative || input.includes('-')) {
event.preventDefault()
return
}
}
if (this.allowDecimal && this.decimalPart && /[0-9]/.test(key)) {
if (this.decimalPart.length >= this.maxDecimals) {
event.preventDefault()
return
}
}
},
handleInput(event) {
// تبدیل کاما به ممیز برای کیبوردهای محلی
if (this.allowDecimal && event.target.value.includes(',')) {
this.inputValue = event.target.value.replace(',', '.')
let value = event.target.value || ''
value = this.convertPersianToEnglish(value)
value = value.replace(/,/g, '')
const regex = this.allowDecimal ? /[^0-9.-]/g : /[^0-9-]/g
value = value.replace(regex, '')
// حذف نقاط اضافی
const parts = value.split('.')
if (parts.length > 2) {
value = parts[0] + '.' + parts.slice(1).join('')
}
// اگر نقطه در انتهای عدد باشد، اجازه میدهیم باقی بماند
if (value.endsWith('.')) {
this.setPartsFromString(value.slice(0, -1))
this.inputValue = this.formatNumber() + '.'
return
}
this.setPartsFromString(value)
this.inputValue = this.formatNumber()
}
}
}
@ -178,6 +293,6 @@ export default {
:deep(.v-text-field .v-input__prepend-inner) {
padding-right: 0;
margin-right: 0;
margin-right: auto;
}
</style>

View file

@ -67,11 +67,11 @@
<Hcommoditysearch v-model="item.name" density="compact" hide-details class="my-0" style="font-size: 0.8rem;" return-object @update:modelValue="handleCommodityChange(item)"></Hcommoditysearch>
</td>
<td class="text-center px-2">
<Hnumberinput v-model="item.count" density="compact" @update:modelValue="recalculateTotals" class="my-0" style="font-size: 0.8rem;" :allow-decimal="true"></Hnumberinput>
<Hnumberinput v-model="item.count" density="compact" @update:modelValue="recalculateTotals" class="my-0" style="font-size: 0.8rem;" :max-decimals="2" :allow-decimal="true"></Hnumberinput>
</td>
<td class="text-center px-2">
<div class="d-flex align-center justify-center">
<Hnumberinput v-model="item.price" density="compact" @update:modelValue="recalculateTotals" class="my-0" style="font-size: 0.8rem;" :allow-decimal="true"></Hnumberinput>
<Hnumberinput v-model="item.price" density="compact" @update:modelValue="recalculateTotals" class="my-0" style="font-size: 0.8rem;" :allow-decimal="false"></Hnumberinput>
<v-tooltip v-if="item.name && item.price < item.name.priceBuy" text="قیمت فروش کمتر از قیمت خرید است" location="bottom">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" color="warning" size="small" class="mr-1">mdi-alert</v-icon>
@ -906,24 +906,24 @@ export default {
this.totalInvoice = Number(data.totalInvoice);
this.finalTotal = Number(data.finalTotal);
// تبدیل قیمتها به قیمت خالص (بدون مالیات)
// تبدیل قیمتها به قیمت پایه (بدون مالیات)
this.items = data.items.map(item => {
const basePrice = Number(item.price);
const tax = Number(item.tax);
const netPrice = Math.round(basePrice - tax);
const priceWithoutTax = Math.round(basePrice / (1 + (this.taxPercent / 100)));
return {
name: {
id: item.name.id,
name: item.name.name,
code: item.name.code,
priceSell: netPrice // قیمت فروش بدون مالیات
priceSell: basePrice // قیمت فروش با مالیات
},
count: Number(item.count),
price: netPrice, // قیمت واحد بدون مالیات
price: basePrice, // قیمت واحد با مالیات
discountPercent: Number(item.discountPercent),
discountAmount: Number(item.discountAmount),
total: netPrice, // جمع ردیف بدون مالیات
total: Number(item.total),
description: item.description,
showPercentDiscount: item.showPercentDiscount,
tax: tax

View file

@ -109,31 +109,46 @@ export default defineComponent({
this.totalRec = response.data.relatedDocs.reduce((sum: number, rdoc: any) => sum + parseInt(rdoc.amount), 0);
});
axios.get(`/api/sell/get/info/${this.$route.params.id}`).then((response) => {
this.person = response.data.person;
this.discountAll = response.data.discountAll;
this.transferCost = response.data.transferCost;
this.item.doc.profit = response.data.profit;
this.commoditys = response.data.rows
.filter((item: any) => item.commodity != null)
.map((item: any) => {
this.totalTax += parseInt(item.tax);
this.totalDiscount += parseInt(item.discount);
axios.get(`/api/sell/v2/get/${this.$route.params.id}`).then((response) => {
if (response.data.result === 1) {
const data = response.data.data;
this.person = {
id: data.person?.id,
nikename: data.person?.name,
mobile: '',
tel: '',
addres: '',
postalcode: ''
};
this.discountAll = data.totalDiscount;
this.transferCost = data.shippingCost;
this.item.doc.profit = 0; // این مقدار در API جدید موجود نیست
this.totalTax = 0;
this.totalDiscount = 0;
this.commoditys = data.items.map((item: any) => {
this.totalTax += item.tax;
this.totalDiscount += item.discountAmount;
return {
commodity: item.commodity,
count: item.commodity_count,
price: parseInt((parseInt(item.bs) - parseInt(item.tax) + parseInt(item.discount)) / parseFloat(item.commodity_count)),
bs: item.bs,
bd: item.bd,
id: item.commodity.id,
des: item.des,
discount: item.discount,
commodity: {
id: item.name.id,
name: item.name.name,
unit: 'عدد'
},
count: item.count,
price: item.price,
bs: item.total,
bd: item.discountAmount,
id: item.name.id,
des: item.description,
discount: item.discountAmount,
tax: item.tax,
sumWithoutTax: item.bs - item.tax,
sumTotal: item.bs,
sumWithoutTax: item.total - item.tax,
sumTotal: item.total,
table: 53,
};
});
}
});
axios.post(`/api/business/get/info/${localStorage.getItem('activeBid')}`).then((response) => {