Add invoice display, prevent editing on anything other than incoming tier

This commit is contained in:
Ramon Gutierrez 2018-02-05 13:14:55 +08:00
parent 075c9e7dd5
commit 887823eee0
8 changed files with 205 additions and 67 deletions

View file

@ -9,6 +9,7 @@ use App\Ramcar\WarrantyClass;
use App\Ramcar\DiscountApply; use App\Ramcar\DiscountApply;
use App\Ramcar\TradeInType; use App\Ramcar\TradeInType;
use App\Ramcar\InvoiceCriteria; use App\Ramcar\InvoiceCriteria;
use App\Ramcar\InvoiceStatus;
use App\Entity\JobOrder; use App\Entity\JobOrder;
use App\Entity\BatteryManufacturer; use App\Entity\BatteryManufacturer;
use App\Entity\Customer; use App\Entity\Customer;
@ -61,8 +62,9 @@ class JobOrderController extends BaseController
return $this->render('job-order/form.html.twig', $params); return $this->render('job-order/form.html.twig', $params);
} }
public function incomingSubmit(Request $req, ValidatorInterface $validator) public function incomingSubmit(Request $req, ValidatorInterface $validator, InvoiceCreator $ic)
{ {
error_log(print_r($req->request->all(), true));
$this->denyAccessUnlessGranted('jo_in.list', null, 'No access.'); $this->denyAccessUnlessGranted('jo_in.list', null, 'No access.');
// initialize error list // initialize error list
@ -109,6 +111,41 @@ class JobOrderController extends BaseController
->setAgentNotes($req->request->get('agent_notes')) ->setAgentNotes($req->request->get('agent_notes'))
->setDeliveryAddress($req->request->get('delivery_address')); ->setDeliveryAddress($req->request->get('delivery_address'));
// instantiate invoice criteria
$criteria = new InvoiceCriteria();
$ierror = $this->invoicePromo($em, $criteria, $req->request->get('invoice_promo'));
$invoice_items = $req->request->get('invoice_items');
if (!$ierror && !empty($invoice_items))
$ierror = $this->invoiceBatteries($em, $criteria, $invoice_items);
if ($ierror)
{
$error_array['invoice'] = $ierror;
}
else
{
// generate the invoice
$iobj = $ic->processCriteria($criteria);
$iobj->setStatus(InvoiceStatus::DRAFT)
->setCreatedBy($this->getUser());
// validate
$ierrors = $validator->validate($iobj);
// add errors to list
foreach ($ierrors as $error) {
$error_array[$error->getPropertyPath()] = $error->getMessage();
}
// add invoice to JO
$obj->setInvoice($iobj);
// save
$em->persist($iobj);
}
// validate // validate
$errors = $validator->validate($obj); $errors = $validator->validate($obj);
@ -729,7 +766,7 @@ class JobOrderController extends BaseController
{ {
$invoice['items'][] = [ $invoice['items'][] = [
'title' => $item->getTitle(), 'title' => $item->getTitle(),
'quantity' => $item->getQuantity(), // TODO: quantities are always 1, hardcoded into InvoiceCreator. no way of accepting quantities on InvoiceCriteria 'quantity' => number_format($item->getQuantity()), // TODO: quantities are always 1, hardcoded into InvoiceCreator. no way of accepting quantities on InvoiceCriteria
'unit_price' => number_format($item->getPrice(), 2), 'unit_price' => number_format($item->getPrice(), 2),
'amount' => number_format($item->getPrice() * $item->getQuantity(), 2) // TODO: should this calculation should be a saved value on InvoiceItem instead? 'amount' => number_format($item->getPrice() * $item->getQuantity(), 2) // TODO: should this calculation should be a saved value on InvoiceItem instead?
]; ];

View file

@ -40,23 +40,23 @@ class Invoice
// user that created the invoice // user that created the invoice
/** /**
* @ORM\ManyToOne(targetEntity="User", inversedBy="invoice") * @ORM\ManyToOne(targetEntity="User", inversedBy="invoices")
* @ORM\JoinColumn(name="user_id", referencedColumnName="id") * @ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/ */
protected $created_by; protected $created_by;
// the job order the invoice was created from // the job order the invoice was created from
/** /**
* @ORM\ManyToOne(targetEntity="JobOrder", inversedBy="invoice") * @ORM\OneToOne(targetEntity="JobOrder", inversedBy="invoice")
* @ORM\JoinColumn(name="job_order_id", referencedColumnName="id") * @ORM\JoinColumn(name="job_order_id", referencedColumnName="id")
*/ */
protected $job_order; protected $job_order;
// invoice items // invoice items
/** /**
* @ORM\OneToMany(targetEntity="InvoiceItem", mappedBy="invoice") * @ORM\OneToMany(targetEntity="InvoiceItem", mappedBy="invoice", cascade={"persist"})
*/ */
protected $invoice_items; protected $items;
// total discount (amount, not %) // total discount (amount, not %)
/** /**
@ -94,10 +94,17 @@ class Invoice
*/ */
protected $status; protected $status;
// promo used by this invoice
/**
* @ORM\ManyToOne(targetEntity="Promo")
* @ORM\JoinColumn(name="promo_id", referencedColumnName="id")
*/
protected $promo;
public function __construct() public function __construct()
{ {
$this->date_create = new DateTime(); $this->date_create = new DateTime();
$this->invoice_items = new ArrayCollection(); $this->items = new ArrayCollection();
} }
public function getID() public function getID()
@ -156,20 +163,20 @@ class Invoice
public function addItem(InvoiceItem $item) public function addItem(InvoiceItem $item)
{ {
$this->invoice_items->add($item); $this->items->add($item);
$item->setInvoice($this); $item->setInvoice($this);
return $this; return $this;
} }
public function clearItems() public function clearItems()
{ {
$this->invoice_items->clear(); $this->items->clear();
return $this; return $this;
} }
public function getItems() public function getItems()
{ {
return $this->invoice_items; return $this->items;
} }
public function setDiscount($discount) public function setDiscount($discount)
@ -237,4 +244,15 @@ class Invoice
{ {
return $this->status; return $this->status;
} }
public function setPromo(Promo $promo)
{
$this->promo = $promo;
return $this;
}
public function getPromo()
{
return $this->promo;
}
} }

View file

@ -43,6 +43,13 @@ class InvoiceItem
*/ */
protected $price; protected $price;
// battery the item is linked to
/**
* @ORM\ManyToOne(targetEntity="Battery")
* @ORM\JoinColumn(name="battery_id", referencedColumnName="id")
*/
protected $battery;
public function __construct() public function __construct()
{ {
$this->title = ''; $this->title = '';
@ -97,4 +104,15 @@ class InvoiceItem
{ {
return $this->price; return $this->price;
} }
public function setBattery(Battery $battery)
{
$this->battery = $battery;
return $this;
}
public function getBattery()
{
return $this->battery;
}
} }

View file

@ -162,6 +162,9 @@ class JobOrder
protected $delivery_address; protected $delivery_address;
// invoice // invoice
/**
* @ORM\OneToOne(targetEntity="Invoice", mappedBy="job_order")
*/
protected $invoice; protected $invoice;
public function __construct() public function __construct()
@ -397,4 +400,16 @@ class JobOrder
{ {
return $this->delivery_address; return $this->delivery_address;
} }
public function setInvoice(Invoice $invoice)
{
$this->invoice = $invoice;
$invoice->setJobOrder($this);
return $this;
}
public function getInvoice()
{
return $this->invoice;
}
} }

