Invoice time support (#269)

* Changed invoice date to datetime

* Fixed code style errors

* Update TimeFormatsController.php

* Update TimeFormatter.php

* Update TimeFormatsController namespace

* Fix missing comma in language file

* Fix formatting

---------

Co-authored-by: troky <troky2001@yahoo.com>
This commit is contained in:
Darko Gjorgjijoski
2025-01-12 13:32:47 +01:00
committed by GitHub
parent 32e03b98a3
commit f52b73f517
14 changed files with 242 additions and 3 deletions

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\V1\Admin\General;
use App\Http\Controllers\Controller;
use App\Space\TimeFormatter;
use Illuminate\Http\Request;
class TimeFormatsController extends Controller
{
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
*/
public function __invoke(Request $request)
{
return response()->json([
'time_formats' => TimeFormatter::get_list(),
]);
}
}

View File

@@ -46,6 +46,16 @@ class CloneInvoiceController extends Controller
$exchange_rate = $invoice->exchange_rate; $exchange_rate = $invoice->exchange_rate;
$dateFormat = 'Y-m-d';
$invoiceTimeEnabled = CompanySetting::getSetting(
'invoice_use_time',
$request->header('company')
);
if ($invoiceTimeEnabled === 'YES') {
$dateFormat .= ' H:i';
}
$newInvoice = Invoice::create([ $newInvoice = Invoice::create([
'invoice_date' => $date->format('Y-m-d'), 'invoice_date' => $date->format('Y-m-d'),
'due_date' => $due_date, 'due_date' => $due_date,

View File

@@ -38,6 +38,15 @@ class CompanySettingRequest extends FormRequest
'carbon_date_format' => [ 'carbon_date_format' => [
'required', 'required',
], ],
'moment_time_format' => [
'required',
],
'carbon_time_format' => [
'required',
],
'invoice_use_time' => [
'required',
],
]; ];
} }
} }

View File

@@ -227,6 +227,9 @@ class Company extends Model implements HasMedia
'fiscal_year' => '1-12', 'fiscal_year' => '1-12',
'carbon_date_format' => 'Y/m/d', 'carbon_date_format' => 'Y/m/d',
'moment_date_format' => 'YYYY/MM/DD', 'moment_date_format' => 'YYYY/MM/DD',
'carbon_time_format' => 'H:i',
'moment_time_format' => 'HH:mm',
'invoice_use_time' => 'NO',
'notification_email' => 'noreply@invoiceshelf.com', 'notification_email' => 'noreply@invoiceshelf.com',
'notify_invoice_viewed' => 'NO', 'notify_invoice_viewed' => 'NO',
'notify_estimate_viewed' => 'NO', 'notify_estimate_viewed' => 'NO',

View File

@@ -195,6 +195,12 @@ class Invoice extends Model implements HasMedia
public function getFormattedInvoiceDateAttribute($value) public function getFormattedInvoiceDateAttribute($value)
{ {
$dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id); $dateFormat = CompanySetting::getSetting('carbon_date_format', $this->company_id);
$timeFormat = CompanySetting::getSetting('carbon_time_format', $this->company_id);
$invoiceTimeEnabled = CompanySetting::getSetting('invoice_use_time', $this->company_id);
if ($invoiceTimeEnabled === 'YES') {
$dateFormat .= ' '.$timeFormat;
}
return Carbon::parse($this->invoice_date)->translatedFormat($dateFormat); return Carbon::parse($this->invoice_date)->translatedFormat($dateFormat);
} }

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Space;
use Carbon\Carbon;
class TimeFormatter
{
protected static $formats = [
[
'carbon_format' => 'H:i',
'moment_format' => 'HH:mm',
],
[
'carbon_format' => 'g:i a',
'moment_format' => 'h:mm a',
],
];
public static function get_list()
{
$new = [];
foreach (static::$formats as $format) {
$new[] = [
'display_time' => Carbon::now()->format($format['carbon_format']),
'carbon_format_value' => $format['carbon_format'],
'moment_format_value' => $format['moment_format'],
];
}
return $new;
}
}

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('invoices', function (Blueprint $table) {
$table->datetime('invoice_date')->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('invoices', function (Blueprint $table) {
$table->date('invoice_date')->change();
});
}
};

View File

