2015-09-02 05:48:32 +00:00
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
2021-09-02 11:14:59 +00:00
2017-09-29 09:41:50 +00:00
import copy
import json
2021-09-02 11:14:59 +00:00
2015-09-02 05:48:32 +00:00
import frappe
from frappe import _
from frappe . utils import cstr , flt
2018-02-13 09:12:40 +00:00
2021-09-02 11:14:59 +00:00
2015-09-02 05:48:32 +00:00
class ItemVariantExistsError ( frappe . ValidationError ) : pass
class InvalidItemAttributeValueError ( frappe . ValidationError ) : pass
class ItemTemplateCannotHaveStock ( frappe . ValidationError ) : pass
@frappe.whitelist ( )
2017-03-21 16:48:34 +00:00
def get_variant ( template , args = None , variant = None , manufacturer = None ,
manufacturer_part_no = None ) :
""" Validates Attributes and their Values, then looks for an exactly
matching Item Variant
2015-09-02 05:48:32 +00:00
: param item : Template Item
: param args : A dictionary with " Attribute " as key and " Attribute Value " as value
"""
2017-03-21 16:48:34 +00:00
item_template = frappe . get_doc ( ' Item ' , template )
2015-09-02 05:48:32 +00:00
2017-03-21 16:48:34 +00:00
if item_template . variant_based_on == ' Manufacturer ' and manufacturer :
return make_variant_based_on_manufacturer ( item_template , manufacturer ,
manufacturer_part_no )
else :
2018-02-13 09:12:40 +00:00
if isinstance ( args , str ) :
2017-03-21 16:48:34 +00:00
args = json . loads ( args )
if not args :
frappe . throw ( _ ( " Please specify at least one attribute in the Attributes table " ) )
return find_variant ( template , args , variant )
def make_variant_based_on_manufacturer ( template , manufacturer , manufacturer_part_no ) :
''' Make and return a new variant based on manufacturer and
manufacturer part no '''
from frappe . model . naming import append_number_if_name_exists
variant = frappe . new_doc ( ' Item ' )
copy_attributes_to_variant ( template , variant )
2015-09-02 05:48:32 +00:00
2017-04-25 11:57:53 +00:00
variant . manufacturer = manufacturer
variant . manufacturer_part_no = manufacturer_part_no
2017-03-21 16:48:34 +00:00
variant . item_code = append_number_if_name_exists ( ' Item ' , template . name )
return variant
2015-09-02 05:48:32 +00:00
2016-07-15 07:10:47 +00:00
def validate_item_variant_attributes ( item , args = None ) :
2018-02-15 06:09:45 +00:00
if isinstance ( item , str ) :
2016-07-15 09:41:46 +00:00
item = frappe . get_doc ( ' Item ' , item )
2016-07-15 07:10:47 +00:00
if not args :
2016-07-15 07:12:41 +00:00
args = { d . attribute . lower ( ) : d . attribute_value for d in item . attributes }
2016-06-02 12:19:16 +00:00
2017-12-13 13:10:52 +00:00
attribute_values , numeric_values = get_attribute_values ( item )
2016-06-02 12:19:16 +00:00
2015-09-02 05:48:32 +00:00
for attribute , value in args . items ( ) :
2016-07-15 09:41:46 +00:00
if not value :
continue
if attribute . lower ( ) in numeric_values :
numeric_attribute = numeric_values [ attribute . lower ( ) ]
2016-12-15 12:54:32 +00:00
validate_is_incremental ( numeric_attribute , attribute , value , item . name )
2015-09-02 05:48:32 +00:00
2016-12-15 12:54:32 +00:00
else :
attributes_list = attribute_values . get ( attribute . lower ( ) , [ ] )
2020-06-01 06:26:33 +00:00
validate_item_attribute_value ( attributes_list , attribute , value , item . name , from_variant = True )
2015-09-02 05:48:32 +00:00
2016-12-15 12:54:32 +00:00
def validate_is_incremental ( numeric_attribute , attribute , value , item ) :
from_range = numeric_attribute . from_range
to_range = numeric_attribute . to_range
increment = numeric_attribute . increment
2015-09-02 05:48:32 +00:00
2016-12-15 12:54:32 +00:00
if increment == 0 :
# defensive validation to prevent ZeroDivisionError
frappe . throw ( _ ( " Increment for Attribute {0} cannot be 0 " ) . format ( attribute ) )
2015-09-02 05:48:32 +00:00
2016-12-15 12:54:32 +00:00
is_in_range = from_range < = flt ( value ) < = to_range
precision = max ( len ( cstr ( v ) . split ( " . " ) [ - 1 ] . rstrip ( " 0 " ) ) for v in ( value , increment ) )
#avoid precision error by rounding the remainder
remainder = flt ( ( flt ( value ) - from_range ) % increment , precision )
2015-09-02 05:48:32 +00:00
2016-12-15 12:54:32 +00:00
is_incremental = remainder == 0 or remainder == increment
2016-06-02 12:19:16 +00:00
2016-12-15 12:54:32 +00:00
if not ( is_in_range and is_incremental ) :
frappe . throw ( _ ( " Value for Attribute {0} must be within the range of {1} to {2} in the increments of {3} for Item {4} " ) \
. format ( attribute , from_range , to_range , increment , item ) ,
InvalidItemAttributeValueError , title = _ ( ' Invalid Attribute ' ) )
2020-06-01 06:26:33 +00:00
def validate_item_attribute_value ( attributes_list , attribute , attribute_value , item , from_variant = True ) :
2018-03-07 10:01:08 +00:00
allow_rename_attribute_value = frappe . db . get_single_value ( ' Item Variant Settings ' , ' allow_rename_attribute_value ' )
if allow_rename_attribute_value :
pass
elif attribute_value not in attributes_list :
2020-06-01 06:26:33 +00:00
if from_variant :
frappe . throw ( _ ( " {0} is not a valid Value for Attribute {1} of Item {2} . " ) . format (
frappe . bold ( attribute_value ) , frappe . bold ( attribute ) , frappe . bold ( item ) ) , InvalidItemAttributeValueError , title = _ ( " Invalid Value " ) )
else :
2020-06-17 13:35:40 +00:00
msg = _ ( " The value {0} is already assigned to an existing Item {1} . " ) . format (
2020-06-01 06:26:33 +00:00
frappe . bold ( attribute_value ) , frappe . bold ( item ) )
msg + = " <br> " + _ ( " To still proceed with editing this Attribute Value, enable {0} in Item Variant Settings. " ) . format ( frappe . bold ( " Allow Rename Attribute Value " ) )
frappe . throw ( msg , InvalidItemAttributeValueError , title = _ ( ' Edit Not Allowed ' ) )
2015-09-02 05:48:32 +00:00
2017-12-13 13:10:52 +00:00
def get_attribute_values ( item ) :
2016-07-15 07:10:47 +00:00
if not frappe . flags . attribute_values :
attribute_values = { }
2016-07-15 09:41:46 +00:00
numeric_values = { }
2016-07-15 07:10:47 +00:00
for t in frappe . get_all ( " Item Attribute Value " , fields = [ " parent " , " attribute_value " ] ) :
2016-07-15 09:41:46 +00:00
attribute_values . setdefault ( t . parent . lower ( ) , [ ] ) . append ( t . attribute_value )
2017-12-13 13:10:52 +00:00
for t in frappe . get_all ( ' Item Variant Attribute ' ,
fields = [ " attribute " , " from_range " , " to_range " , " increment " ] ,
filters = { ' numeric_values ' : 1 , ' parent ' : item . variant_of } ) :
numeric_values [ t . attribute . lower ( ) ] = t
2016-07-15 07:10:47 +00:00
frappe . flags . attribute_values = attribute_values
2016-07-15 09:41:46 +00:00
frappe . flags . numeric_values = numeric_values
2016-07-15 07:10:47 +00:00
2016-07-15 09:41:46 +00:00
return frappe . flags . attribute_values , frappe . flags . numeric_values
2016-07-15 07:10:47 +00:00
2015-12-31 07:50:32 +00:00
def find_variant ( template , args , variant_item_code = None ) :
2018-09-21 04:50:52 +00:00
conditions = [ """ (iv_attribute.attribute= {0} and iv_attribute.attribute_value= {1} ) """ \
2015-09-02 05:48:32 +00:00
. format ( frappe . db . escape ( key ) , frappe . db . escape ( cstr ( value ) ) ) for key , value in args . items ( ) ]
conditions = " or " . join ( conditions )
2021-08-16 19:18:36 +00:00
from erpnext . e_commerce . variant_selector . utils import get_item_codes_by_attributes
2019-05-03 08:11:50 +00:00
possible_variants = [ i for i in get_item_codes_by_attributes ( args , template ) if i != variant_item_code ]
2015-09-02 05:48:32 +00:00
for variant in possible_variants :
variant = frappe . get_doc ( " Item " , variant )
if len ( args . keys ( ) ) == len ( variant . get ( " attributes " ) ) :
# has the same number of attributes and values
# assuming no duplication as per the validation in Item
match_count = 0
for attribute , value in args . items ( ) :
for row in variant . attributes :
if row . attribute == attribute and row . attribute_value == cstr ( value ) :
# this row matches
match_count + = 1
break
if match_count == len ( args . keys ( ) ) :
return variant . name
@frappe.whitelist ( )
def create_variant ( item , args ) :
2018-02-13 09:12:40 +00:00
if isinstance ( args , str ) :
2015-09-02 05:48:32 +00:00
args = json . loads ( args )
template = frappe . get_doc ( " Item " , item )
variant = frappe . new_doc ( " Item " )
2017-03-21 16:48:34 +00:00
variant . variant_based_on = ' Item Attribute '
2015-09-02 05:48:32 +00:00
variant_attributes = [ ]
for d in template . attributes :
variant_attributes . append ( {
" attribute " : d . attribute ,
" attribute_value " : args . get ( d . attribute )
} )
variant . set ( " attributes " , variant_attributes )
copy_attributes_to_variant ( template , variant )
2017-05-19 07:05:36 +00:00
make_variant_item_code ( template . item_code , template . item_name , variant )
2015-09-02 05:48:32 +00:00
return variant
2017-11-16 12:36:26 +00:00
@frappe.whitelist ( )
def enqueue_multiple_variant_creation ( item , args ) :
# There can be innumerable attribute combinations, enqueue
2018-08-16 05:02:49 +00:00
if isinstance ( args , str ) :
2018-07-03 05:18:59 +00:00
variants = json . loads ( args )
total_variants = 1
for key in variants :
total_variants * = len ( variants [ key ] )
if total_variants > = 600 :
2019-07-03 09:45:08 +00:00
frappe . throw ( _ ( " Please do not create more than 500 items at a time " ) )
2018-07-03 05:18:59 +00:00
return
2018-08-16 03:52:33 +00:00
if total_variants < 10 :
return create_multiple_variants ( item , args )
else :
frappe . enqueue ( " erpnext.controllers.item_variant.create_multiple_variants " ,
item = item , args = args , now = frappe . flags . in_test ) ;
return ' queued '
2017-11-16 12:36:26 +00:00
def create_multiple_variants ( item , args ) :
2018-08-16 03:52:33 +00:00
count = 0
2018-02-13 09:12:40 +00:00
if isinstance ( args , str ) :
2017-11-16 12:36:26 +00:00
args = json . loads ( args )
args_set = generate_keyed_value_combinations ( args )
for attribute_values in args_set :
if not get_variant ( item , args = attribute_values ) :
variant = create_variant ( item , attribute_values )
variant . save ( )
2018-08-16 03:52:33 +00:00
count + = 1
return count
2017-11-16 12:36:26 +00:00
def generate_keyed_value_combinations ( args ) :
"""
From this :
args = { " attr1 " : [ " a " , " b " , " c " ] , " attr2 " : [ " 1 " , " 2 " ] , " attr3 " : [ " A " ] }
To this :
[
{ u ' attr1 ' : u ' a ' , u ' attr2 ' : u ' 1 ' , u ' attr3 ' : u ' A ' } ,
{ u ' attr1 ' : u ' b ' , u ' attr2 ' : u ' 1 ' , u ' attr3 ' : u ' A ' } ,
{ u ' attr1 ' : u ' c ' , u ' attr2 ' : u ' 1 ' , u ' attr3 ' : u ' A ' } ,
{ u ' attr1 ' : u ' a ' , u ' attr2 ' : u ' 2 ' , u ' attr3 ' : u ' A ' } ,
{ u ' attr1 ' : u ' b ' , u ' attr2 ' : u ' 2 ' , u ' attr3 ' : u ' A ' } ,
{ u ' attr1 ' : u ' c ' , u ' attr2 ' : u ' 2 ' , u ' attr3 ' : u ' A ' }
]
"""
# Return empty list if empty
if not args :
return [ ]
# Turn `args` into a list of lists of key-value tuples:
# [
# [(u'attr2', u'1'), (u'attr2', u'2')],
# [(u'attr3', u'A')],
# [(u'attr1', u'a'), (u'attr1', u'b'), (u'attr1', u'c')]
# ]
key_value_lists = [ [ ( key , val ) for val in args [ key ] ] for key in args . keys ( ) ]
# Store the first, but as objects
# [{u'attr2': u'1'}, {u'attr2': u'2'}]
results = key_value_lists . pop ( 0 )
results = [ { d [ 0 ] : d [ 1 ] } for d in results ]
# Iterate the remaining
# Take the next list to fuse with existing results
for l in key_value_lists :
new_results = [ ]
for res in results :
for key_val in l :
# create a new clone of object in result
obj = copy . deepcopy ( res )
# to be used with every incoming new value
obj [ key_val [ 0 ] ] = key_val [ 1 ]
# and pushed into new_results
new_results . append ( obj )
results = new_results
return results
2015-09-02 05:48:32 +00:00
def copy_attributes_to_variant ( item , variant ) :
2017-03-21 16:48:34 +00:00
# copy non no-copy fields
2021-02-19 07:20:01 +00:00
exclude_fields = [ " naming_series " , " item_code " , " item_name " , " published_in_website " ,
" opening_stock " , " variant_of " , " valuation_rate " ]
2017-03-21 16:48:34 +00:00
if item . variant_based_on == ' Manufacturer ' :
# don't copy manufacturer values if based on part no
exclude_fields + = [ ' manufacturer ' , ' manufacturer_part_no ' ]
2017-08-29 12:45:57 +00:00
allow_fields = [ d . field_name for d in frappe . get_all ( " Variant Field " , fields = [ ' field_name ' ] ) ]
2017-09-29 09:41:50 +00:00
if " variant_based_on " not in allow_fields :
allow_fields . append ( " variant_based_on " )
2015-09-02 05:48:32 +00:00
for field in item . meta . fields :
2017-07-04 10:13:02 +00:00
# "Table" is part of `no_value_field` but we shouldn't ignore tables
2017-08-29 12:45:57 +00:00
if ( field . reqd or field . fieldname in allow_fields ) and field . fieldname not in exclude_fields :
2015-09-02 05:48:32 +00:00
if variant . get ( field . fieldname ) != item . get ( field . fieldname ) :
2017-09-29 09:41:50 +00:00
if field . fieldtype == " Table " :
variant . set ( field . fieldname , [ ] )
for d in item . get ( field . fieldname ) :
row = copy . deepcopy ( d )
if row . get ( " name " ) :
row . name = None
variant . append ( field . fieldname , row )
else :
variant . set ( field . fieldname , item . get ( field . fieldname ) )
2017-09-28 13:25:40 +00:00
2018-06-13 07:36:25 +00:00
variant . variant_of = item . name
2019-05-30 08:34:08 +00:00
if ' description ' not in allow_fields :
2018-04-24 06:40:45 +00:00
if not variant . description :
2019-07-03 05:04:31 +00:00
variant . description = " "
2019-09-24 13:47:13 +00:00
else :
2018-04-24 06:40:45 +00:00
if item . variant_based_on == ' Item Attribute ' :
if variant . attributes :
2019-05-30 08:34:08 +00:00
attributes_description = item . description + " "
2018-04-24 06:40:45 +00:00
for d in variant . attributes :
attributes_description + = " <div> " + d . attribute + " : " + cstr ( d . attribute_value ) + " </div> "
if attributes_description not in variant . description :
2019-09-24 13:47:13 +00:00
variant . description = attributes_description
2015-09-02 05:48:32 +00:00
2017-05-19 07:05:36 +00:00
def make_variant_item_code ( template_item_code , template_item_name , variant ) :
2015-09-02 05:48:32 +00:00
""" Uses template ' s item code and abbreviations to make variant ' s item code """
if variant . item_code :
return
abbreviations = [ ]
for attr in variant . attributes :
item_attribute = frappe . db . sql ( """ select i.numeric_values, v.abbr
from ` tabItem Attribute ` i left join ` tabItem Attribute Value ` v
on ( i . name = v . parent )
2017-03-09 11:32:55 +00:00
where i . name = % ( attribute ) s and ( v . attribute_value = % ( attribute_value ) s or i . numeric_values = 1 ) """ , {
2015-09-02 05:48:32 +00:00
" attribute " : attr . attribute ,
" attribute_value " : attr . attribute_value
} , as_dict = True )
if not item_attribute :
2019-05-03 08:27:20 +00:00
continue
2016-07-15 09:41:46 +00:00
# frappe.throw(_('Invalid attribute {0} {1}').format(frappe.bold(attr.attribute),
# frappe.bold(attr.attribute_value)), title=_('Invalid Attribute'),
# exc=InvalidItemAttributeValueError)
2015-09-02 05:48:32 +00:00
2017-03-09 11:32:55 +00:00
abbr_or_value = cstr ( attr . attribute_value ) if item_attribute [ 0 ] . numeric_values else item_attribute [ 0 ] . abbr
abbreviations . append ( abbr_or_value )
2015-09-02 05:48:32 +00:00
if abbreviations :
2016-07-15 09:41:46 +00:00
variant . item_code = " {0} - {1} " . format ( template_item_code , " - " . join ( abbreviations ) )
2017-05-19 07:05:36 +00:00
variant . item_name = " {0} - {1} " . format ( template_item_name , " - " . join ( abbreviations ) )
2017-11-14 09:57:28 +00:00
@frappe.whitelist ( )
def create_variant_doc_for_quick_entry ( template , args ) :
variant_based_on = frappe . db . get_value ( " Item " , template , " variant_based_on " )
args = json . loads ( args )
if variant_based_on == " Manufacturer " :
variant = get_variant ( template , * * args )
else :
existing_variant = get_variant ( template , args )
if existing_variant :
return existing_variant
else :
variant = create_variant ( template , args = args )
variant . name = variant . item_code
validate_item_variant_attributes ( variant , args )
return variant . as_dict ( )