Merge pull request '3622-feat(client_defaulter): implemented smart-table' (#881) from 3622-client_defaulter into dev
gitea/salix/pipeline/head This commit looks good
Details
gitea/salix/pipeline/head This commit looks good
Details
Reviewed-on: #881 Reviewed-by: Joan Sanchez <joan@verdnatura.es>
This commit is contained in:
commit
6a29f31bb5
|
@ -305,11 +305,11 @@ export default {
|
||||||
anyCreditInsuranceLine: 'vn-client-credit-insurance-insurance-index vn-tbody > vn-tr',
|
anyCreditInsuranceLine: 'vn-client-credit-insurance-insurance-index vn-tbody > vn-tr',
|
||||||
},
|
},
|
||||||
clientDefaulter: {
|
clientDefaulter: {
|
||||||
anyClient: 'vn-client-defaulter-index vn-tbody > vn-tr',
|
anyClient: 'vn-client-defaulter-index tbody > tr',
|
||||||
firstClientName: 'vn-client-defaulter-index vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(2) > span',
|
firstClientName: 'vn-client-defaulter-index tbody > tr:nth-child(1) > td:nth-child(2) > span',
|
||||||
firstSalesPersonName: 'vn-client-defaulter-index vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(3) > span',
|
firstSalesPersonName: 'vn-client-defaulter-index tbody > tr:nth-child(1) > td:nth-child(3) > span',
|
||||||
firstObservation: 'vn-client-defaulter-index vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(6) > vn-textarea[ng-model="defaulter.observation"]',
|
firstObservation: 'vn-client-defaulter-index tbody > tr:nth-child(1) > td:nth-child(6) > vn-textarea[ng-model="defaulter.observation"]',
|
||||||
allDefaulterCheckbox: 'vn-client-defaulter-index vn-thead vn-multi-check',
|
allDefaulterCheckbox: 'vn-client-defaulter-index thead vn-multi-check',
|
||||||
addObservationButton: 'vn-client-defaulter-index vn-button[icon="icon-notes"]',
|
addObservationButton: 'vn-client-defaulter-index vn-button[icon="icon-notes"]',
|
||||||
observation: '.vn-dialog.shown vn-textarea[ng-model="$ctrl.defaulter.observation"]',
|
observation: '.vn-dialog.shown vn-textarea[ng-model="$ctrl.defaulter.observation"]',
|
||||||
saveButton: 'button[response="accept"]'
|
saveButton: 'button[response="accept"]'
|
||||||
|
|
|
@ -28,8 +28,8 @@ describe('Client defaulter path', () => {
|
||||||
const salesPersonName =
|
const salesPersonName =
|
||||||
await page.waitToGetProperty(selectors.clientDefaulter.firstSalesPersonName, 'innerText');
|
await page.waitToGetProperty(selectors.clientDefaulter.firstSalesPersonName, 'innerText');
|
||||||
|
|
||||||
expect(clientName).toEqual('Ororo Munroe');
|
expect(clientName).toEqual('Batman');
|
||||||
expect(salesPersonName).toEqual('salesPerson');
|
expect(salesPersonName).toEqual('salesPersonNick');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should first observation not changed', async() => {
|
it('should first observation not changed', async() => {
|
||||||
|
@ -65,6 +65,7 @@ describe('Client defaulter path', () => {
|
||||||
|
|
||||||
it('should first observation changed', async() => {
|
it('should first observation changed', async() => {
|
||||||
const message = await page.waitForSnackbar();
|
const message = await page.waitForSnackbar();
|
||||||
|
await page.waitForSelector(selectors.clientDefaulter.firstObservation);
|
||||||
const result = await page.waitToGetProperty(selectors.clientDefaulter.firstObservation, 'value');
|
const result = await page.waitToGetProperty(selectors.clientDefaulter.firstObservation, 'value');
|
||||||
|
|
||||||
expect(message.text).toContain('Observation saved!');
|
expect(message.text).toContain('Observation saved!');
|
||||||
|
|
|
@ -56,18 +56,18 @@ module.exports = Self => {
|
||||||
FROM (
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
DISTINCT c.id clientFk,
|
DISTINCT c.id clientFk,
|
||||||
c.name clientName,
|
c.socialName clientName,
|
||||||
c.salesPersonFk,
|
c.salesPersonFk,
|
||||||
u.name salesPersonName,
|
u.nickname salesPersonName,
|
||||||
d.amount,
|
d.amount,
|
||||||
co.created,
|
co.created,
|
||||||
CONCAT(DATE(co.created), ' ', co.text) observation,
|
co.text observation,
|
||||||
uw.id workerFk,
|
uw.id workerFk,
|
||||||
uw.name workerName,
|
uw.nickname workerName,
|
||||||
c.creditInsurance,
|
c.creditInsurance,
|
||||||
d.defaulterSinced
|
d.defaulterSinced
|
||||||
FROM vn.defaulter d
|
FROM vn.defaulter d
|
||||||
JOIN vn.client c ON c.id = d.clientFk
|
JOIN vn.client c ON c.id = d.clientFk
|
||||||
LEFT JOIN vn.clientObservation co ON co.clientFk = c.id
|
LEFT JOIN vn.clientObservation co ON co.clientFk = c.id
|
||||||
LEFT JOIN account.user u ON u.id = c.salesPersonFk
|
LEFT JOIN account.user u ON u.id = c.salesPersonFk
|
||||||
LEFT JOIN account.user uw ON uw.id = co.workerFk
|
LEFT JOIN account.user uw ON uw.id = co.workerFk
|
||||||
|
|
|
@ -47,12 +47,12 @@ describe('defaulter filter()', () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const options = {transaction: tx};
|
const options = {transaction: tx};
|
||||||
const ctx = {req: {accessToken: {userId: authUserId}}, args: {search: 'bruce'}};
|
const ctx = {req: {accessToken: {userId: authUserId}}, args: {search: 'spider'}};
|
||||||
|
|
||||||
const result = await models.Defaulter.filter(ctx, null, options);
|
const result = await models.Defaulter.filter(ctx, null, options);
|
||||||
const firstRow = result[0];
|
const firstRow = result[0];
|
||||||
|
|
||||||
expect(firstRow.clientName).toEqual('Bruce Wayne');
|
expect(firstRow.clientName).toEqual('Spider man');
|
||||||
|
|
||||||
await tx.rollback();
|
await tx.rollback();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -15,17 +15,18 @@
|
||||||
model="model">
|
model="model">
|
||||||
</vn-searchbar>
|
</vn-searchbar>
|
||||||
</vn-portal>
|
</vn-portal>
|
||||||
<vn-data-viewer
|
<vn-card>
|
||||||
model="model"
|
<smart-table
|
||||||
class="vn-w-xl">
|
model="model"
|
||||||
<vn-card>
|
options="$ctrl.smartTableOptions"
|
||||||
<vn-tool-bar>
|
expr-builder="$ctrl.exprBuilder(param, value)">
|
||||||
<div class="vn-pa-md">
|
<slot-actions>
|
||||||
|
<div>
|
||||||
<div class="totalBox" style="text-align: center;">
|
<div class="totalBox" style="text-align: center;">
|
||||||
<h6 translate>Total</h6>
|
<h6 translate>Total</h6>
|
||||||
<vn-label-value
|
<vn-label-value
|
||||||
label="Balance due"
|
label="Balance due"
|
||||||
value="{{$ctrl.balanceDueTotal}} €">
|
value="{{$ctrl.balanceDueTotal | currency: 'EUR': 2}}">
|
||||||
</vn-label-value>
|
</vn-label-value>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,90 +39,109 @@
|
||||||
icon="icon-notes">
|
icon="icon-notes">
|
||||||
</vn-button>
|
</vn-button>
|
||||||
</div>
|
</div>
|
||||||
</vn-tool-bar>
|
</slot-actions>
|
||||||
<vn-table model="model">
|
<slot-table>
|
||||||
<vn-thead>
|
<table>
|
||||||
<vn-tr>
|
<thead>
|
||||||
<vn-th shrink>
|
<tr>
|
||||||
<vn-multi-check
|
<th shrink>
|
||||||
model="model">
|
<vn-multi-check
|
||||||
</vn-multi-check>
|
model="model">
|
||||||
</vn-th>
|
</vn-multi-check>
|
||||||
<vn-th field="clientName">Client</vn-th>
|
</th>
|
||||||
<vn-th field="salesPersonFk">Comercial</vn-th>
|
<th field="clientName">
|
||||||
<vn-th
|
<span translate>Client</span>
|
||||||
field="amount"
|
</th>
|
||||||
vn-tooltip="Balance due"
|
<th field="salesPersonFk">
|
||||||
number>
|
<span translate>Comercial</span>
|
||||||
Balance D.
|
</th>
|
||||||
</vn-th>
|
<th
|
||||||
<vn-th
|
field="amount"
|
||||||
vn-tooltip="Worker who made the last observation"
|
vn-tooltip="Balance due">
|
||||||
shrink>
|
<span translate>Balance D.</span>
|
||||||
Author
|
</th>
|
||||||
</vn-th>
|
<th
|
||||||
<vn-th expand>Last observation</vn-th>
|
field="workerFk"
|
||||||
<vn-th
|
vn-tooltip="Worker who made the last observation">
|
||||||
vn-tooltip="Credit insurance"
|
<span translate>Author</span>
|
||||||
number>
|
</th>
|
||||||
Credit I.
|
<th field="observation" expand>
|
||||||
</vn-th>
|
<span translate>Last observation</span>
|
||||||
<vn-th shrink-datetime>From</vn-th>
|
</th>
|
||||||
</vn-tr>
|
<th
|
||||||
</vn-thead>
|
vn-tooltip="Last observation date"
|
||||||
<vn-tbody>
|
field="created"
|
||||||
<vn-tr ng-repeat="defaulter in defaulters">
|
shrink-datetime>
|
||||||
<vn-td shrink>
|
<span translate>Last observation D.</span>
|
||||||
<vn-check
|
</th>
|
||||||
ng-model="defaulter.checked"
|
<th
|
||||||
vn-click-stop>
|
vn-tooltip="Credit insurance"
|
||||||
</vn-check>
|
field="creditInsurance" >
|
||||||
</vn-td>
|
<span translate>Credit I.</span>
|
||||||
<vn-td>
|
</th>
|
||||||
<span
|
<th field="defaulterSinced">
|
||||||
vn-click-stop="clientDescriptor.show($event, defaulter.clientFk)"
|
<span translate>From</span>
|
||||||
title ="{{::defaulter.clientName}}"
|
</th>
|
||||||
class="link">
|
</tr>
|
||||||
{{::defaulter.clientName}}
|
</thead>
|
||||||
</span>
|
<tbody>
|
||||||
</vn-td>
|
<tr ng-repeat="defaulter in defaulters">
|
||||||
<vn-td>
|
<td shrink>
|
||||||
<span
|
<vn-check
|
||||||
title="{{::defaulter.salesPersonName}}"
|
ng-model="defaulter.checked"
|
||||||
vn-click-stop="workerDescriptor.show($event, defaulter.salesPersonFk)"
|
vn-click-stop>
|
||||||
class="link" >
|
</vn-check>
|
||||||
{{::defaulter.salesPersonName | dashIfEmpty}}
|
</td>
|
||||||
</span>
|
<td title="{{::defaulter.clientName}}">
|
||||||
</vn-td>
|
<span
|
||||||
<vn-td number>{{::defaulter.amount}}</vn-td>
|
vn-click-stop="clientDescriptor.show($event, defaulter.clientFk)"
|
||||||
<vn-td shrink>
|
title ="{{::defaulter.clientName}}"
|
||||||
<span
|
class="link">
|
||||||
title="{{::defaulter.workerName}}"
|
{{::defaulter.clientName}}
|
||||||
vn-click-stop="workerDescriptor.show($event, defaulter.workerFk)"
|
</span>
|
||||||
class="link" >
|
</td>
|
||||||
{{::defaulter.workerName | dashIfEmpty}}
|
<td>
|
||||||
</span>
|
<span
|
||||||
</vn-td>
|
title="{{::defaulter.salesPersonName}}"
|
||||||
<vn-td expand>
|
vn-click-stop="workerDescriptor.show($event, defaulter.salesPersonFk)"
|
||||||
<vn-textarea
|
class="link">
|
||||||
vn-three
|
{{::defaulter.salesPersonName | dashIfEmpty}}
|
||||||
disabled="true"
|
</span>
|
||||||
label="Observation"
|
</td>
|
||||||
ng-model="defaulter.observation">
|
<td>{{::defaulter.amount | currency: 'EUR': 2}}</td>
|
||||||
</vn-textarea>
|
<td>
|
||||||
</vn-td>
|
<span
|
||||||
<vn-td number>{{::defaulter.creditInsurance}}</vn-td>
|
title="{{::defaulter.workerName}}"
|
||||||
<vn-td shrink-datetime>{{::defaulter.defaulterSinced | date: 'dd/MM/yyyy'}}</vn-td>
|
vn-click-stop="workerDescriptor.show($event, defaulter.workerFk)"
|
||||||
</vn-tr>
|
class="link">
|
||||||
</vn-tbody>
|
{{::defaulter.workerName | dashIfEmpty}}
|
||||||
</vn-table>
|
</span>
|
||||||
</vn-card>
|
</td>
|
||||||
</vn-data-viewer>
|
<td expand>
|
||||||
|
<vn-textarea
|
||||||
|
vn-three
|
||||||
|
disabled="true"
|
||||||
|
ng-model="defaulter.observation">
|
||||||
|
</vn-textarea>
|
||||||
|
</td>
|
||||||
|
<td shrink-datetime>
|
||||||
|
<span class="chip {{::$ctrl.chipColor(defaulter.created)}}">
|
||||||
|
{{::defaulter.created | date: 'dd/MM/yyyy'}}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{::defaulter.creditInsurance | currency: 'EUR': 2}}</td>
|
||||||
|
<td>{{::defaulter.defaulterSinced | date: 'dd/MM/yyyy'}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</slot-table>
|
||||||
|
</smart-table>
|
||||||
|
</vn-card>
|
||||||
<vn-client-descriptor-popover
|
<vn-client-descriptor-popover
|
||||||
vn-id="clientDescriptor">
|
vn-id="client-descriptor">
|
||||||
</vn-client-descriptor-popover>
|
</vn-client-descriptor-popover>
|
||||||
<vn-worker-descriptor-popover
|
<vn-worker-descriptor-popover
|
||||||
vn-id="workerDescriptor">
|
vn-id="worker-descriptor">
|
||||||
</vn-worker-descriptor-popover>
|
</vn-worker-descriptor-popover>
|
||||||
<vn-popup vn-id="dialog-summary-client">
|
<vn-popup vn-id="dialog-summary-client">
|
||||||
<vn-client-summary
|
<vn-client-summary
|
||||||
|
@ -129,37 +149,6 @@
|
||||||
</vn-client-summary>
|
</vn-client-summary>
|
||||||
</vn-popup>
|
</vn-popup>
|
||||||
|
|
||||||
<!--Context menu-->
|
|
||||||
<vn-contextmenu vn-id="contextmenu" targets="['vn-data-viewer']" model="model"
|
|
||||||
expr-builder="$ctrl.exprBuilder(param, value)">
|
|
||||||
<slot-menu>
|
|
||||||
<vn-item translate
|
|
||||||
ng-if="contextmenu.isFilterAllowed()"
|
|
||||||
ng-click="contextmenu.filterBySelection()">
|
|
||||||
Filter by selection
|
|
||||||
</vn-item>
|
|
||||||
<vn-item translate
|
|
||||||
ng-if="contextmenu.isFilterAllowed()"
|
|
||||||
ng-click="contextmenu.excludeSelection()">
|
|
||||||
Exclude selection
|
|
||||||
</vn-item>
|
|
||||||
<vn-item translate
|
|
||||||
ng-if="contextmenu.isFilterAllowed()"
|
|
||||||
ng-click="contextmenu.removeFilter()">
|
|
||||||
Remove filter
|
|
||||||
</vn-item>
|
|
||||||
<vn-item translate
|
|
||||||
ng-click="contextmenu.removeAllFilters()">
|
|
||||||
Remove all filters
|
|
||||||
</vn-item>
|
|
||||||
<vn-item translate
|
|
||||||
ng-if="contextmenu.isActionAllowed()"
|
|
||||||
ng-click="contextmenu.copyValue()">
|
|
||||||
Copy value
|
|
||||||
</vn-item>
|
|
||||||
</slot-menu>
|
|
||||||
</vn-contextmenu>
|
|
||||||
|
|
||||||
<!-- Dialog of add notes button -->
|
<!-- Dialog of add notes button -->
|
||||||
<vn-dialog
|
<vn-dialog
|
||||||
vn-id="notesDialog"
|
vn-id="notesDialog"
|
||||||
|
|
|
@ -6,17 +6,61 @@ export default class Controller extends Section {
|
||||||
constructor($element, $) {
|
constructor($element, $) {
|
||||||
super($element, $);
|
super($element, $);
|
||||||
this.defaulter = {};
|
this.defaulter = {};
|
||||||
|
|
||||||
|
this.smartTableOptions = {
|
||||||
|
activeButtons: {
|
||||||
|
search: true
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
field: 'clientName',
|
||||||
|
autocomplete: {
|
||||||
|
url: 'Clients',
|
||||||
|
showField: 'socialName',
|
||||||
|
valueField: 'socialName'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'salesPersonFk',
|
||||||
|
autocomplete: {
|
||||||
|
url: 'Workers/activeWithInheritedRole',
|
||||||
|
where: `{role: 'salesPerson'}`,
|
||||||
|
searchFunction: '{firstName: $search}',
|
||||||
|
showField: 'nickname',
|
||||||
|
valueField: 'id',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'workerFk',
|
||||||
|
autocomplete: {
|
||||||
|
url: 'Workers/activeWithInheritedRole',
|
||||||
|
searchFunction: '{firstName: $search}',
|
||||||
|
showField: 'nickname',
|
||||||
|
valueField: 'id',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'observation',
|
||||||
|
searchable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'created',
|
||||||
|
searchable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'defaulterSinced',
|
||||||
|
searchable: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
get balanceDueTotal() {
|
get balanceDueTotal() {
|
||||||
let balanceDueTotal = 0;
|
let balanceDueTotal = 0;
|
||||||
|
const defaulters = this.$.model.data || [];
|
||||||
|
|
||||||
if (this.checked.length > 0) {
|
for (let defaulter of defaulters)
|
||||||
for (let defaulter of this.checked)
|
balanceDueTotal += defaulter.amount;
|
||||||
balanceDueTotal += defaulter.amount;
|
|
||||||
|
|
||||||
return balanceDueTotal;
|
|
||||||
}
|
|
||||||
|
|
||||||
return balanceDueTotal;
|
return balanceDueTotal;
|
||||||
}
|
}
|
||||||
|
@ -32,6 +76,22 @@ export default class Controller extends Section {
|
||||||
return checkedLines;
|
return checkedLines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chipColor(date) {
|
||||||
|
const day = 24 * 60 * 60 * 1000;
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const observationShipped = new Date(date);
|
||||||
|
observationShipped.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const difference = today - observationShipped;
|
||||||
|
|
||||||
|
if (difference > (day * 20))
|
||||||
|
return 'alert';
|
||||||
|
if (difference > (day * 10))
|
||||||
|
return 'warning';
|
||||||
|
}
|
||||||
|
|
||||||
onResponse() {
|
onResponse() {
|
||||||
if (!this.defaulter.observation)
|
if (!this.defaulter.observation)
|
||||||
throw new UserError(`The message can't be empty`);
|
throw new UserError(`The message can't be empty`);
|
||||||
|
@ -52,7 +112,10 @@ export default class Controller extends Section {
|
||||||
|
|
||||||
exprBuilder(param, value) {
|
exprBuilder(param, value) {
|
||||||
switch (param) {
|
switch (param) {
|
||||||
|
case 'creditInsurance':
|
||||||
|
case 'amount':
|
||||||
case 'clientName':
|
case 'clientName':
|
||||||
|
case 'workerFk':
|
||||||
case 'salesPersonFk':
|
case 'salesPersonFk':
|
||||||
return {[`d.${param}`]: value};
|
return {[`d.${param}`]: value};
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,11 +39,7 @@ describe('client defaulter', () => {
|
||||||
describe('balanceDueTotal() getter', () => {
|
describe('balanceDueTotal() getter', () => {
|
||||||
it('should return balance due total', () => {
|
it('should return balance due total', () => {
|
||||||
const data = controller.$.model.data;
|
const data = controller.$.model.data;
|
||||||
data[1].checked = true;
|
const expectedAmount = data[0].amount + data[1].amount + data[2].amount;
|
||||||
data[2].checked = true;
|
|
||||||
|
|
||||||
const checkedRows = controller.checked;
|
|
||||||
const expectedAmount = checkedRows[0].amount + checkedRows[1].amount;
|
|
||||||
|
|
||||||
const result = controller.balanceDueTotal;
|
const result = controller.balanceDueTotal;
|
||||||
|
|
||||||
|
@ -51,6 +47,31 @@ describe('client defaulter', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('chipColor()', () => {
|
||||||
|
it('should return undefined when the date is the present', () => {
|
||||||
|
let today = new Date();
|
||||||
|
let result = controller.chipColor(today);
|
||||||
|
|
||||||
|
expect(result).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return warning when the date is 10 days in the past', () => {
|
||||||
|
let pastDate = new Date();
|
||||||
|
pastDate = pastDate.setDate(pastDate.getDate() - 11);
|
||||||
|
let result = controller.chipColor(pastDate);
|
||||||
|
|
||||||
|
expect(result).toEqual('warning');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return alert when the date is 20 days in the past', () => {
|
||||||
|
let pastDate = new Date();
|
||||||
|
pastDate = pastDate.setDate(pastDate.getDate() - 21);
|
||||||
|
let result = controller.chipColor(pastDate);
|
||||||
|
|
||||||
|
expect(result).toEqual('alert');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('onResponse()', () => {
|
describe('onResponse()', () => {
|
||||||
it('should return error for empty message', () => {
|
it('should return error for empty message', () => {
|
||||||
let error;
|
let error;
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
Last observation: Última observación
|
|
||||||
Add observation: Añadir observación
|
Add observation: Añadir observación
|
||||||
Search client: Buscar clientes
|
|
||||||
Add observation to all selected clients: Añadir observación a {{total}} cliente(s) seleccionado(s)
|
Add observation to all selected clients: Añadir observación a {{total}} cliente(s) seleccionado(s)
|
||||||
Credit I.: Crédito A.
|
|
||||||
Balance D.: Saldo V.
|
Balance D.: Saldo V.
|
||||||
|
Credit I.: Crédito A.
|
||||||
|
Last observation: Última observación
|
||||||
|
Last observation D.: Fecha última O.
|
||||||
|
Last observation date: Fecha última observación
|
||||||
|
Search client: Buscar clientes
|
||||||
Worker who made the last observation: Trabajador que ha realizado la última observación
|
Worker who made the last observation: Trabajador que ha realizado la última observación
|
Loading…
Reference in New Issue