Compare commits

..

13 Commits

Author SHA1 Message Date
9608ab6207 remove compound interest and remove unused code 2023-08-28 15:49:20 +05:30
c14eb1b414 minor fix 2023-08-28 10:41:43 +05:30
6d0edb4b5a fix table pagination filter issue 2023-08-26 10:48:43 +05:30
0dc8941975 fix minimum total issue 2023-08-26 10:46:35 +05:30
f11437ce63 add validation on requests 2023-08-25 18:30:08 +05:30
dbd75bbe68 add changes in tax per item calculation 2023-08-25 17:45:43 +05:30
4fc67c74e4 remove commit in estimate storage 2023-08-25 09:44:43 +05:30
27660c6bce fix initial tax per item issue 2023-08-25 09:33:30 +05:30
05d5ce26fd Adds Zip to the required PHP Extension check (#1146)
* Ignore .DS_Store

* Checks for required ZIP extension
2023-02-19 11:42:34 +05:30
393fe20010 Fix incorrect invoice creation message (#1109)
Creation message now includes the views disk in its path.
2022-12-17 18:20:51 +05:30
57bdbd2897 feat: front-end files bulding (#1098)
Co-authored-by: Aramayis <>
2022-12-17 18:19:49 +05:30
7447cc24f9 feat: uffizzi integration (#1091)
Co-authored-by: Aramayis <>
2022-11-21 18:35:59 +05:30
889d22d92c Update php version (#1083)
Version 7.4 no longer works when running docker-compose/setup.sh. Updated to 8.1 in Dockerfile. Issue is now resolved and Crater sets up as expected without version conflicts.
2022-11-03 19:29:16 +05:30
26 changed files with 760 additions and 128 deletions

161
.github/workflows/uffizzi-build.yml vendored Normal file
View File

@ -0,0 +1,161 @@
name: Build PR Image
on:
pull_request:
types: [opened,synchronize,reopened,closed]
jobs:
build-application:
name: Build and Push `application`
runs-on: ubuntu-latest
if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }}
outputs:
tags: ${{ steps.meta.outputs.tags }}
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Generate UUID image name
id: uuid
run: echo "UUID_TAG_APP=$(uuidgen)" >> $GITHUB_ENV
- name: Docker metadata
id: meta
uses: docker/metadata-action@v3
with:
images: registry.uffizzi.com/${{ env.UUID_TAG_APP }}
tags: type=raw,value=60d
- name: Build and Push Image to registry.uffizzi.com ephemeral registry
uses: docker/build-push-action@v2
with:
push: true
context: ./
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
file: ./uffizzi/Dockerfile
build-nginx:
needs:
- build-application
name: Build and Push `nginx`
runs-on: ubuntu-latest
if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }}
outputs:
tags: ${{ steps.meta.outputs.tags }}
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Generate UUID image name
id: uuid
run: echo "UUID_TAG_NGINX=$(uuidgen)" >> $GITHUB_ENV
- name: Docker metadata
id: meta
uses: docker/metadata-action@v3
with:
images: registry.uffizzi.com/${{ env.UUID_TAG_NGINX }}
tags: type=raw,value=60d
- name: Build and Push Image to Uffizzi ephemeral registry
uses: docker/build-push-action@v2
with:
push: true
context: ./
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
file: ./uffizzi/nginx/Dockerfile
build-args: |
BASE_IMAGE=${{ needs.build-application.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-crond:
name: Build and Push `crond`
runs-on: ubuntu-latest
if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }}
outputs:
tags: ${{ steps.meta.outputs.tags }}
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Generate UUID image name
id: uuid
run: echo "UUID_TAG_CROND=$(uuidgen)" >> $GITHUB_ENV
- name: Docker metadata
id: meta
uses: docker/metadata-action@v3
with:
images: registry.uffizzi.com/${{ env.UUID_TAG_CROND }}
tags: type=raw,value=60d
- name: Build and Push Image to registry.uffizzi.com ephemeral registry
uses: docker/build-push-action@v2
with:
push: true
context: ./
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
file: ./uffizzi/crond/Dockerfile
cache-from: type=gha
cache-to: type=gha,mode=max
render-compose-file:
name: Render Docker Compose File
# Pass output of this workflow to another triggered by `workflow_run` event.
runs-on: ubuntu-latest
outputs:
compose-file-cache-key: ${{ steps.hash.outputs.hash }}
needs:
- build-application
- build-nginx
- build-crond
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Render Compose File
run: |
APP_IMAGE=$(echo ${{ needs.build-application.outputs.tags }})
export APP_IMAGE
NGINX_IMAGE=$(echo ${{ needs.build-nginx.outputs.tags }})
export NGINX_IMAGE
CROND_IMAGE=$(echo ${{ needs.build-crond.outputs.tags }})
export CROND_IMAGE
# Render simple template from environment variables.
envsubst < ./uffizzi/docker-compose.uffizzi.yml > docker-compose.rendered.yml
cat docker-compose.rendered.yml
- name: Upload Rendered Compose File as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: docker-compose.rendered.yml
retention-days: 2
- name: Serialize PR Event to File
run: |
cat << EOF > event.json
${{ toJSON(github.event) }}
EOF
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: event.json
retention-days: 2
delete-preview:
name: Call for Preview Deletion
runs-on: ubuntu-latest
if: ${{ github.event.action == 'closed' }}
steps:
# If this PR is closing, we will not render a compose file nor pass it to the next workflow.
- name: Serialize PR Event to File
run: echo '${{ toJSON(github.event) }}' > event.json
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: event.json
retention-days: 2

84
.github/workflows/uffizzi-preview.yml vendored Normal file
View File

@ -0,0 +1,84 @@
name: Deploy Uffizzi Preview
on:
workflow_run:
workflows:
- "Build PR Image"
types:
- completed
jobs:
cache-compose-file:
name: Cache Compose File
runs-on: ubuntu-latest
outputs:
compose-file-cache-key: ${{ env.COMPOSE_FILE_HASH }}
pr-number: ${{ env.PR_NUMBER }}
steps:
- name: 'Download artifacts'
# Fetch output (zip archive) from the workflow run that triggered this workflow.
uses: actions/github-script@v6
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "preview-spec"
})[0];
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data));
- name: 'Unzip artifact'
run: unzip preview-spec.zip
- name: Read Event into ENV
run: |
echo 'EVENT_JSON<<EOF' >> $GITHUB_ENV
cat event.json >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Hash Rendered Compose File
id: hash
# If the previous workflow was triggered by a PR close event, we will not have a compose file artifact.
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
run: echo "COMPOSE_FILE_HASH=$(md5sum docker-compose.rendered.yml | awk '{ print $1 }')" >> $GITHUB_ENV
- name: Cache Rendered Compose File
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
uses: actions/cache@v3
with:
path: docker-compose.rendered.yml
key: ${{ env.COMPOSE_FILE_HASH }}
- name: Read PR Number From Event Object
id: pr
run: echo "PR_NUMBER=${{ fromJSON(env.EVENT_JSON).number }}" >> $GITHUB_ENV
- name: DEBUG - Print Job Outputs
if: ${{ runner.debug }}
run: |
echo "PR number: ${{ env.PR_NUMBER }}"
echo "Compose file hash: ${{ env.COMPOSE_FILE_HASH }}"
cat event.json
deploy-uffizzi-preview:
name: Use Remote Workflow to Preview on Uffizzi
needs:
- cache-compose-file
uses: UffizziCloud/preview-action/.github/workflows/reusable.yaml@v2.6.1
with:
# If this workflow was triggered by a PR close event, cache-key will be an empty string
# and this reusable workflow will delete the preview deployment.
compose-file-cache-key: ${{ needs.cache-compose-file.outputs.compose-file-cache-key }}
compose-file-cache-path: docker-compose.rendered.yml
server: https://app.uffizzi.com/
pr-number: ${{ needs.cache-compose-file.outputs.pr-number }}
permissions:
contents: read
pull-requests: write
id-token: write