View file

@ -91,6 +91,12 @@ class User implements AdvancedUserInterface, Serializable
*/ */
protected $tickets; protected $tickets;
// invoices made by this user
/**
* @ORM\OneToMany(targetEntity="Invoice", mappedBy="created_by")
*/
protected $invoices;
public function __construct() public function __construct()
{ {
$this->roles = new ArrayCollection(); $this->roles = new ArrayCollection();
@ -290,4 +296,9 @@ class User implements AdvancedUserInterface, Serializable
{ {
return $this->tickets; return $this->tickets;
} }
public function getInvoices()
{
return $this->invoices;
}
} }

View file

@ -0,0 +1,18 @@
<?php
namespace App\Ramcar;
class InvoiceStatus extends NameValue
{
const DRAFT = 'draft';
const FINALIZED = 'finalized';
const PAID = 'paid';
const CANCELLED = 'cancelled';
const COLLECTION = [
'draft' => 'Draft',
'finalized' => 'Finalized',
'paid' => 'Paid',
'cancelled' => 'Cancelled',
];
}

View file

@ -85,7 +85,8 @@ class InvoiceCreator
$item->setInvoice($invoice) $item->setInvoice($invoice)
->setTitle($batt->getModel()->getName() . ' ' . $batt->getSize()->getName()) ->setTitle($batt->getModel()->getName() . ' ' . $batt->getSize()->getName())
->setQuantity($qty) ->setQuantity($qty)
->setPrice($sell_price); ->setPrice($sell_price)
->setBattery($batt);
$invoice->addItem($item); $invoice->addItem($item);
} }
@ -149,6 +150,9 @@ class InvoiceCreator
$total['discount'] = $discount; $total['discount'] = $discount;
$total['total_price'] -= $discount; $total['total_price'] -= $discount;
// process
$invoice->setPromo($promo);
} }
public function processCriteria(InvoiceCriteria $criteria) public function processCriteria(InvoiceCriteria $criteria)