@@ -864,6 +864,7 @@
"primary_currency": "Primary Currency", "primary_currency": "Primary Currency",
"timezone": "Time Zone", "timezone": "Time Zone",
"date_format": "Date Format", "date_format": "Date Format",
"time_format": "Time Format",
"currencies": { "currencies": {
"title": "Currencies", "title": "Currencies",
"currency": "Currency | Currencies", "currency": "Currency | Currencies",
@@ -990,6 +991,7 @@
"delimiter_description": "Single character for specifying the boundary between 2 separate components. By default its set to -", "delimiter_description": "Single character for specifying the boundary between 2 separate components. By default its set to -",
"delimiter_param_label": "Delimiter Value", "delimiter_param_label": "Delimiter Value",
"date_format": "Date Format", "date_format": "Date Format",
"time_format": "Time Format",
"date_format_description": "A local date and time field which accepts a format parameter. The default format: 'Y' renders the current year.", "date_format_description": "A local date and time field which accepts a format parameter. The default format: 'Y' renders the current year.",
"date_format_param_label": "Format", "date_format_param_label": "Format",
"sequence": "Sequence", "sequence": "Sequence",
@@ -1234,6 +1236,7 @@
"time_zone": "Time Zone", "time_zone": "Time Zone",
"fiscal_year": "Financial Year", "fiscal_year": "Financial Year",
"date_format": "Date Format", "date_format": "Date Format",
"time_format": "Time Fromat",
"discount_setting": "Discount Setting", "discount_setting": "Discount Setting",
"discount_per_item": "Discount Per Item ", "discount_per_item": "Discount Per Item ",
"discount_setting_description": "Enable this if you want to add Discount to individual invoice items. By default, Discount is added directly to the invoice.", "discount_setting_description": "Enable this if you want to add Discount to individual invoice items. By default, Discount is added directly to the invoice.",
@@ -1246,6 +1249,7 @@
"select_language": "Select Language", "select_language": "Select Language",
"select_time_zone": "Select Time Zone", "select_time_zone": "Select Time Zone",
"select_date_format": "Select Date Format", "select_date_format": "Select Date Format",
"select_time_format": "Select Time Format",
"select_financial_year": "Select Financial Year", "select_financial_year": "Select Financial Year",
"recurring_invoice_status": "Recurring Invoice Status", "recurring_invoice_status": "Recurring Invoice Status",
"create_status": "Create Status", "create_status": "Create Status",
@@ -1254,6 +1258,8 @@
"update_status": "Update Status", "update_status": "Update Status",
"completed": "Completed", "completed": "Completed",
"company_currency_unchangeable": "Company currency cannot be changed", "company_currency_unchangeable": "Company currency cannot be changed",
"invoice_use_time": "Use time in invoices",
"invoice_use_time_description": "Enable this if you want to select exact invoice time.",
"fiscal_years": { "fiscal_years": {
"january_december": "January - December", "january_december": "January - December",
"february_january": "February - January", "february_january": "February - January",
@@ -1422,6 +1428,7 @@
"time_zone": "Time Zone", "time_zone": "Time Zone",
"fiscal_year": "Financial Year", "fiscal_year": "Financial Year",
"date_format": "Date Format", "date_format": "Date Format",
"time_format": "Time Format",
"from_address": "From Address", "from_address": "From Address",
"username": "Username", "username": "Username",
"next": "Next", "next": "Next",

View File

@@ -21,6 +21,7 @@ export const useGlobalStore = (useWindow = false) => {
// Global Lists // Global Lists
timeZones: [], timeZones: [],
dateFormats: [], dateFormats: [],
timeFormats: [],
currencies: [], currencies: [],
countries: [], countries: [],
languages: [], languages: [],
@@ -156,6 +157,25 @@ export const useGlobalStore = (useWindow = false) => {
}) })
}, },
fetchTimeFormats() {
return new Promise((resolve, reject) => {
if (this.timeFormats.length) {
resolve(this.timeFormats)
} else {
axios
.get('/api/v1/time/formats')
.then((response) => {
this.timeFormats = response.data.time_formats
resolve(response)
})
.catch((err) => {
handleError(err)
reject(err)
})
}
})
},
fetchTimeZones() { fetchTimeZones() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (this.timeZones.length) { if (this.timeZones.length) {

View File

@@ -499,7 +499,13 @@ export const useInvoiceStore = (useWindow = false) => {
this.newInvoice.sales_tax_address_type = companyStore.selectedCompanySettings.sales_tax_address_type this.newInvoice.sales_tax_address_type = companyStore.selectedCompanySettings.sales_tax_address_type
this.newInvoice.discount_per_item = this.newInvoice.discount_per_item =
companyStore.selectedCompanySettings.discount_per_item companyStore.selectedCompanySettings.discount_per_item
this.newInvoice.invoice_date = moment().format('YYYY-MM-DD')
let dateFormat = 'YYYY-MM-DD';
if (companyStore.selectedCompanySettings.invoice_use_time === 'YES') {
dateFormat += ' HH:mm'
}
this.newInvoice.invoice_date = moment().format(dateFormat)
if (companyStore.selectedCompanySettings.invoice_set_due_date_automatically === 'YES') { if (companyStore.selectedCompanySettings.invoice_set_due_date_automatically === 'YES') {
this.newInvoice.due_date = moment() this.newInvoice.due_date = moment()
.add(companyStore.selectedCompanySettings.invoice_due_date_days, 'days') .add(companyStore.selectedCompanySettings.invoice_due_date_days, 'days')

View File

@@ -20,6 +20,8 @@
:content-loading="isLoading" :content-loading="isLoading"
:calendar-button="true" :calendar-button="true"
calendar-button-icon="calendar" calendar-button-icon="calendar"
:enableTime="enableTime"
:time24hr="time24h"
/> />
</BaseInputGroup> </BaseInputGroup>
@@ -61,8 +63,10 @@
</template> </template>
<script setup> <script setup>
import { computed } from 'vue'
import ExchangeRateConverter from '@/scripts/admin/components/estimate-invoice-common/ExchangeRateConverter.vue' import ExchangeRateConverter from '@/scripts/admin/components/estimate-invoice-common/ExchangeRateConverter.vue'
import { useInvoiceStore } from '@/scripts/admin/stores/invoice' import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
import { useCompanyStore } from '@/scripts/admin/stores/company'
const props = defineProps({ const props = defineProps({
v: { v: {
@@ -80,4 +84,17 @@ const props = defineProps({
}) })
const invoiceStore = useInvoiceStore() const invoiceStore = useInvoiceStore()
const companyStore = useCompanyStore()
const enableTime = computed(() => {
return (
companyStore.selectedCompanySettings.invoice_use_time === 'YES'
);
})
const time24h = computed(() => {
return (
companyStore.selectedCompanySettings.carbon_time_format.indexOf('H') > -1
);
})
</script> </script>

View File

@@ -80,10 +80,11 @@
label="display_date" label="display_date"
value-prop="carbon_format_value" value-prop="carbon_format_value"
track-by="display_date" track-by="display_date"
searchable :searchable="true"
:invalid="v$.carbon_date_format.$error" :invalid="v$.carbon_date_format.$error"
class="w-full" class="w-full"
/> />
</BaseInputGroup> </BaseInputGroup>
<BaseInputGroup <BaseInputGroup
@@ -104,8 +105,36 @@
class="w-full" class="w-full"
/> />
</BaseInputGroup> </BaseInputGroup>
<BaseInputGroup
:label="$t('settings.preferences.time_format')"
:content-loading="isFetchingInitialData"
:error="
v$.carbon_time_format.$error &&
v$.carbon_time_format.$errors[0].$message
"
required
>
<BaseMultiselect
v-model="settingsForm.carbon_time_format"
:content-loading="isFetchingInitialData"
:options="globalStore.timeFormats"
label="display_time"
value-prop="carbon_format_value"
track-by="display_time"
:searchable="true"
:invalid="v$.carbon_time_format.$error"
class="w-full"
/>
</BaseInputGroup>
</BaseInputGrid> </BaseInputGrid>
<BaseSwitchSection
v-model="invoiceUseTimeField"
:title="$t('settings.preferences.invoice_use_time')"
:description="$t('settings.preferences.invoice_use_time_description')"
/>
<BaseButton <BaseButton
:content-loading="isFetchingInitialData" :content-loading="isFetchingInitialData"
:disabled="isSaving" :disabled="isSaving"
@@ -218,6 +247,33 @@ watch(
} }
) )
watch(
() => settingsForm.carbon_time_format,
(val) => {
if (val) {
const timeFormatObject = globalStore.timeFormats.find((d) => {
return d.carbon_format_value === val
})
settingsForm.moment_time_format = timeFormatObject.moment_format_value
}
}
)
const invoiceUseTimeField = computed({
get: () => {
return settingsForm.invoice_use_time === 'YES'
},
set: async (newValue) => {
const value = newValue ? 'YES' : 'NO'
let data = {
settings: {
invoice_use_time: value,
},
}
settingsForm.invoice_use_time = value
}
})
const discountPerItemField = computed({ const discountPerItemField = computed({
get: () => { get: () => {
return settingsForm.discount_per_item === 'YES' return settingsForm.discount_per_item === 'YES'
@@ -271,12 +327,21 @@ const rules = computed(() => {
moment_date_format: { moment_date_format: {
required: helpers.withMessage(t('validation.required'), required), required: helpers.withMessage(t('validation.required'), required),
}, },
carbon_time_format: {
required: helpers.withMessage(t('validation.required'), required),
},
moment_time_format: {
required: helpers.withMessage(t('validation.required'), required),
},
time_zone: { time_zone: {
required: helpers.withMessage(t('validation.required'), required), required: helpers.withMessage(t('validation.required'), required),
}, },
fiscal_year: { fiscal_year: {
required: helpers.withMessage(t('validation.required'), required), required: helpers.withMessage(t('validation.required'), required),
}, },
invoice_use_time: {
required: helpers.withMessage(t('validation.required'), required),
},
} }
}) })
@@ -292,6 +357,7 @@ async function setInitialData() {
Promise.all([ Promise.all([
globalStore.fetchCurrencies(), globalStore.fetchCurrencies(),
globalStore.fetchDateFormats(), globalStore.fetchDateFormats(),
globalStore.fetchTimeFormats(),
globalStore.fetchTimeZones(), globalStore.fetchTimeZones(),
]).then(([res1]) => { ]).then(([res1]) => {
isFetchingInitialData.value = false isFetchingInitialData.value = false

View File

@@ -254,6 +254,14 @@ const carbonFormat = computed(() => {
return companyStore.selectedCompanySettings?.carbon_date_format return companyStore.selectedCompanySettings?.carbon_date_format
}) })
const carbonFormatWithTime = computed(() => {
let format = companyStore.selectedCompanySettings?.carbon_date_format
if (companyStore.selectedCompanySettings?.invoice_use_time === 'YES') {
format += ' ' + companyStore.selectedCompanySettings?.carbon_time_format
}
return format.replace("g", "h").replace("a", "K");
})
const hasIconSlot = computed(() => { const hasIconSlot = computed(() => {
return !!slots.icon return !!slots.icon
}) })
@@ -301,7 +309,7 @@ watch(
config.altFormat = carbonFormat.value ? carbonFormat.value : 'd M Y' config.altFormat = carbonFormat.value ? carbonFormat.value : 'd M Y'
} else { } else {
config.altFormat = carbonFormat.value config.altFormat = carbonFormat.value
? `${carbonFormat.value} H:i ` ? `${carbonFormatWithTime.value}`
: 'd M Y H:i' : 'd M Y H:i'
} }
}, },

View File

@@ -39,6 +39,7 @@ use App\Http\Controllers\V1\Admin\General\NotesController;
use App\Http\Controllers\V1\Admin\General\NumberPlaceholdersController; use App\Http\Controllers\V1\Admin\General\NumberPlaceholdersController;
use App\Http\Controllers\V1\Admin\General\SearchController; use App\Http\Controllers\V1\Admin\General\SearchController;
use App\Http\Controllers\V1\Admin\General\SearchUsersController; use App\Http\Controllers\V1\Admin\General\SearchUsersController;
use App\Http\Controllers\V1\Admin\General\TimeFormatsController;
use App\Http\Controllers\V1\Admin\General\TimezonesController; use App\Http\Controllers\V1\Admin\General\TimezonesController;
use App\Http\Controllers\V1\Admin\Invoice\ChangeInvoiceStatusController; use App\Http\Controllers\V1\Admin\Invoice\ChangeInvoiceStatusController;
use App\Http\Controllers\V1\Admin\Invoice\CloneInvoiceController; use App\Http\Controllers\V1\Admin\Invoice\CloneInvoiceController;
@@ -230,6 +231,8 @@ Route::prefix('/v1')->group(function () {
Route::get('/date/formats', DateFormatsController::class); Route::get('/date/formats', DateFormatsController::class);
Route::get('/time/formats', TimeFormatsController::class);
Route::get('/next-number', NextNumberController::class); Route::get('/next-number', NextNumberController::class);
Route::get('/number-placeholders', NumberPlaceholdersController::class); Route::get('/number-placeholders', NumberPlaceholdersController::class);