1
.gitignore vendored
View File

@ -17,3 +17,4 @@ Homestead.yaml
/public/docs
/.scribe
!storage/fonts/.gitkeep
.DS_Store

View File

@ -1,4 +1,4 @@
FROM php:7.4-fpm
FROM php:8.1-fpm
# Arguments defined in docker-compose.yml
ARG user

View File

@ -55,7 +55,7 @@ class CreateTemplateCommand extends Command
copy(public_path("/build/img/PDF/{$type}1.png"), public_path("/build/img/PDF/{$templateName}.png"));
copy(resource_path("/static/img/PDF/{$type}1.png"), resource_path("/static/img/PDF/{$templateName}.png"));
$path = resource_path("app/pdf/{$type}/{$templateName}.blade.php");
$path = resource_path("views/app/pdf/{$type}/{$templateName}.blade.php");
$type = ucfirst($type);
$this->info("{$type} Template created successfully at ".$path);

View File

@ -45,15 +45,21 @@ class EstimatesRequest extends FormRequest
'nullable'
],
'discount' => [
'numeric',
'required',
],
'discount_val' => [
'integer',
'required',
],
'sub_total' => [
'integer',
'required',
],
'total' => [
'integer',
'numeric',
'max:99999999',
'required',
],
'tax' => [
@ -77,9 +83,11 @@ class EstimatesRequest extends FormRequest
'required',
],
'items.*.quantity' => [
'integer',
'required',
],
'items.*.price' => [
'integer',
'required',
],
];

