Source of file LineItem.php
Size: 17,744 Bytes - Last Modified: 2021-12-23T10:24:32+00:00
/var/www/docs.ssmods.com/process/src/src/model/LineItem.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676 | <?php namespace SilverCommerce\OrdersAdmin\Model; use SilverStripe\i18n\i18n; use SilverStripe\ORM\DataObject; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\DropdownField; use SilverStripe\Forms\ReadonlyField; use SilverStripe\SiteConfig\SiteConfig; use SilverCommerce\TaxAdmin\Model\TaxRate; use SilverCommerce\TaxAdmin\Traits\Taxable; use SilverStripe\Core\Manifest\ModuleLoader; use SilverStripe\Subsites\State\SubsiteState; use SilverStripe\Forms\GridField\GridFieldEditButton; use SilverStripe\Forms\GridField\GridFieldDataColumns; use SilverStripe\ORM\FieldType\DBHTMLText as HTMLText; use SilverCommerce\TaxAdmin\Interfaces\TaxableProvider; use SilverStripe\Forms\GridField\GridFieldAddNewButton; use SilverStripe\Forms\GridField\GridFieldDeleteAction; use Symbiote\GridFieldExtensions\GridFieldEditableColumns; use Symbiote\GridFieldExtensions\GridFieldAddNewInlineButton; use SilverStripe\Forms\GridField\GridFieldAddExistingAutocompleter; /** * A LineItem is a single line item on an order, extimate or even in * the shopping cart. * * An item has a number of fields that describes a product: * * - Key: ID used to detect this item * - Title: Title of the item * - Content: Description of this object * - Quantity: Number or items in this order * - Weight: Weight of this item (unit of measurment is defined globally) * - TaxRate: Rate of tax for this item (e.g. 20.00 for 20%) * - ProductClass: ClassName of product that this item is matched against * - StockID: Unique identifier of this item (used with ProductClass * match to a product) * - Locked: Is this a locked item? Locked items cannot be changed in the * shopping cart * - Deliverable: Is this a product that can be delivered? This can effect * delivery options * * @method Estimate Parent * @method TaxRate Tax * @method TaxRate TaxRate * @method \SilverStripe\ORM\HasManyList Customisations * * @author Mo <morven@ilateral.co.uk> */ class LineItem extends DataObject implements TaxableProvider { use Taxable; private static $table_name = 'LineItem'; /** * The name of the param used on a related product to * track Stock Levels. * * Defaults to StockLevel * * @var string * @config */ private static $stock_param = "StockLevel"; /** * Standard database columns * * @var array * @config */ private static $db = [ "Key" => "Varchar(255)", "Title" => "Varchar", "BasePrice" => "Decimal(9,3)", "Price" => "Currency", "Quantity" => "Int", "StockID" => "Varchar(100)", "ProductClass" => "Varchar", "Locked" => "Boolean", "Stocked" => "Boolean", "Deliverable" => "Boolean" ]; /** * Foreign key associations in DB * * @var array * @config */ private static $has_one = [ "Parent" => Estimate::class, "Tax" => TaxRate::class, "TaxRate" => TaxRate::class ]; /** * One to many associations * * @var array * @config */ private static $has_many = [ "Customisations" => LineItemCustomisation::class ]; /** * Specify default values of a field * * @var array * @config */ private static $defaults = [ "Quantity" => 1, "ProductClass" => "Product", "Locked" => false, "Stocked" => false, "Deliverable" => true ]; /** * Function to DB Object conversions * * @var array * @config */ private static $casting = [ "UnitPrice" => "Currency(9,3)", "UnitTax" => "Currency(9,3)", "UnitTotal" => "Currency(9,3)", "SubTotal" => "Currency(9,3)", "TaxRate" => "Decimal", "TaxTotal" => "Currency(9,3)", "Total" => "Currency(9,3)", "CustomisationList" => "Text", "CustomisationAndPriceList" => "Text", ]; /** * Fields to display in list tables * * @var array * @config */ private static $summary_fields = [ "Quantity", "Title", "StockID", "BasePrice", "TaxRateID", "CustomisationAndPriceList" ]; private static $field_labels = [ "BasePrice"=> "Item Price", "Price" => "Item Price", "TaxID" => "Tax", "TaxRateID"=> "Tax", "CustomisationAndPriceList" => "Customisations" ]; /** * Get the basic price for this object * * @return float */ public function getBasePrice() { return $this->dbObject('BasePrice')->getValue(); } /** * Return the tax rate for this Object * * @return TaxRate */ public function getTaxRate() { return $this->TaxRate(); } /** * Get the locale from the site * * @return string */ public function getLocale() { return i18n::get_locale(); } /** * Get should this field automatically show the price including TAX? * * @return bool */ public function getShowPriceWithTax() { $config = SiteConfig::current_site_config(); $show = $config->ShowPriceAndTax; $result = $this->filterTaxableExtensionResults( $this->extend("updateShowPriceWithTax", $show) ); if (!empty($result)) { return (bool)$result; } return (bool)$show; } /** * We don't want to show a tax string on Line Items * * @return false */ public function getShowTaxString() { return false; } /** * Modify default field scaffolding in admin * * @return FieldList */ public function getCMSFields() { $this->beforeUpdateCMSFields(function ($fields) { $config = SiteConfig::current_site_config(); $fields->removeByName( [ 'Customisation', 'Price', 'TaxID' ] ); $fields->addFieldToTab( "Root.Main", ReadonlyField::create("Key"), "Title" ); $fields->addFieldToTab( "Root.Main", DropdownField::create( "TaxRateID", $this->fieldLabel("TaxRate"), $config->TaxRates()->map() ), "Quantity" ); // Change unlink button to remove on customisation $custom_field = $fields->dataFieldByName("Customisations"); if ($custom_field) { $config = $custom_field->getConfig(); $config ->removeComponentsByType(GridFieldDeleteAction::class) ->removeComponentsByType(GridFieldDataColumns::class) ->removeComponentsByType(GridFieldEditButton::class) ->removeComponentsByType(GridFieldAddNewButton::class) ->removeComponentsByType(GridFieldAddExistingAutocompleter::class) ->addComponents( new GridFieldEditableColumns(), new GridFieldAddNewInlineButton(), new GridFieldEditButton(), new GridFieldDeleteAction() ); $custom_field->setConfig($config); } }); return parent::getCMSFields(); } /** * Get the price for a single line item (unit), minus any tax * * @return float */ public function getNoTaxPrice() { $price = $this->getBasePrice(); foreach ($this->Customisations() as $customisation) { $price += $customisation->getBasePrice(); } $result = $this->getOwner()->filterTaxableExtensionResults( $this->extend("updateNoTaxPrice", $price) ); if (!empty($result)) { return $result; } return $price; } public function getUnitPrice() { return $this->getNoTaxPrice(); } /** * Get the amount of tax for a single unit of this item * * **NOTE** Tax is rounded at the single item price to avoid multiplication * weirdness. For example 49.995 + 20% is 59.994 for one product, * but 239.976 for 4 (it should be 239.96) * * @return float */ public function getUnitTax() { // Calculate and round tax now to try and minimise penny rounding issues $total = ($this->UnitPrice / 100) * $this->TaxPercentage; $this->extend("updateUnitTax", $total); return $total; } /** * Overwrite TaxAmount with unit tax * * @return float */ public function getTaxAmount() { return $this->UnitTax; } /** * Get the total price and tax for a single unit * * @return float */ public function getUnitTotal() { $total = $this->UnitPrice + $this->UnitTax; $this->extend("updateUnitTotal", $total); return $total; } /** * Get the value of this item, minus any tax * * @return float */ public function getSubTotal() { $total = $this->NoTaxPrice * $this->Quantity; $this->extend("updateSubTotal", $total); return $total; } /** * Get the total amount of tax for a single unit of this item * * @return float */ public function getTaxTotal() { $total = $this->UnitTax * $this->Quantity; $this->extend("updateTaxTotal", $total); return $total; } /** * Get the value of this item, minus any tax * * @return float */ public function getTotal() { $total = $this->SubTotal + $this->TaxTotal; $this->extend("updateTotal", $total); return $total; } /** * Get an image object associated with this line item. * By default this is retrieved from the base product. * * @return Image | null */ public function Image() { $product = $this->FindStockItem(); if ($product && method_exists($product, "SortedImages")) { return $product->SortedImages()->first(); } elseif ($product && method_exists($product, "Images")) { return $product->Images()->first(); } elseif ($product && method_exists($product, "Image") && $product->Image()->exists()) { return $product->Image(); } } /** * Provide a string of customisations seperated by a comma but not * including a price * * @return string */ public function getCustomisationList() { $return = []; $items = $this->Customisations(); if ($items && $items->exists()) { foreach ($items as $item) { $return[] = $item->Title . ': ' . $item->Value; } } $this->extend("updateCustomisationList", $return); return implode(", ", $return); } /** * Provide a string of customisations seperated by a comma and * including a price * * @return string */ public function getCustomisationAndPriceList() { $return = []; $items = $this->Customisations(); if ($items && $items->exists()) { foreach ($items as $item) { $return[] = $item->Title . ': ' . $item->Value . ' (' . $item->getFormattedPrice() . ')'; } } $this->extend("updateCustomisationAndPriceList", $return); return implode(", ", $return); } /** * Get list of customisations rendering into a basic * HTML string * * @return HTMLText */ public function CustomisationHTML() { $return = HTMLText::create(); $items = $this->Customisations(); $html = ""; if ($items && $items->exists()) { foreach ($items as $item) { $html .= $item->Title . ': ' . $item->Value . ";<br/>"; } } $return->setValue($html); $this->extend("updateCustomisationHTML", $return); return $return; } /** * Match this item to another object in the Database, by the * provided details. * * @param $relation_name = The class name of the related dataobject * @param $relation_col = The column name of the related object * @param $match_col = The column we use to match the two objects * @return DataObject */ public function Match($relation_name = null, $relation_col = "StockID", $match_col = "StockID") { // Try to determine relation name if (!$relation_name && !$this->ProductClass && class_exists(CatalogueProduct::class)) { $relation_name = CatalogueProduct::class; } elseif (!$relation_name && $this->ProductClass) { $relation_name = $this->ProductClass; } // Setup filter and check for existence of subsites // (as sometimes dubplicate stock ID's are found from another subsite) $filter = []; $filter[$relation_col] = $this->{$match_col}; $subsites_exists = ModuleLoader::inst()->getManifest()->moduleExists('silverstripe/subsites'); if ($subsites_exists) { $filter['SubsiteID'] = SubsiteState::singleton()->getSubsiteId(); } return $relation_name::get() ->filter($filter) ->first(); } /** * Find our original stock item (useful for adding links back to the * original product). * * This function is a synonym for @Link Match (as a result of) merging * LineItem * * @return DataObject */ public function FindStockItem() { return $this->Match(); } /** * Check stock levels for this item, will return the actual number * of remaining stock after removing the current quantity * * @param $qty The quantity we want to check against * @return Int */ public function checkStockLevel($qty) { $stock_param = $this->config()->get("stock_param"); $item = $this->Match(); $stock = ($item->$stock_param) ? $item->$stock_param : 0; $return = $stock - $qty; $this->extend("updateCheckStockLevel", $return, $qty); return $return; } /** * Generate a key based on this item and its customisations * * @return string */ public function generateKey() { // Generate a unique item key based on the current ID and customisations $key = base64_encode( json_encode( $this->Customisations()->map("Title", "Value")->toArray() ) ); return $this->StockID . ':' . $key; } /** * Only order creators or users with VIEW admin rights can view * * @return Boolean */ public function canView($member = null, $context = []) { $extended = $this->extend('canView', $member); if ($extended && $extended !== null) { return $extended; } return $this->Parent()->canView($member); } /** * Anyone can create an order item * * @return Boolean */ public function canCreate($member = null, $context = []) { $extended = $this->extend('canCreate', $member); if ($extended && $extended !== null) { return $extended; } return true; } /** * No one can edit items once they are created * * @return Boolean */ public function canEdit($member = null, $context = []) { $extended = $this->extend('canEdit', $member); if ($extended && $extended !== null) { return $extended; } return $this->Parent()->canEdit($member); } /** * No one can delete items once they are created * * @return Boolean */ public function canDelete($member = null, $context = []) { $extended = $this->extend('canDelete', $member); if ($extended && $extended !== null) { return $extended; } return $this->Parent()->canEdit($member); } /** * Overwrite default duplicate function * * @param boolean $doWrite (write the cloned object to DB) * @return DataObject $clone The duplicated object */ public function duplicate($doWrite = true, $manyMany = "many_many") { $clone = parent::duplicate($doWrite); // Ensure we clone any customisations if ($doWrite) { foreach ($this->Customisations() as $customisation) { $new_item = $customisation->duplicate(false); $new_item->ParentID = $clone->ID; $new_item->write(); } } return $clone; } /** * Pre-write tasks * * @return void */ public function onBeforeWrite() { parent::onBeforeWrite(); $this->Key = $this->generateKey(); } /** * Clean up DB on deletion * * @return void */ public function onBeforeDelete() { parent::onBeforeDelete(); foreach ($this->Customisations() as $customisation) { $customisation->delete(); } } } |