View file

@ -265,35 +265,39 @@
<div class="form-group m-form__group row"> <div class="form-group m-form__group row">
<div class="col-lg-6"> <div class="col-lg-6">
<label>Discount Type</label> <label>Discount Type</label>
<select class="form-control m-input" id="invoice-promo" name="invoice-promo"> {% if mode == 'create' %}
<option value="">None</option> <select class="form-control m-input" id="invoice-promo" name="invoice_promo">
{% for promo in promos %} <option value="">None</option>
<option value="{{ promo.getID() }}">{{ promo.getName() ~ ' (' ~ promo.getDiscountRate * 100 ~ '% applied to ' ~ discount_apply[promo.getDiscountApply] ~ ')' }}</option> {% for promo in promos %}
{% endfor %} <option value="{{ promo.getID() }}">{{ promo.getName() ~ ' (' ~ promo.getDiscountRate * 100 ~ '% applied to ' ~ discount_apply[promo.getDiscountApply] ~ ')' }}</option>
</select> {% endfor %}
<div class="form-control-feedback hide" data-field="invoice_promo"></div> </select>
<div class="form-control-feedback hide" data-field="invoice_promo"></div>
{% else %}
<input type="text" id="invoice-promo" class="form-control m-input" value="{{ obj.getInvoice ? obj.getInvoice.getPromo.getName : 'None' }}" disabled>
{% endif %}
</div> </div>
<div class="col-lg-3"> <div class="col-lg-3">
<label>Promo Discount</label> <label>Promo Discount</label>
<input type="text" id="invoice-promo-discount" class="form-control m-input text-right" value="0.00" disabled> <input type="text" id="invoice-promo-discount" class="form-control m-input text-right" value="{{ obj.getInvoice ? obj.getInvoice.getDiscount|number_format(2) : '0.00' }}" disabled>
</div> </div>
<div class="col-lg-3"> <div class="col-lg-3">
<label>Trade In</label> <label>Trade In</label>
<input type="text" id="invoice-trade-in" class="form-control m-input text-right" value="0.00" disabled> <input type="text" id="invoice-trade-in" class="form-control m-input text-right" value="{{ obj.getInvoice ? obj.getInvoice.getTradeIn|number_format(2) : '0.00' }}" disabled>
</div> </div>
</div> </div>
<div class="form-group m-form__group row"> <div class="form-group m-form__group row">
<div class="col-lg-3"> <div class="col-lg-3">
<label>Price</label> <label>Price</label>
<input type="text" id="invoice-price" class="form-control m-input text-right" value="0.00" disabled> <input type="text" id="invoice-price" class="form-control m-input text-right" value="{{ obj.getInvoice ? obj.getInvoice.getVATExclusivePrice|number_format(2) : '0.00' }}" disabled>
</div> </div>
<div class="col-lg-3"> <div class="col-lg-3">
<label>VAT</label> <label>VAT</label>
<input type="text" id="invoice-vat" class="form-control m-input text-right" value="0.00" disabled> <input type="text" id="invoice-vat" class="form-control m-input text-right" value="{{ obj.getInvoice ? obj.getInvoice.getVAT|number_format : '0.00' }}" disabled>
</div> </div>
<div class="col-lg-6"> <div class="col-lg-6">
<label>Total Amount</label> <label>Total Amount</label>
<input type="text" id="invoice-total-amount" class="form-control m-input text-right" value="0.00" disabled> <input type="text" id="invoice-total-amount" class="form-control m-input text-right" value="{{ obj.getInvoice ? obj.getInvoice.getTotalPrice|number_format(2) : '0.00' }}" disabled>
</div> </div>
</div> </div>
<div class="form-group m-form__group row"> <div class="form-group m-form__group row">
@ -309,52 +313,65 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr class="placeholder-row"> {% if not obj.getInvoice or (obj.getInvoice and obj.getInvoice.getItems|length == 0) %}
<td colspan="4"> <tr class="placeholder-row">
No items to display. <td colspan="4">
</td> No items to display.
</tr> </td>
</tr>
{% else %}
{% for item in obj.getInvoice.getItems %}
<tr data-key="{{ loop.index0 }}">
<td>{{ item.getTitle }}</td>
<td class="text-right">{{ item.getPrice|number_format(2) }}</td>
<td class="text-right">{{ item.getQuantity|number_format }}</td>
<td class="text-right">{{ (item.getPrice * item.getQuantity)|number_format(2) }}</td>
</tr>
{% endfor %}
{% endif %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
<div class="form-group m-form__group row"> {% if mode == 'create' %}
<div class="col-lg-2"> <div class="form-group m-form__group row">
<label for="invoice-bmfg">Manufacturer</label> <div class="col-lg-2">
<select class="form-control m-input" id="invoice-bmfg"> <label for="invoice-bmfg">Manufacturer</label>
{% for manufacturer in bmfgs %} <select class="form-control m-input" id="invoice-bmfg">
<option value="{{ manufacturer.getID() }}">{{ manufacturer.getName() }}</option> {% for manufacturer in bmfgs %}
{% endfor %} <option value="{{ manufacturer.getID() }}">{{ manufacturer.getName() }}</option>
</select> {% endfor %}
</select>
</div>
<div class="col-lg-2">
<label for="invoice-battery">Battery</label>
<select class="form-control m-input" id="invoice-battery" data-value="" disabled>
<option value="">Select a manufacturer first</option>
</select>
</div>
<div class="col-lg-2">
<label for="invoice-trade-in-type">Trade In</label>
<select class="form-control m-input" id="invoice-trade-in-type">
<option value="">None</option>
{% for key, type in trade_in_types %}
<option value="{{ key }}">{{ type }}</option>
{% endfor %}
</select>
</div>
<div class="col-lg-2">
<label for="invoice-quantity">Quantity</label>
<input type="text" id="invoice-quantity" class="form-control m-input text-right" value="1">
</div>
<div class="col-lg-2">
<div><label>&nbsp;</label></div>
<button type="button" class="btn btn-primary" id="btn-add-to-invoice">Add to Invoice</button>
</div>
<div class="col-lg-2 text-right">
<div><label>&nbsp;</label></div>
<button type="button" class="btn btn-danger" id="btn-reset-invoice">Reset Invoice</button>
</div>
</div> </div>
<div class="col-lg-2"> {% endif %}
<label for="invoice-battery">Battery</label>
<select class="form-control m-input" id="invoice-battery" data-value="" disabled>
<option value="">Select a manufacturer first</option>
</select>
</div>
<div class="col-lg-2">
<label for="invoice-trade-in-type">Trade In</label>
<select class="form-control m-input" id="invoice-trade-in-type">
<option value="">None</option>
{% for key, type in trade_in_types %}
<option value="{{ key }}">{{ type }}</option>
{% endfor %}
</select>
</div>
<div class="col-lg-2">
<label for="invoice-quantity">Quantity</label>
<input type="text" id="invoice-quantity" class="form-control m-input text-right" value="1">
</div>
<div class="col-lg-2">
<div><label>&nbsp;</label></div>
<button type="button" class="btn btn-primary" id="btn-add-to-invoice">Add to Invoice</button>
</div>
<div class="col-lg-2 text-right">
<div><label>&nbsp;</label></div>
<button type="button" class="btn btn-danger" id="btn-reset-invoice">Reset Invoice</button>
</div>
</div>
</div> </div>
{% if mode == 'update-processing' %} {% if mode == 'update-processing' %}
@ -916,9 +933,9 @@ $(function() {
// add to invoice array // add to invoice array
invoiceItems.push({ invoiceItems.push({
'battery': battery, battery: battery,
'quantity': qty, quantity: qty,
'trade_in': tradeIn, trade_in: tradeIn,
}); });
// regenerate the invoice // regenerate the invoice