diff --git a/tests/userpoints.filled.database.all.php.gz b/tests/userpoints.filled.database.all.php.gz
index dc54f63372cc124c487c98ed821c190ab08b88a0..17ab4dc489f177d0ee02b5b1aaf4c43c071a412f 100644
GIT binary patch
delta 4200
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

delta 4214
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
M|9hIhBN9*u0OsF!H2?qr

diff --git a/tests/userpoints.filled.database.php b/tests/userpoints.filled.database.php
index 6a0c027..5dd4a96 100644
--- a/tests/userpoints.filled.database.php
+++ b/tests/userpoints.filled.database.php
@@ -433,6 +433,16 @@ db_insert('system')->fields(array(
   'status' => 1,
 ))
 ->values(array(
+  'name' => 'userpoints_field',
+  'filename' => drupal_get_path('module', 'userpoints') . '/userpoints_field/userpoints_field.module',
+  'type' => 'module',
+  'owner' => '',
+  'bootstrap' => 0,
+  'weight' => 0,
+  'schema_version' => '7000',
+  'status' => 1,
+))
+->values(array(
   'name' => 'entity',
   'filename' => drupal_get_path('module', 'entity') . '/entity.module',
   'type' => 'module',
diff --git a/userpoints.info b/userpoints.info
index 9a9331c..e9c7199 100644
--- a/userpoints.info
+++ b/userpoints.info
@@ -9,3 +9,4 @@ files[]=views/userpoints_views_handler_filter_category.inc
 files[]=views/userpoints_views_handler_argument_category.inc
 configure = admin/config/people/userpoints/settings
 dependencies[]=entity
+dependencies[]=userpoints_field
diff --git a/userpoints.install b/userpoints.install
index 842f664..1df0961 100644
--- a/userpoints.install
+++ b/userpoints.install
@@ -273,6 +273,28 @@ function userpoints_install() {
   catch (Exception $e) {
     watchdog_exception('userpoints', $e, 'Failed to create default bundle.', array(), WATCHDOG_CRITICAL);
   }
+
+  // Clear field cache in case userpoints_field is installed at the same time.
+  field_cache_clear();
+
+  $field_info = array(
+    'field_name' => 'field_userpoints',
+    'type' => 'userpoints',
+    'cardinality' => FIELD_CARDINALITY_UNLIMITED,
+  );  
+
+  $field = field_create_field($field_info);
+
+  $instance_info = array(
+    'field_name' => 'field_userpoints',
+    'entity_type' => 'user',
+    'bundle' => 'user',
+    'label' => st('Points'),
+  );
+
+  // Create user points field instance.
+  $instance = field_create_instance($instance_info);
+  drupal_set_message(t('Created field %field_name.', array('%field_name' => $field_info['field_name'])));
 }
 
 /**
@@ -289,6 +311,8 @@ function userpoints_uninstall() {
   }
 
   variable_del('userpoints_default_bundle', 'userpoints');
+
+  field_delete_field('field_userpoints');
 }
 
 /**
@@ -473,3 +497,29 @@ function userpoints_update_7200() {
   );
   db_add_field('userpoints_txn', 'type', $type_field);
 }
+
+/**
+ * Add default userpoints field to user entity.
+ */
+function userpoints_update_7201() {
+
+  $field_info = array(
+    'field_name' => 'field_userpoints',
+    'type' => 'userpoints',
+    'cardinality' => FIELD_CARDINALITY_UNLIMITED,
+  );  
+
+  // Create default points field.
+  $field = field_create_field($field_info);
+
+  $instance_info = array(
+    'field_name' => 'field_userpoints',
+    'entity_type' => 'user',
+    'bundle' => 'user',
+    'label' => st('Points'),
+  );
+
+  // Create user points field instance.
+  $instance = field_create_instance($instance_info);
+}
+
diff --git a/userpoints.module b/userpoints.module
index ff4e274..8284f94 100644
--- a/userpoints.module
+++ b/userpoints.module
@@ -608,6 +608,37 @@ function userpoints_token_info() {
 }
 
 /**
+ * Get sum of points from transactions. This is an aggregate function.
+ *
+ * @param $uid
+ *   An optional user ID to filter by.
+ * @param $type
+ *   An optional transaction type to filter by.
+ *
+ * @return
+ *   Number of points given parameters.
+ *
+ * @ingroup userpoints_api
+ */
+function userpoints_transaction_get_points($uid = NULL, $type = NULL) {
+  $query = db_select('userpoints_txn');
+  $query->addExpression('SUM(points)', 'points_sum');
+
+  if (isset($uid)) {
+    $query->condition('uid', $uid);
+  }
+
+  if (isset($type)) {
+    $query->condition('type', $type);
+  }
+
+  $points = $query->execute()->fetchField();
+
+  // Possible non-numeric value returned from fetchField.
+  return ($points) ? $points : 0;
+}
+
+/**
  * Get current points of a user.
  *
  * @param $uid
diff --git a/userpoints.transaction.inc b/userpoints.transaction.inc
index d902792..2630b8f 100644
--- a/userpoints.transaction.inc
+++ b/userpoints.transaction.inc
@@ -1225,6 +1225,9 @@ class UserpointsTransactionController extends EntityAPIController {
 
     // Update totals if the transaction is approved and not expired.
     if ($entity->isApproved() && !$entity->isExpired()) {
+      // @todo account is StdClass and not entity class.
+      $account = user_load($entity->uid);
+      $this->updateFieldTotals($account, $entity);
       $this->updateTotals($entity->tid, $entity->uid, $entity->points);
     }
 
@@ -1238,6 +1241,41 @@ class UserpointsTransactionController extends EntityAPIController {
     return $return;
   }
 
+  /**
+   * Update point field(s) values.
+   *
+   * @todo Currently hardcoded for user accounts only.
+   */
+  protected function updateFieldTotals($entity, $transaction) {
+    $entity_type = 'user';
+    $delta = 0;
+
+    if (isset($entity->field_userpoints[LANGUAGE_NONE])) {
+      foreach ($entity->field_userpoints[LANGUAGE_NONE] as $num => $values) {
+        if ($transaction->type == $values['bundle']) {
+          // Find the corresponding index for a bundle.
+          $delta = $num;
+          break;
+        }
+        else {
+          $delta++;
+        }
+      }
+    }
+
+    // Set the field values for points and bundle for a given index. Max points
+    // are calculated in field CRUD.
+    $item = &$entity->field_userpoints[LANGUAGE_NONE][$delta];
+    $item['bundle'] = $transaction->type;
+    $item['points'] = userpoints_transaction_get_points($entity->uid, $transaction->type);
+
+    if (!isset($item['max_points']) || $item['points'] > $item['max_points']) {
+      // Set maximum points only if current points is greater.
+      $item['max_points'] = $item['points'];
+    }
+
+    entity_save($entity_type, $entity);
+  }
 
   /**
    * Update the total aggregations of the corresponding user.
@@ -1380,6 +1418,7 @@ class UserpointsTransactionMetadataController extends EntityDefaultMetadataContr
     $properties['uid'] = array(
       'label' => t('User ID'),
       'description' => t('ID of the user who received the point.'),
+      'type' => 'user',
     ) + $properties['uid'];
 
     $properties['approver_uid'] = array(
diff --git a/userpoints_field/userpoints_field.info b/userpoints_field/userpoints_field.info
new file mode 100644
index 0000000..99f6b69
--- /dev/null
+++ b/userpoints_field/userpoints_field.info
@@ -0,0 +1,5 @@
+name = Userpoints Field
+description = Provides a points field and widget to store aggregate points values by bundle.
+package = Userpoints
+core = 7.x
+dependencies[]=field
diff --git a/userpoints_field/userpoints_field.install b/userpoints_field/userpoints_field.install
new file mode 100644
index 0000000..39eebc2
--- /dev/null
+++ b/userpoints_field/userpoints_field.install
@@ -0,0 +1,31 @@
+<?php
+/**
+ * @file
+ * Userpoints field schema and installation.
+ */
+
+/**
+ * Implements hook_field_schema().
+ */
+function userpoints_field_field_schema($field) {
+  if ($field['type'] == 'userpoints') {
+    return array(
+      'columns' => array(
+        'points' => array(
+          'type' => 'numeric',
+          'not null' => TRUE,
+        ),  
+        'max_points' => array(
+          'type' => 'numeric',
+          'not null' => TRUE,
+        ),
+        'bundle' => array(
+          'type' => 'varchar',
+          'length' => 100,
+          'not null' => TRUE,
+        ),
+      ),  
+    );  
+  }
+}
+
diff --git a/userpoints_field/userpoints_field.module b/userpoints_field/userpoints_field.module
new file mode 100644
index 0000000..60855ad
--- /dev/null
+++ b/userpoints_field/userpoints_field.module
@@ -0,0 +1,165 @@
+<?php
+
+/**
+ * @file
+ *   Provides denormalized/aggregated points field implementation.
+ */
+
+
+/**
+ * Implements hook_field_info().
+ *
+ * @todo Remove no_ui flag when this becomes feasible for all entities.
+ */
+function userpoints_field_field_info() {
+  return array(
+    'userpoints' => array(
+      'label' => t('Points'),
+      'description' => t('The total number of points associated with an entity (User).'),
+      'default_widget' => 'userpoints_hidden',
+      'default_formatter' => 'userpoints_default',
+      'no_ui' => TRUE,
+      'property_type' => 'struct',
+      'property_callbacks' => array(
+        'entity_metadata_field_default_property_callback',
+        'userpoints_field_metadata_property_callback',
+      ),
+    ),
+  );
+}
+
+/**
+ * Implements hook_field_widget_info().
+ */
+function userpoints_field_field_widget_info() {
+  return array(
+    'userpoints_hidden' => array(
+      'label' => t('Points (hidden)'),
+      'description' => t('A dummy widget that displays the current point value associated with the entity.'),
+      'field types' => array('userpoints'),
+      'behaviors' => array(
+        'multiple values' => FIELD_BEHAVIOR_DEFAULT,
+        'default value' => FIELD_BEHAVIOR_DEFAULT,
+      ),
+    ),
+  );
+}
+
+/**
+ * Implements hook_field_formatter_info().
+ */
+function userpoints_field_field_formatter_info() {
+  return array(
+    'userpoints_default' => array(
+      'label' => t('Default'),
+      'field types' => array('userpoints'),
+      'settings' => array(
+        'display' => 'points',
+      ),
+    ),
+  );
+}
+
+function userpoints_field_field_is_empty($item, $field) {
+  if (empty($item['points'])) {
+    return TRUE;
+  }
+
+  return FALSE;
+}
+
+/**
+ * Implements hook_field_widget_form().
+ *
+ * Do not display any widget. All points will be saved via the userpoints transaction system.
+ */
+function userpoints_field_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, &$items, $delta, $element) {
+  return $element;
+}
+
+/**
+ * Implements hook_field_formatter_settings_summary().
+ */
+function userpoints_field_field_formatter_settings_summary($field, $instance, $view_mode) {
+  $display = $instance['display'][$view_mode];
+
+  $option = ($display['settings']['display'] == 'points') ? 'Points' : 'Max Points';
+  
+  return t('Display @option', array('@option' => $option));
+}
+
+/**
+ * Implements hook_field_formatter_settings_form().
+ */
+function userpoints_field_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
+  $display = $instance['display'][$view_mode];
+  $settings = $display['settings'];
+
+  $form['display'] = array(
+    '#type' => 'select',
+    '#title' => t('Display'),
+    '#description' => t('Select what to display.'),
+    '#options' => array('points' => t('Points'), 'max_points' => t('Max Points')),
+    '#default_value' => $settings['display'],
+  );
+
+  return $form;
+}
+
+/**
+ * Implements hook_field_formatter_view().
+ *
+ * @todo Replace the logic in this with a better theme_userpoints_points().
+ */
+function userpoints_field_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
+  $element = array();
+
+  $types = userpoints_transaction_get_types();
+  foreach ($items as $delta => $item) {
+    $element[$delta] = array(
+      '#theme' => 'userpoints_points',
+      '#points' => ($display['settings']['display'] == 'points') ? $item['points'] : $item['max_points'],
+      '#prefix' => '<span>' . check_plain($types[$item['bundle']]->label) . ': </span>',
+    );
+  }
+
+  return $element;
+}
+
+/**
+ * Userpoints field metadata property callback interprets userpoints field
+ * as a struct type.
+ *
+ * A userpoints field is a read-only data structure. All points should be
+ * modified with userpoints transactions.
+ */
+function userpoints_field_metadata_property_callback(&$info, $entity_type, $field, $instance, $field_type) {
+
+  $name = $field['field_name'];
+  $property = &$info[$entity_type]['bundles'][$instance['bundle']]['properties'][$name];
+
+  $property['property info'] = array(
+    'points' => array(
+      'type' => 'integer',
+      'label' => t('Points'),
+      'sanitized' => FALSE,
+      'getter callback' => 'entity_property_verbatim_get',
+      'raw getter callback' => 'entity_property_verbatim_get',
+    ),  
+    'max_points' => array(
+      'type' => 'integer',
+      'label' => t('Max points'),
+      'sanitized' => FALSE,
+      'getter callback' => 'entity_property_verbatim_get',
+      'raw getter callback' => 'entity_property_verbatim_get',
+    ),  
+    'bundle' => array(
+      'type' => 'userpoints_transaction_type',
+      'label' => t('Userpoints transaction type'),
+      'sanitized' => FALSE,
+      'getter callback' => 'entity_property_verbatim_get',
+      'raw getter callback' => 'entity_property_verbatim_get',
+    ),
+  );
+}
+