View File

@ -45,15 +45,21 @@ class InvoicesRequest extends FormRequest
'nullable'
],
'discount' => [
'numeric',
'required',
],
'discount_val' => [
'integer',
'required',
],
'sub_total' => [
'integer',
'required',
],
'total' => [
'integer',
'numeric',
'max:99999999',
'required',
],
'tax' => [
@ -77,9 +83,11 @@ class InvoicesRequest extends FormRequest
'required',
],
'items.*.quantity' => [
'integer',
'required',
],
'items.*.price' => [
'integer',
'required',
],
];

View File

@ -43,15 +43,21 @@ class RecurringInvoiceRequest extends FormRequest
'nullable'
],
'discount' => [
'numeric',
'required',
],
'discount_val' => [
'integer',
'required',
],
'sub_total' => [
'integer',
'required',
],
'total' => [
'integer',
'numeric',
'max:99999999',
'required',
],
'tax' => [

54
composer.lock generated
View File

@ -1633,22 +1633,22 @@
},
{
"name": "guzzlehttp/guzzle",
"version": "7.4.5",
"version": "7.4.4",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "1dd98b0564cb3f6bd16ce683cb755f94c10fbd82"
"reference": "e3ff079b22820c2029d4c2a87796b6a0b8716ad8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/1dd98b0564cb3f6bd16ce683cb755f94c10fbd82",
"reference": "1dd98b0564cb3f6bd16ce683cb755f94c10fbd82",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/e3ff079b22820c2029d4c2a87796b6a0b8716ad8",
"reference": "e3ff079b22820c2029d4c2a87796b6a0b8716ad8",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/promises": "^1.5",
"guzzlehttp/psr7": "^1.9 || ^2.4",
"guzzlehttp/psr7": "^1.8.3 || ^2.1",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0",
"symfony/deprecation-contracts": "^2.2 || ^3.0"
@ -1737,7 +1737,7 @@
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
"source": "https://github.com/guzzle/guzzle/tree/7.4.5"
"source": "https://github.com/guzzle/guzzle/tree/7.4.4"
},
"funding": [
{
@ -1753,20 +1753,20 @@
"type": "tidelift"
}
],
"time": "2022-06-20T22:16:13+00:00"
"time": "2022-06-09T21:39:15+00:00"
},
{
"name": "guzzlehttp/promises",
"version": "1.5.2",
"version": "1.5.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "b94b2807d85443f9719887892882d0329d1e2598"
"reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/b94b2807d85443f9719887892882d0329d1e2598",
"reference": "b94b2807d85443f9719887892882d0329d1e2598",
"url": "https://api.github.com/repos/guzzle/promises/zipball/fe752aedc9fd8fcca3fe7ad05d419d32998a06da",
"reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da",
"shasum": ""
},
"require": {
@ -1821,7 +1821,7 @@
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
"source": "https://github.com/guzzle/promises/tree/1.5.2"
"source": "https://github.com/guzzle/promises/tree/1.5.1"
},
"funding": [
{
@ -1837,20 +1837,20 @@
"type": "tidelift"
}
],
"time": "2022-08-28T14:55:35+00:00"
"time": "2021-10-22T20:56:57+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "2.4.3",
"version": "2.3.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "67c26b443f348a51926030c83481b85718457d3d"
"reference": "83260bb50b8fc753c72d14dc1621a2dac31877ee"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/67c26b443f348a51926030c83481b85718457d3d",
"reference": "67c26b443f348a51926030c83481b85718457d3d",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/83260bb50b8fc753c72d14dc1621a2dac31877ee",
"reference": "83260bb50b8fc753c72d14dc1621a2dac31877ee",
"shasum": ""
},
"require": {
@ -1864,21 +1864,17 @@
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.1",
"bamarni/composer-bin-plugin": "^1.4.1",
"http-interop/http-factory-tests": "^0.9",
"phpunit/phpunit": "^8.5.29 || ^9.5.23"
"phpunit/phpunit": "^8.5.8 || ^9.3.10"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "2.4-dev"
"dev-master": "2.3-dev"
}
},
"autoload": {
@ -1940,7 +1936,7 @@
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.4.3"
"source": "https://github.com/guzzle/psr7/tree/2.3.0"
},
"funding": [
{
@ -1956,7 +1952,7 @@
"type": "tidelift"
}
],
"time": "2022-10-26T14:07:24+00:00"
"time": "2022-06-09T08:26:02+00:00"
},
{
"name": "hamcrest/hamcrest-php",
@ -7670,7 +7666,7 @@
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.0.2",
"version": "v3.0.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
@ -7717,7 +7713,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.0.2"
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.0.1"
},
"funding": [
{
@ -11860,5 +11856,5 @@
"php": "^7.4 || ^8.0"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.1.0"
}

View File

@ -27,6 +27,7 @@ return [
'tokenizer',
'JSON',
'cURL',
'zip',
],
'apache' => [
'mod_rewrite',

View File

@ -271,23 +271,19 @@ const price = computed({
} else {
updateItemAttribute('price', newValue)
}
setDiscount()
},
})
const subtotal = computed(() => props.itemData.price * props.itemData.quantity)
const subtotal = computed(() => Math.round(props.itemData.price * props.itemData.quantity))
const discount = computed({
get: () => {
return props.itemData.discount
},
set: (newValue) => {
if (props.itemData.discount_type === 'percentage') {
updateItemAttribute('discount_val', (subtotal.value * newValue) / 100)
} else {
updateItemAttribute('discount_val', Math.round(newValue * 100))
}
updateItemAttribute('discount', newValue)
setDiscount()
},
})
@ -313,7 +309,7 @@ const showRemoveButton = computed(() => {
const totalSimpleTax = computed(() => {
return Math.round(
sumBy(props.itemData.taxes, function (tax) {
if (!tax.compound_tax) {
if (tax.amount) {
return tax.amount
}
return 0
@ -321,18 +317,7 @@ const totalSimpleTax = computed(() => {
)
})
const totalCompoundTax = computed(() => {
return Math.round(
sumBy(props.itemData.taxes, function (tax) {
if (tax.compound_tax) {
return tax.amount
}
return 0
})
)
})
const totalTax = computed(() => totalSimpleTax.value + totalCompoundTax.value)
const totalTax = computed(() => totalSimpleTax.value)
const rules = {
name: {
@ -416,6 +401,16 @@ function updateTax(data) {
syncItemToStore()
}
function setDiscount() {
const newValue = props.store[props.storeProp].items[props.index].discount
if (props.itemData.discount_type === 'percentage'){
updateItemAttribute('discount_val', Math.round((subtotal.value * newValue) / 100))
}else{
updateItemAttribute('discount_val', Math.round(newValue * 100))
}
}
function searchVal(val) {
updateItemAttribute('name', val)
}
@ -485,10 +480,12 @@ function syncItemToStore() {
total: total.value,
sub_total: subtotal.value,
totalSimpleTax: totalSimpleTax.value,
totalCompoundTax: totalCompoundTax.value,
totalTax: totalTax.value,
tax: totalTax.value,
taxes: [...itemTaxes],
tax_type_ids: itemTaxes.flatMap(_t =>
_t.tax_type_id ? _t.tax_type_id : [],
),
}
props.store.updateItem(data)

View File

@ -146,14 +146,14 @@ const filteredTypes = computed(() => {
})
const taxAmount = computed(() => {
if (localTax.compound_tax && props.discountedTotal) {
return ((props.discountedTotal + props.totalTax) * localTax.percent) / 100
}
if (props.discountedTotal && localTax.percent) {
const taxPerItemEnabled = props.store[props.storeProp].tax_per_item === 'YES'
const discountPerItemEnabled = props.store[props.storeProp].discount_per_item === 'YES'
if (taxPerItemEnabled && !discountPerItemEnabled){
return getTaxAmount()
}
return (props.discountedTotal * localTax.percent) / 100
}
return 0
})
@ -171,6 +171,13 @@ watch(
}
)
watch(
() => taxAmount.value,
() => {
updateRowTax()
},
)
// Set SelectedTax
if (props.taxData.tax_type_id > 0) {
selectedTax.value = taxTypeStore.taxTypes.find(
@ -183,7 +190,6 @@ updateRowTax()
function onSelectTax(val) {
localTax.percent = val.percent
localTax.tax_type_id = val.id
localTax.compound_tax = val.compound_tax
localTax.name = val.name
updateRowTax()
@ -220,6 +226,27 @@ function openTaxModal() {
function removeTax(index) {
props.store.$patch((state) => {
state[props.storeProp].items[props.itemIndex].taxes.splice(index, 1)
state[props.storeProp].items[props.itemIndex].tax = 0
state[props.storeProp].items[props.itemIndex].totalTax = 0
})
}
function getTaxAmount() {
let total = 0
let discount = 0
const itemTotal = props.discountedTotal
const modelDiscount = props.store[props.storeProp].discount ? props.store[props.storeProp].discount : 0
const type = props.store[props.storeProp].discount_type
if (modelDiscount > 0) {
props.store[props.storeProp].items.forEach((_i) => {
total += _i.total
})
const proportion = (itemTotal / total).toFixed(2)
discount = type === 'fixed' ? modelDiscount * 100 : (total * modelDiscount) / 100
const itemDiscount = Math.round(discount * proportion)
const discounted = itemTotal - itemDiscount
return Math.round((discounted * localTax.percent) / 100)
}
return Math.round((props.discountedTotal * localTax.percent) / 100)
}
</script>

View File

@ -191,7 +191,7 @@
</template>
<script setup>
import { computed, inject, ref } from 'vue'
import { computed, inject, ref, watch } from 'vue'
import Guid from 'guid'
import Tax from './CreateTotalTaxes.vue'
import TaxStub from '@/scripts/admin/stub/abilities'
@ -227,19 +227,20 @@ const utils = inject('$utils')
const companyStore = useCompanyStore()
watch(
() => props.store[props.storeProp].items,
(val) => {
setDiscount()
}, { deep: true },
)
const totalDiscount = computed({
get: () => {
return props.store[props.storeProp].discount
},
set: (newValue) => {
if (props.store[props.storeProp].discount_type === 'percentage') {
props.store[props.storeProp].discount_val = Math.round(
(props.store.getSubTotal * newValue) / 100
)
} else {
props.store[props.storeProp].discount_val = Math.round(newValue * 100)
}
props.store[props.storeProp].discount = newValue
setDiscount()
},
})
@ -265,7 +266,7 @@ const itemWiseTaxes = computed(() => {
} else if (tax.tax_type_id) {
taxes.push({
tax_type_id: tax.tax_type_id,
amount: tax.amount,
amount: Math.round(tax.amount),
percent: tax.percent,
name: tax.name,
})
@ -284,6 +285,19 @@ const defaultCurrency = computed(() => {
}
})
function setDiscount() {
const newValue = props.store[props.storeProp].discount
if (props.store[props.storeProp].discount_type === 'percentage') {
props.store[props.storeProp].discount_val
= Math.round((props.store.getSubTotal * newValue) / 100)
return
}
props.store[props.storeProp].discount_val = Math.round(newValue * 100)
}
function selectFixed() {
if (props.store[props.storeProp].discount_type === 'fixed') {
return
@ -295,24 +309,21 @@ function selectFixed() {
}
function selectPercentage() {
if (props.store[props.storeProp].discount_type === 'percentage') {
if (props.store[props.storeProp].discount_type === 'percentage'){
return
}
props.store[props.storeProp].discount_val =
(props.store.getSubTotal * props.store[props.storeProp].discount) / 100
const val = Math.round(props.store[props.storeProp].discount * 100) / 100
props.store[props.storeProp].discount_val
= Math.round((props.store.getSubTotal * val) / 100)
props.store[props.storeProp].discount_type = 'percentage'
}
function onSelectTax(selectedTax) {
let amount = 0
if (selectedTax.compound_tax && props.store.getSubtotalWithDiscount) {
amount = Math.round(
((props.store.getSubtotalWithDiscount + props.store.getTotalSimpleTax) *
selectedTax.percent) /
100
)
} else if (props.store.getSubtotalWithDiscount && selectedTax.percent) {
if (props.store.getSubtotalWithDiscount && selectedTax.percent) {
amount = Math.round(
(props.store.getSubtotalWithDiscount * selectedTax.percent) / 100
)
@ -323,7 +334,6 @@ function onSelectTax(selectedTax) {
id: Guid.raw(),
name: selectedTax.name,
percent: selectedTax.percent,
compound_tax: selectedTax.compound_tax,
tax_type_id: selectedTax.id,
amount,
}

View File

@ -77,17 +77,6 @@
@input="v$.currentTaxType.description.$touch()"
/>
</BaseInputGroup>
<BaseInputGroup
:label="$t('tax_types.compound_tax')"
variant="horizontal"
class="flex flex-row-reverse"
>
<BaseSwitch
v-model="taxTypeStore.currentTaxType.compound_tax"
class="flex items-center"
/>
</BaseInputGroup>
</BaseInputGrid>
</div>
<div
@ -209,14 +198,7 @@ async function submitTaxTypeData() {
function SelectTax(taxData) {
let amount = 0
if (taxData.compound_tax && estimateStore.getSubtotalWithDiscount) {
amount = Math.round(
((estimateStore.getSubtotalWithDiscount +
estimateStore.getTotalSimpleTax) *
taxData.percent) /
100
)
} else if (estimateStore.getSubtotalWithDiscount && taxData.percent) {
if (estimateStore.getSubtotalWithDiscount && taxData.percent) {
amount = Math.round(
(estimateStore.getSubtotalWithDiscount * taxData.percent) / 100
)
@ -226,7 +208,6 @@ function SelectTax(taxData) {
id: Guid.raw(),
name: taxData.name,
percent: taxData.percent,
compound_tax: taxData.compound_tax,
tax_type_id: taxData.id,
amount,
}
@ -242,7 +223,6 @@ function selectItemTax(taxData) {
id: Guid.raw(),
name: taxData.name,
percent: taxData.percent,
compound_tax: taxData.compound_tax,
tax_type_id: taxData.id,
}
modalStore.refreshData(data)

View File

@ -143,7 +143,8 @@ export const useEstimateStore = (useWindow = false) => {
axios
.get(`/api/v1/estimates/${id}`)
.then((response) => {
Object.assign(this.newEstimate, response.data.data)
this.setEstimateData(response.data.data)
this.setCustomerAddresses(this.newEstimate.customer)
resolve(response)
})
.catch((err) => {
@ -154,6 +155,41 @@ export const useEstimateStore = (useWindow = false) => {
})
},
setEstimateData(estimate) {
Object.assign(this.newEstimate, estimate)
if (this.newEstimate.tax_per_item === 'YES') {
this.newEstimate.items.forEach((_i) => {
if (_i.taxes && !_i.taxes.length){
_i.taxes.push({ ...taxStub, id: Guid.raw() })
}
})
}
if (this.newEstimate.discount_per_item === 'YES') {
this.newEstimate.items.forEach((_i, index) => {
if (_i.discount_type === 'fixed'){
this.newEstimate.items[index].discount = _i.discount / 100
}
})
}
else {
if (this.newEstimate.discount_type === 'fixed'){
this.newEstimate.discount = this.newEstimate.discount / 100
}
}
},
setCustomerAddresses(customer) {
const customer_business = customer.customer_business
if (customer_business?.billing_address){
this.newEstimate.customer.billing_address = customer_business.billing_address
}
if (customer_business?.shipping_address){
this.newEstimate.customer.shipping_address = customer_business.shipping_address
}
},
addSalesTaxUs() {
const taxTypeStore = useTaxTypeStore()
let salesTax = { ...taxStub }

View File

@ -133,8 +133,8 @@ export const useInvoiceStore = (useWindow = false) => {
axios
.get(`/api/v1/invoices/${id}`)
.then((response) => {
Object.assign(this.newInvoice, response.data.data)
this.newInvoice.customer = response.data.data.customer
this.setInvoiceData(response.data.data)
this.setCustomerAddresses(this.newInvoice.customer)
resolve(response)
})
.catch((err) => {
@ -144,6 +144,38 @@ export const useInvoiceStore = (useWindow = false) => {
})
},
setInvoiceData(invoice) {
Object.assign(this.newInvoice, invoice)
if (this.newInvoice.tax_per_item === 'YES') {
this.newInvoice.items.forEach((_i) => {
if (_i.taxes && !_i.taxes.length)
_i.taxes.push({ ...taxStub, id: Guid.raw() })
})
}
if (this.newInvoice.discount_per_item === 'YES') {
this.newInvoice.items.forEach((_i, index) => {
if (_i.discount_type === 'fixed')
this.newInvoice.items[index].discount = _i.discount / 100
})
}
else {
if (this.newInvoice.discount_type === 'fixed')
this.newInvoice.discount = this.newInvoice.discount / 100
}
},
setCustomerAddresses(customer) {
const customer_business = customer.customer_business
if (customer_business?.billing_address)
this.newInvoice.customer.billing_address = customer_business.billing_address
if (customer_business?.shipping_address)
this.newInvoice.customer.shipping_address = customer_business.shipping_address
},
addSalesTaxUs() {
const taxTypeStore = useTaxTypeStore()
let salesTax = { ...taxStub }

View File

@ -138,6 +138,7 @@
<script setup>
import { computed, ref, watch, onMounted } from 'vue'
import { cloneDeep } from 'lodash'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import {
@ -257,11 +258,30 @@ async function submitForm() {
isSaving.value = true
let data = {
let data = cloneDeep({
...estimateStore.newEstimate,
sub_total: estimateStore.getSubTotal,
total: estimateStore.getTotal,
tax: estimateStore.getTotalTax,
})
if (data.discount_per_item === 'YES') {
data.items.forEach((item, index) => {
if (item.discount_type === 'fixed'){
data.items[index].discount = Math.round(item.discount * 100)
}
})
}
else {
if (data.discount_type === 'fixed'){
data.discount = Math.round(data.discount * 100)
}
}
if (
!estimateStore.newEstimate.tax_per_item === 'YES'
&& data.taxes.length
){
data.tax_type_ids = data.taxes.map(_t => _t.tax_type_id)
}
const action = isEdit.value

View File

@ -147,6 +147,7 @@ import {
decimal,
} from '@vuelidate/validators'
import useVuelidate from '@vuelidate/core'
import { cloneDeep } from 'lodash'
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
import { useModuleStore } from '@/scripts/admin/stores/module'
@ -258,11 +259,29 @@ async function submitForm() {
isSaving.value = true
let data = {
let data = cloneDeep({
...invoiceStore.newInvoice,
sub_total: invoiceStore.getSubTotal,
total: invoiceStore.getTotal,
tax: invoiceStore.getTotalTax,
})
if (data.discount_per_item === 'YES') {
data.items.forEach((item, index) => {
if (item.discount_type === 'fixed'){
data.items[index].discount = item.discount * 100
}
})
}
else {
if (data.discount_type === 'fixed'){
data.discount = data.discount * 100
}
}
if (
!invoiceStore.newInvoice.tax_per_item === 'YES'
&& data.taxes.length
){
data.tax_type_ids = data.taxes.map(_t => _t.tax_type_id)
}
try {

View File

@ -20,21 +20,6 @@
:data="fetchData"
:columns="taxTypeColumns"
>
<template #cell-compound_tax="{ row }">
<BaseBadge
:bg-color="
utils.getBadgeStatusColor(row.data.compound_tax ? 'YES' : 'NO')
.bgColor
"
:color="
utils.getBadgeStatusColor(row.data.compound_tax ? 'YES' : 'NO')
.color
"
>
{{ row.data.compound_tax ? 'Yes' : 'No'.replace('_', ' ') }}
</BaseBadge>
</template>
<template #cell-percent="{ row }"> {{ row.data.percent }} % </template>
<template v-if="hasAtleastOneAbility()" #cell-actions="{ row }">
@ -91,11 +76,6 @@ const taxTypeColumns = computed(() => {
thClass: 'extra',
tdClass: 'font-medium text-gray-900',
},
{
key: 'compound_tax',
label: t('settings.tax_types.compound_tax'),
tdClass: 'font-medium text-gray-900',
},
{
key: 'percent',
label: t('settings.tax_types.percent'),

View File

@ -309,6 +309,8 @@ function changeSorting(column) {
}
if (!usesLocalData.value) {
if (pagination.value)
pagination.value.currentPage = 1
mapDataToRows()
}
}
@ -326,7 +328,10 @@ async function pageChange(page) {
await mapDataToRows()
}
async function refresh() {
async function refresh(isPreservePage = false) {
if (pagination.value && !isPreservePage)
pagination.value.currentPage = 1
await mapDataToRows()
}

40
uffizzi/.env.example Normal file
View File

@ -0,0 +1,40 @@
APP_ENV=production
APP_KEY=base64:kgk/4DW1vEVy7aEvet5FPp5un6PIGe/so8H0mvoUtW0=
APP_DEBUG=true
APP_LOG_LEVEL=debug
APP_URL=http://crater.test
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=crater
DB_USERNAME=crater
DB_PASSWORD=crater
BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_DRIVER=sync
SESSION_DRIVER=cookie
SESSION_LIFETIME=1440
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_DRIVER=smtp
MAIL_HOST=
MAIL_PORT=
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=
PUSHER_APP_ID=
PUSHER_KEY=
PUSHER_SECRET=
SANCTUM_STATEFUL_DOMAINS=crater.test
SESSION_DOMAIN=crater.test
TRUSTED_PROXIES="*"
CRON_JOB_AUTH_TOKEN=""

64
uffizzi/Dockerfile Normal file
View File

@ -0,0 +1,64 @@
FROM php:8.1-fpm
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
curl \
libpng-dev \
libonig-dev \
libxml2-dev \
zip \
unzip \
libzip-dev \
libmagickwand-dev \
mariadb-client \
npm
# Clear cache
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
RUN pecl install imagick \
&& docker-php-ext-enable imagick
# Install PHP extensions
RUN docker-php-ext-install pdo_mysql mbstring zip exif pcntl bcmath gd
# Get latest Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Create system user to run Composer and Artisan Commands
RUN useradd -G www-data,root -u 1000 -d /home/crater-user crater-user
RUN mkdir -p /home/crater-user/.composer && \
chown -R crater-user:crater-user /home/crater-user
# Mounted volumes
COPY ./ /var/www
COPY ./docker-compose/php/uploads.ini /usr/local/etc/php/conf.d/uploads.ini
COPY ./uffizzi/.env.example /var/www/.env
# Set working directory
WORKDIR /var/www
RUN chown -R crater-user:crater-user ./
RUN chmod -R 775 composer.json composer.lock \
composer.lock storage/framework/ \
storage/logs/ bootstrap/cache/ /home/crater-user/.composer
RUN chown -R $(whoami):$(whoami) /var/log/
RUN chmod -R 775 /var/log
# Cleanup manually generated build files
RUN rm -rf /var/www/public/build
RUN npm config set user 0
RUN npm config set unsafe-perm true
# Frontend bulding
RUN sed -i 's/DB_CONNECTION=mysql/DB_CONNECTION=sqlite/g' /var/www/.env
RUN sed -i 's/DB_DATABASE=crater/DB_DATABASE=\/tmp\/crater.sqlite/g' /var/www/.env
RUN touch /tmp/crater.sqlite
RUN composer install --no-interaction --prefer-dist
RUN npm i -f
RUN npm install --save-dev sass
RUN export NODE_OPTIONS="--max-old-space-size=4096" && /usr/bin/npx vite build --target=es2020
RUN sed -i 's/DB_CONNECTION=sqlite/DB_CONNECTION=mysql/g' /var/www/.env
RUN sed -i 's/DB_DATABASE=\/tmp\/crater.sqlite/DB_DATABASE=crater/g' /var/www/.env
USER crater-user

68
uffizzi/crond/Dockerfile Normal file
View File

@ -0,0 +1,68 @@
FROM php:8.1-fpm as build
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
curl \
libpng-dev \
libonig-dev \
libxml2-dev \
zip \
unzip \
libzip-dev \
libmagickwand-dev \
mariadb-client
# Clear cache
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
RUN pecl install imagick \
&& docker-php-ext-enable imagick
# Install PHP extensions
RUN docker-php-ext-install pdo_mysql mbstring zip exif pcntl bcmath gd
# Get latest Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Create system user to run Composer and Artisan Commands
RUN useradd -G www-data,root -u 1000 -d /home/crater-user crater-user
RUN mkdir -p /home/crater-user/.composer && \
chown -R crater-user:crater-user /home/crater-user
# Mounted volumes
COPY ./ /var/www
COPY ./docker-compose/php/uploads.ini /usr/local/etc/php/conf.d/uploads.ini
COPY ./uffizzi/.env.example /var/www/.env
# Set working directory
WORKDIR /var/www
RUN chown -R crater-user:crater-user ./
RUN chmod -R 775 composer.json composer.lock \
composer.lock storage/framework/ \
storage/logs/ bootstrap/cache/ /home/crater-user/.composer
RUN composer config --no-plugins allow-plugins.pestphp/pest-plugin true && \
composer install --no-interaction --prefer-dist --optimize-autoloader && \
php artisan storage:link || true && \
php artisan key:generate
FROM php:8.0-fpm-alpine
RUN apk add --no-cache \
php8-bcmath
RUN docker-php-ext-install pdo pdo_mysql bcmath
COPY docker-compose/crontab /etc/crontabs/root
# Mounted volumes
COPY --from=build /var/www /var/www
RUN chown -R $(whoami):$(whoami) /var/www/
RUN chmod -R 775 /var/www/
RUN chown -R $(whoami):$(whoami) /var/log/
RUN chmod -R 775 /var/log/
CMD ["crond", "-f"]

View File

@ -0,0 +1,58 @@
version: '3'
x-uffizzi:
ingress:
service: nginx
port: 80
services:
app:
image: "${APP_IMAGE}"
restart: unless-stopped
working_dir: /var/www/
command: ["-c","
composer config --no-plugins allow-plugins.pestphp/pest-plugin true &&
composer install --no-interaction --prefer-dist --optimize-autoloader &&
php artisan storage:link || true &&
php artisan key:generate --force &&
php-fpm",
]
entrypoint: /bin/sh
depends_on:
- db
deploy:
resources:
limits:
memory: 1000m
db:
image: mariadb
restart: always
environment:
MYSQL_USER: crater
MYSQL_PASSWORD: crater
MYSQL_DATABASE: crater
MYSQL_ROOT_PASSWORD: crater
ports:
- '33006:3306'
deploy:
resources:
limits:
memory: 500m
nginx:
image: "${NGINX_IMAGE}"
restart: unless-stopped
ports:
- 80:80
depends_on:
- app
resources:
limits:
memory: 500m
cron:
image: "${CROND_IMAGE}"
restart: always

9
uffizzi/nginx/Dockerfile Normal file
View File

@ -0,0 +1,9 @@
ARG BASE_IMAGE
FROM $BASE_IMAGE as build
FROM nginx:1.17-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY --from=build /var/www /var/www
COPY ./uffizzi/nginx/nginx /etc/nginx/conf.d/

View File

@ -0,0 +1,22 @@
server {
client_max_body_size 64M;
listen 80;
index index.php index.html;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /var/www/public;
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass localhost:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_read_timeout 300;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
gzip_static on;
}
}