Merge branch 'hotfix' of github.com:frappe/erpnext into hotfix

This commit is contained in:
Nabin Hait
2017-05-18 14:06:06 +05:30
1362 changed files with 149751 additions and 99555 deletions

View File

@@ -63,6 +63,11 @@ class AccountsController(TransactionBase):
if self.doctype == 'Purchase Invoice':
self.validate_paid_amount()
def before_print(self):
if self.doctype in ['Purchase Order', 'Sales Order']:
if self.get("group_same_items"):
self.group_similar_items()
def validate_paid_amount(self):
if hasattr(self, "is_pos") or hasattr(self, "is_paid"):
is_paid = self.get("is_pos") or self.get("is_paid")
@@ -108,7 +113,7 @@ class AccountsController(TransactionBase):
date_field = "transaction_date"
if date_field and self.get(date_field):
validate_fiscal_year(self.get(date_field), self.fiscal_year,
validate_fiscal_year(self.get(date_field), self.fiscal_year, self.company,
self.meta.get_label(date_field), self)
def validate_due_date(self):
@@ -122,6 +127,11 @@ class AccountsController(TransactionBase):
validate_due_date(self.posting_date, self.due_date, "Supplier", self.supplier, self.company)
def set_price_list_currency(self, buying_or_selling):
if self.meta.get_field("posting_date"):
transaction_date = self.posting_date
else:
transaction_date = self.transaction_date
if self.meta.get_field("currency"):
# price list part
fieldname = "selling_price_list" if buying_or_selling.lower() == "selling" \
@@ -134,8 +144,8 @@ class AccountsController(TransactionBase):
self.plc_conversion_rate = 1.0
elif not self.plc_conversion_rate:
self.plc_conversion_rate = get_exchange_rate(
self.price_list_currency, self.company_currency)
self.plc_conversion_rate = get_exchange_rate(self.price_list_currency,
self.company_currency, transaction_date)
# currency
if not self.currency:
@@ -145,7 +155,7 @@ class AccountsController(TransactionBase):
self.conversion_rate = 1.0
elif not self.conversion_rate:
self.conversion_rate = get_exchange_rate(self.currency,
self.company_currency)
self.company_currency, transaction_date)
def set_missing_item_details(self, for_validate=False):
"""set missing item values"""
@@ -416,7 +426,7 @@ class AccountsController(TransactionBase):
max_allowed_amt = flt(ref_amt * (100 + tolerance) / 100)
if total_billed_amt - max_allowed_amt > 0.01:
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow overbilling, please set in Stock Settings").format(item.item_code, item.idx, max_allowed_amt))
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set in Buying Settings").format(item.item_code, item.idx, max_allowed_amt))
def get_company_default(self, fieldname):
from erpnext.accounts.utils import get_company_default
@@ -561,6 +571,42 @@ class AccountsController(TransactionBase):
frappe.throw(_("Row #{0}: Asset {1} cannot be submitted, it is already {2}")
.format(d.idx, d.asset, asset.status))
def delink_advance_entries(self, linked_doc_name):
total_allocated_amount = 0
for adv in self.advances:
consider_for_total_advance = True
if adv.reference_name == linked_doc_name:
frappe.db.sql("""delete from `tab{0} Advance`
where name = %s""".format(self.doctype), adv.name)
consider_for_total_advance = False
if consider_for_total_advance:
total_allocated_amount += flt(adv.allocated_amount, adv.precision("allocated_amount"))
frappe.db.set_value(self.doctype, self.name, "total_advance",
total_allocated_amount, update_modified=False)
def group_similar_items(self):
group_item_qty = {}
group_item_amount = {}
for item in self.items:
group_item_qty[item.item_code] = group_item_qty.get(item.item_code, 0) + item.qty
group_item_amount[item.item_code] = group_item_amount.get(item.item_code, 0) + item.amount
duplicate_list = []
for item in self.items:
if item.item_code in group_item_qty:
item.qty = group_item_qty[item.item_code]
item.amount = group_item_amount[item.item_code]
del group_item_qty[item.item_code]
else:
duplicate_list.append(item)
for item in duplicate_list:
self.remove(item)
@frappe.whitelist()
def get_tax_rate(account_head):
return frappe.db.get_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True)
@@ -665,7 +711,7 @@ def get_advance_journal_entries(party_type, party, party_account, amount_field,
.format(order_doctype, order_condition))
reference_condition = " and (" + " or ".join(conditions) + ")" if conditions else ""
journal_entries = frappe.db.sql("""
select
"Journal Entry" as reference_type, t1.name as reference_name,
@@ -725,8 +771,8 @@ def get_advance_payment_entries(party_type, party, party_account,
def update_invoice_status():
# Daily update the status of the invoices
frappe.db.sql(""" update `tabSales Invoice` set status = 'Overdue'
frappe.db.sql(""" update `tabSales Invoice` set status = 'Overdue'
where due_date < CURDATE() and docstatus = 1 and outstanding_amount > 0""")
frappe.db.sql(""" update `tabPurchase Invoice` set status = 'Overdue'
frappe.db.sql(""" update `tabPurchase Invoice` set status = 'Overdue'
where due_date < CURDATE() and docstatus = 1 and outstanding_amount > 0""")

View File

@@ -178,6 +178,9 @@ class BuyingController(StockController):
for item in self.get("items"):
item.rm_supp_cost = 0.0
if self.is_subcontracted == "No" and self.get("supplied_items"):
self.set('supplied_items', [])
def update_raw_materials_supplied(self, item, raw_material_table):
bom_items = self.get_items_from_bom(item.item_code, item.bom)
raw_materials_cost = 0
@@ -223,7 +226,8 @@ class BuyingController(StockController):
})
if not rm.rate:
from erpnext.stock.stock_ledger import get_valuation_rate
rm.rate = get_valuation_rate(bom_item.item_code, self.supplier_warehouse)
rm.rate = get_valuation_rate(bom_item.item_code, self.supplier_warehouse,
self.doctype, self.name)
else:
rm.rate = bom_item.rate
@@ -302,6 +306,7 @@ class BuyingController(StockController):
# validate accepted and rejected qty
def validate_accepted_rejected_qty(self):
for d in self.get("items"):
self.validate_negative_quantity(d, ["received_qty","qty", "rejected_qty"])
if not flt(d.received_qty) and flt(d.qty):
d.received_qty = flt(d.qty) - flt(d.rejected_qty)
@@ -315,6 +320,16 @@ class BuyingController(StockController):
if ((flt(d.qty) + flt(d.rejected_qty)) != flt(d.received_qty)):
frappe.throw(_("Accepted + Rejected Qty must be equal to Received quantity for Item {0}").format(d.item_code))
def validate_negative_quantity(self, item_row, field_list):
if self.is_return:
return
item_row = item_row.as_dict()
for fieldname in field_list:
if flt(item_row[fieldname]) < 0:
frappe.throw(_("Row #{0}: {1} can not be negative for item {2}".format(item_row['idx'],
frappe.get_meta(item_row.doctype).get_label(fieldname), item_row['item_code'])))
def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_voucher=False):
self.update_ordered_qty()

View File

@@ -12,19 +12,44 @@ class InvalidItemAttributeValueError(frappe.ValidationError): pass
class ItemTemplateCannotHaveStock(frappe.ValidationError): pass
@frappe.whitelist()
def get_variant(template, args, variant=None):
"""Validates Attributes and their Values, then looks for an exactly matching Item Variant
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
:param item: Template Item
:param args: A dictionary with "Attribute" as key and "Attribute Value" as value
"""
if isinstance(args, basestring):
args = json.loads(args)
item_template = frappe.get_doc('Item', template)
if not args:
frappe.throw(_("Please specify at least one attribute in the Attributes table"))
if item_template.variant_based_on=='Manufacturer' and manufacturer:
return make_variant_based_on_manufacturer(item_template, manufacturer,
manufacturer_part_no)
else:
if isinstance(args, basestring):
args = json.loads(args)
return find_variant(template, args, variant)
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)
variant.append("manufacturers", {
"manufacturer": manufacturer,
"manufacturer_part_no": manufacturer_part_no
})
variant.item_code = append_number_if_name_exists('Item', template.name)
return variant
def validate_item_variant_attributes(item, args=None):
if isinstance(item, basestring):
@@ -41,30 +66,37 @@ def validate_item_variant_attributes(item, args=None):
if attribute.lower() in numeric_values:
numeric_attribute = numeric_values[attribute.lower()]
validate_is_incremental(numeric_attribute, attribute, value, item.name)
from_range = numeric_attribute.from_range
to_range = numeric_attribute.to_range
increment = numeric_attribute.increment
else:
attributes_list = attribute_values.get(attribute.lower(), [])
validate_item_attribute_value(attributes_list, attribute, value, item.name)
if increment == 0:
# defensive validation to prevent ZeroDivisionError
frappe.throw(_("Increment for Attribute {0} cannot be 0").format(attribute))
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
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)
if increment == 0:
# defensive validation to prevent ZeroDivisionError
frappe.throw(_("Increment for Attribute {0} cannot be 0").format(attribute))
is_incremental = remainder==0 or remainder==increment
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)
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.name),
InvalidItemAttributeValueError, title=_('Invalid Attribute'))
is_incremental = remainder==0 or remainder==increment
elif value not in attribute_values.get(attribute.lower(), []):
frappe.throw(_("Value {0} for Attribute {1} does not exist in the list of valid Item Attribute Values for Item {2}").format(
value, attribute, item.name), InvalidItemAttributeValueError, title=_('Invalid Attribute'))
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'))
def validate_item_attribute_value(attributes_list, attribute, attribute_value, item):
if attribute_value not in attributes_list:
frappe.throw(_("Value {0} for Attribute {1} does not exist in the list of valid Item Attribute Values for Item {2}").format(
attribute_value, attribute, item), InvalidItemAttributeValueError, title=_('Invalid Attribute'))
def get_attribute_values():
if not frappe.flags.attribute_values:
@@ -124,6 +156,7 @@ def create_variant(item, args):
template = frappe.get_doc("Item", item)
variant = frappe.new_doc("Item")
variant.variant_based_on = 'Item Attribute'
variant_attributes = []
for d in template.attributes:
@@ -140,17 +173,28 @@ def create_variant(item, args):
def copy_attributes_to_variant(item, variant):
from frappe.model import no_value_fields
# copy non no-copy fields
exclude_fields = ["item_code", "item_name", "show_in_website"]
if item.variant_based_on=='Manufacturer':
# don't copy manufacturer values if based on part no
exclude_fields += ['manufacturer', 'manufacturer_part_no']
for field in item.meta.fields:
if field.fieldtype not in no_value_fields and (not field.no_copy)\
and field.fieldname not in ("item_code", "item_name", "show_in_website"):
and field.fieldname not in exclude_fields:
if variant.get(field.fieldname) != item.get(field.fieldname):
variant.set(field.fieldname, item.get(field.fieldname))
variant.variant_of = item.name
variant.has_variants = 0
if variant.attributes:
variant.description += "\n"
for d in variant.attributes:
variant.description += "<p>" + d.attribute + ": " + cstr(d.attribute_value) + "</p>"
if item.variant_based_on=='Item Attribute':
if variant.attributes:
variant.description += "\n"
for d in variant.attributes:
variant.description += "<p>" + d.attribute + ": " + cstr(d.attribute_value) + "</p>"
def make_variant_item_code(template_item_code, variant):
"""Uses template's item code and abbreviations to make variant's item code"""
@@ -162,7 +206,7 @@ def make_variant_item_code(template_item_code, variant):
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)
where i.name=%(attribute)s and v.attribute_value=%(attribute_value)s""", {
where i.name=%(attribute)s and (v.attribute_value=%(attribute_value)s or i.numeric_values = 1)""", {
"attribute": attr.attribute,
"attribute_value": attr.attribute_value
}, as_dict=True)
@@ -173,11 +217,8 @@ def make_variant_item_code(template_item_code, variant):
# frappe.bold(attr.attribute_value)), title=_('Invalid Attribute'),
# exc=InvalidItemAttributeValueError)
if item_attribute[0].numeric_values:
# don't generate item code if one of the attributes is numeric
return
abbreviations.append(item_attribute[0].abbr)
abbr_or_value = cstr(attr.attribute_value) if item_attribute[0].numeric_values else item_attribute[0].abbr
abbreviations.append(abbr_or_value)
if abbreviations:
variant.item_code = "{0}-{1}".format(template_item_code, "-".join(abbreviations))

View File

@@ -1,61 +0,0 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
cur_frm.cscript.onload = function(doc, cdt, cdn) {
cur_frm.add_fetch('customer', 'customer_name', 'customer_name');
cur_frm.add_fetch('supplier', 'supplier_name', 'supplier_name');
cur_frm.fields_dict.customer.get_query = erpnext.queries.customer;
cur_frm.fields_dict.supplier.get_query = erpnext.queries.supplier;
if(cur_frm.fields_dict.lead) {
cur_frm.fields_dict.lead.get_query = erpnext.queries.lead;
cur_frm.add_fetch('lead', 'lead_name', 'lead_name');
}
if(doc.__islocal) {
var last_route = frappe.route_history.slice(-2, -1)[0];
if(last_route && last_route[0]==="Form") {
var doctype = last_route[1],
docname = last_route.slice(2).join("/");
if(["Customer", "Quotation", "Sales Order", "Sales Invoice", "Delivery Note",
"Installation Note", "Opportunity", "Warranty Claim", "Maintenance Visit",
"Maintenance Schedule"]
.indexOf(doctype)!==-1) {
var refdoc = frappe.get_doc(doctype, docname);
if((refdoc.doctype == "Quotation" && refdoc.quotation_to=="Customer") ||
(refdoc.doctype == "Opportunity" && refdoc.enquiry_from=="Customer") ||
!in_list(["Opportunity", "Quotation"], doctype)) {
cur_frm.set_value("customer", refdoc.customer || refdoc.name);
cur_frm.set_value("customer_name", refdoc.customer_name);
if(cur_frm.doc.doctype==="Address")
cur_frm.set_value("address_title", cur_frm.doc.customer_name);
}
}
else if(["Supplier", "Supplier Quotation", "Purchase Order", "Purchase Invoice", "Purchase Receipt"]
.indexOf(doctype)!==-1) {
var refdoc = frappe.get_doc(doctype, docname);
cur_frm.set_value("supplier", refdoc.supplier || refdoc.name);
cur_frm.set_value("supplier_name", refdoc.supplier_name);
if(cur_frm.doc.doctype==="Address")
cur_frm.set_value("address_title", cur_frm.doc.supplier_name);
}
else if(["Lead", "Opportunity", "Quotation"]
.indexOf(doctype)!==-1) {
var refdoc = frappe.get_doc(doctype, docname);
if((refdoc.doctype == "Quotation" && refdoc.quotation_to=="Lead") ||
(refdoc.doctype == "Opportunity" && refdoc.enquiry_from=="Lead") || (doctype=="Lead")) {
cur_frm.set_value("lead", refdoc.lead || refdoc.name);
cur_frm.set_value("lead_name", refdoc.customer_name || refdoc.company_name || refdoc.lead_name);
if(cur_frm.doc.doctype==="Address")
cur_frm.set_value("address_title", cur_frm.doc.lead_name);
}
}
else if(doctype == "Sales Partner") {
cur_frm.set_value("sales_partner", docname);
}
}
}
}

View File

@@ -16,14 +16,13 @@ def print_settings_for_item_table(doc):
if doc.flags.compact_item_print:
doc.print_templates["description"] = "templates/print_formats/includes/item_table_description.html"
doc.hide_in_print_layout += ["item_code", "item_name", "image"]
doc.flags.compact_item_fields = ["description", "qty", "rate", "amount"]
doc.flags.show_in_description = []
doc.flags.format_columns = format_columns
for df in doc.meta.fields:
if df.fieldtype not in ("Section Break", "Column Break", "Button"):
if not doc.is_print_hide(df.fieldname):
if df.fieldname not in doc.hide_in_print_layout and df.fieldname not in doc.flags.compact_item_fields:
doc.hide_in_print_layout.append(df.fieldname)
doc.flags.show_in_description.append(df.fieldname)
def format_columns(display_columns, compact_fields):
compact_fields = compact_fields + ["image", "item_code", "item_name"]
final_columns = []
for column in display_columns:
if column not in compact_fields:
final_columns.append(column)
return final_columns

View File

@@ -3,32 +3,10 @@
from __future__ import unicode_literals
import frappe
from frappe.desk.reportview import get_match_cond
from frappe.model.db_query import DatabaseQuery
from frappe.desk.reportview import get_match_cond, get_filters_cond
from frappe.utils import nowdate
from collections import defaultdict
def get_filters_cond(doctype, filters, conditions):
if filters:
flt = filters
if isinstance(filters, dict):
filters = filters.items()
flt = []
for f in filters:
if isinstance(f[1], basestring) and f[1][0] == '!':
flt.append([doctype, f[0], '!=', f[1][1:]])
else:
value = frappe.db.escape(f[1]) if isinstance(f[1], basestring) else f[1]
flt.append([doctype, f[0], '=', value])
query = DatabaseQuery(doctype)
query.filters = flt
query.conditions = conditions
query.build_filter_conditions(flt, conditions)
cond = ' and ' + ' and '.join(query.conditions)
else:
cond = ''
return cond
# searches for active employees
def employee_query(doctype, txt, searchfield, start, page_len, filters):
@@ -88,7 +66,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters):
fields = ["name", "customer_group", "territory"]
else:
fields = ["name", "customer_name", "customer_group", "territory"]
meta = frappe.get_meta("Customer")
fields = fields + [f for f in meta.get_search_fields() if not f in fields]
@@ -371,3 +349,46 @@ def get_expense_account(doctype, txt, searchfield, start, page_len, filters):
'company': filters.get("company", ""),
'txt': "%%%s%%" % frappe.db.escape(txt)
})
@frappe.whitelist()
def warehouse_query(doctype, txt, searchfield, start, page_len, filters):
# Should be used when item code is passed in filters.
conditions, bin_conditions = [], []
filter_dict = get_doctype_wise_filters(filters)
sub_query = """ select round(`tabBin`.actual_qty, 2) from `tabBin`
where `tabBin`.warehouse = `tabWarehouse`.name
{bin_conditions} """.format(
bin_conditions=get_filters_cond(doctype, filter_dict.get("Bin"), bin_conditions))
response = frappe.db.sql("""select `tabWarehouse`.name,
CONCAT_WS(" : ", "Actual Qty", ifnull( ({sub_query}), 0) ) as actual_qty
from `tabWarehouse`
where
`tabWarehouse`.`{key}` like %(txt)s
{fcond} {mcond}
order by
`tabWarehouse`.name desc
limit
%(start)s, %(page_len)s
""".format(
sub_query=sub_query,
key=frappe.db.escape(searchfield),
fcond=get_filters_cond(doctype, filter_dict.get("Warehouse"), conditions),
mcond=get_match_cond(doctype)
),
{
"txt": "%%%s%%" % frappe.db.escape(txt),
"start": start,
"page_len": page_len
})
return response
def get_doctype_wise_filters(filters):
# Helper function to seperate filters doctype_wise
filter_dict = defaultdict(list)
for row in filters:
filter_dict[row[0]].append(row)
return filter_dict

View File

@@ -93,6 +93,9 @@ def make_new_document(reference_doc, date_field, posting_date):
"next_date": get_next_date(reference_doc.next_date, mcount,cint(reference_doc.repeat_on_day_of_month))
})
if new_document.meta.get_field('set_posting_time'):
new_document.set('set_posting_time', 1)
# copy document fields
for fieldname in ("owner", "recurring_type", "repeat_on_day_of_month",
"recurring_id", "notification_email_address", "is_recurring", "end_date",

View File

@@ -184,7 +184,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
doc.return_against = source.name
doc.ignore_pricing_rule = 1
if doctype == "Sales Invoice":
doc.is_pos = 0
doc.is_pos = source.is_pos
# look for Print Heading "Credit Note"
if not doc.select_print_heading:
@@ -198,6 +198,21 @@ def make_return_doc(doctype, source_name, target_doc=None):
if tax.charge_type == "Actual":
tax.tax_amount = -1 * tax.tax_amount
if doc.get("is_return"):
if doc.doctype == 'Sales Invoice':
doc.set('payments', [])
for data in source.payments:
doc.append('payments', {
'mode_of_payment': data.mode_of_payment,
'type': data.type,
'amount': -1 * data.amount,
'base_amount': -1 * data.base_amount
})
elif doc.doctype == 'Purchase Invoice':
doc.paid_amount = -1 * source.paid_amount
doc.base_paid_amount = -1 * source.base_paid_amount
doc.discount_amount = -1 * source.discount_amount
doc.run_method("calculate_taxes_and_totals")
def update_item(source_doc, target_doc, source_parent):

View File

@@ -9,6 +9,7 @@ from frappe import _, throw
from erpnext.stock.get_item_details import get_bin_details
from erpnext.stock.utils import get_incoming_rate
from erpnext.stock.stock_ledger import get_valuation_rate
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.controllers.stock_controller import StockController
@@ -34,6 +35,7 @@ class SellingController(StockController):
super(SellingController, self).validate()
self.validate_max_discount()
self.validate_selling_price()
self.set_qty_as_per_stock_uom()
check_active_sales_items(self)
def set_missing_values(self, for_validate=False):
@@ -163,6 +165,13 @@ class SellingController(StockController):
if discount and flt(d.discount_percentage) > discount:
frappe.throw(_("Maxiumm discount for Item {0} is {1}%").format(d.item_code, discount))
def set_qty_as_per_stock_uom(self):
for d in self.get("items"):
if d.meta.get_field("stock_qty"):
if not d.conversion_factor:
frappe.throw(_("Row {0}: Conversion Factor is mandatory").format(d.idx))
d.stock_qty = flt(d.qty) * flt(d.conversion_factor)
def validate_selling_price(self):
def throw_message(item_name, rate, ref_rate_field):
frappe.throw(_("""Selling price for item {0} is lower than its {1}. Selling price should be atleast {2}""")
@@ -211,9 +220,10 @@ class SellingController(StockController):
il.append(frappe._dict({
'warehouse': d.warehouse,
'item_code': d.item_code,
'qty': d.qty,
'uom': d.stock_uom,
'qty': d.stock_qty,
'uom': d.uom,
'stock_uom': d.stock_uom,
'conversion_factor': d.conversion_factor,
'batch_no': cstr(d.get("batch_no")).strip(),
'serial_no': cstr(d.get("serial_no")).strip(),
'name': d.name,
@@ -282,6 +292,8 @@ class SellingController(StockController):
sl_entries = []
for d in self.get_item_list():
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1 and flt(d.qty):
if flt(d.conversion_factor)==0.0:
d.conversion_factor = get_conversion_factor(d.item_code, d.uom).get("conversion_factor") or 1.0
return_rate = 0
if cint(self.is_return) and self.return_against and self.docstatus==1:
return_rate = self.get_incoming_rate_for_sales_return(d.item_code, self.return_against)

View File

@@ -6,7 +6,6 @@ import frappe
from frappe.utils import flt, comma_or, nowdate, getdate
from frappe import _
from frappe.model.document import Document
from erpnext.accounts.party_status import notify_status
def validate_status(status, options):
if status not in options:
@@ -14,15 +13,17 @@ def validate_status(status, options):
status_map = {
"Lead": [
["Converted", "has_customer"],
["Lost Quotation", "has_lost_quotation"],
["Opportunity", "has_opportunity"],
["Quotation", "has_quotation"],
["Converted", "has_customer"],
],
"Opportunity": [
["Quotation", "has_quotation"],
["Converted", "has_ordered_quotation"],
["Lost", "eval:self.status=='Lost'"],
["Lost", "has_lost_quotation"],
["Closed", "eval:self.status=='Closed'"]
],
"Quotation": [
["Draft", None],
@@ -101,6 +102,8 @@ class StatusUpdater(Document):
def set_status(self, update=False, status=None, update_modified=True):
if self.is_new():
if self.get('amended_from'):
self.status = 'Draft'
return
if self.doctype in status_map:
@@ -283,7 +286,6 @@ class StatusUpdater(Document):
target = frappe.get_doc(args["target_parent_dt"], args["name"])
target.set_status(update=True)
target.notify_update()
notify_status(target)
def _update_modified(self, args, update_modified):
args['update_modified'] = ''

View File

@@ -9,9 +9,14 @@ import frappe.defaults
from erpnext.accounts.utils import get_fiscal_year
from erpnext.accounts.general_ledger import make_gl_entries, delete_gl_entries, process_gl_map
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.stock.stock_ledger import get_valuation_rate
class StockController(AccountsController):
def make_gl_entries(self, repost_future_gle=True):
def validate(self):
super(StockController, self).validate()
self.validate_inspection()
def make_gl_entries(self, gl_entries=None, repost_future_gle=True, from_repost=False):
if self.docstatus == 2:
delete_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
@@ -19,8 +24,9 @@ class StockController(AccountsController):
warehouse_account = get_warehouse_account()
if self.docstatus==1:
gl_entries = self.get_gl_entries(warehouse_account)
make_gl_entries(gl_entries)
if not gl_entries:
gl_entries = self.get_gl_entries(warehouse_account)
make_gl_entries(gl_entries, from_repost=from_repost)
if repost_future_gle:
items, warehouses = self.get_items_and_warehouses()
@@ -38,32 +44,43 @@ class StockController(AccountsController):
gl_list = []
warehouse_with_no_account = []
for detail in voucher_details:
sle_list = sle_map.get(detail.name)
for item_row in voucher_details:
sle_list = sle_map.get(item_row.name)
if sle_list:
for sle in sle_list:
if warehouse_account.get(sle.warehouse):
# from warehouse account
self.check_expense_account(detail)
self.check_expense_account(item_row)
# If item is not a sample item
# and ( valuation rate not mentioned in an incoming entry
# or incoming entry not found while delivering the item),
# try to pick valuation rate from previous sle or Item master and update in SLE
# Otherwise, throw an exception
if not sle.stock_value_difference and self.doctype != "Stock Reconciliation" \
and not item_row.get("is_sample_item"):
sle = self.update_stock_ledger_entries(sle)
gl_list.append(self.get_gl_dict({
"account": warehouse_account[sle.warehouse]["name"],
"against": detail.expense_account,
"cost_center": detail.cost_center,
"against": item_row.expense_account,
"cost_center": item_row.cost_center,
"remarks": self.get("remarks") or "Accounting Entry for Stock",
"debit": flt(sle.stock_value_difference, 2),
}, warehouse_account[sle.warehouse]["account_currency"]))
# to target warehouse / expense account
gl_list.append(self.get_gl_dict({
"account": detail.expense_account,
"account": item_row.expense_account,
"against": warehouse_account[sle.warehouse]["name"],
"cost_center": detail.cost_center,
"cost_center": item_row.cost_center,
"remarks": self.get("remarks") or "Accounting Entry for Stock",
"credit": flt(sle.stock_value_difference, 2),
"project": detail.get("project") or self.get("project")
"project": item_row.get("project") or self.get("project")
}))
elif sle.warehouse not in warehouse_with_no_account:
warehouse_with_no_account.append(sle.warehouse)
@@ -72,12 +89,32 @@ class StockController(AccountsController):
for wh in warehouse_with_no_account:
if frappe.db.get_value("Warehouse", wh, "company"):
frappe.throw(_("Warehouse {0} is not linked to any account, please create/link the corresponding (Asset) account for the warehouse.").format(wh))
msgprint(_("No accounting entries for the following warehouses") + ": \n" +
"\n".join(warehouse_with_no_account))
return process_gl_map(gl_list)
def update_stock_ledger_entries(self, sle):
sle.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
self.doctype, self.name)
sle.stock_value = flt(sle.qty_after_transaction) * flt(sle.valuation_rate)
sle.stock_value_difference = flt(sle.actual_qty) * flt(sle.valuation_rate)
if sle.name:
frappe.db.sql("""
update
`tabStock Ledger Entry`
set
stock_value = %(stock_value)s,
valuation_rate = %(valuation_rate)s,
stock_value_difference = %(stock_value_difference)s
where
name = %(name)s""", (sle))
return sle
def get_voucher_details(self, default_expense_account, default_cost_center, sle_map):
if self.doctype == "Stock Reconciliation":
return [frappe._dict({ "name": voucher_detail_no, "expense_account": default_expense_account,
@@ -125,10 +162,18 @@ class StockController(AccountsController):
def get_stock_ledger_details(self):
stock_ledger = {}
for sle in frappe.db.sql("""select warehouse, stock_value_difference,
voucher_detail_no, item_code, posting_date, actual_qty
from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s""",
(self.doctype, self.name), as_dict=True):
stock_ledger_entries = frappe.db.sql("""
select
name, warehouse, stock_value_difference, valuation_rate,
voucher_detail_no, item_code, posting_date, posting_time,
actual_qty, qty_after_transaction
from
`tabStock Ledger Entry`
where
voucher_type=%s and voucher_no=%s
""", (self.doctype, self.name), as_dict=True)
for sle in stock_ledger_entries:
stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle)
return stock_ledger
@@ -211,7 +256,7 @@ class StockController(AccountsController):
def make_gl_entries_on_cancel(self, repost_future_gle=True):
if frappe.db.sql("""select name from `tabGL Entry` where voucher_type=%s
and voucher_no=%s""", (self.doctype, self.name)):
self.make_gl_entries(repost_future_gle)
self.make_gl_entries(repost_future_gle=repost_future_gle)
def get_serialized_items(self):
serialized_items = []
@@ -234,7 +279,7 @@ class StockController(AccountsController):
incoming_rate = incoming_rate[0][0] if incoming_rate else 0.0
return incoming_rate
def validate_warehouse(self):
from erpnext.stock.utils import validate_warehouse_company
@@ -243,7 +288,7 @@ class StockController(AccountsController):
for w in warehouses:
validate_warehouse_company(w, self.company)
def update_billing_percentage(self, update_modified=True):
self._update_percent_field({
"target_dt": self.doctype + " Item",
@@ -254,6 +299,28 @@ class StockController(AccountsController):
"name": self.name,
}, update_modified)
def validate_inspection(self):
'''Checks if quality inspection is set for Items that require inspection.
On submit, throw an exception'''
inspection_required_fieldname = None
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
inspection_required_fieldname = "inspection_required_before_purchase"
elif self.doctype in ["Delivery Note", "Sales Invoice"]:
inspection_required_fieldname = "inspection_required_before_delivery"
if not inspection_required_fieldname or \
(self.doctype in ["Sales Invoice", "Purchase Invoice"] and not self.update_stock):
return
for d in self.get('items'):
if (frappe.db.get_value("Item", d.item_code, inspection_required_fieldname)
and not d.quality_inspection):
frappe.msgprint(_("Quality Inspection required for Item {0}").format(d.item_code))
if self.docstatus==1:
raise frappe.ValidationError
def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None,
warehouse_account=None):
def _delete_gl_entries(voucher_type, voucher_no):
@@ -265,7 +332,7 @@ def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for
future_stock_vouchers = get_future_stock_vouchers(posting_date, posting_time, for_warehouses, for_items)
gle = get_voucherwise_gl_entries(future_stock_vouchers, posting_date)
for voucher_type, voucher_no in future_stock_vouchers:
existing_gle = gle.get((voucher_type, voucher_no), [])
voucher_obj = frappe.get_doc(voucher_type, voucher_no)
@@ -273,7 +340,7 @@ def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for
if expected_gle:
if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle):
_delete_gl_entries(voucher_type, voucher_no)
voucher_obj.make_gl_entries(repost_future_gle=False)
voucher_obj.make_gl_entries(gl_entries=expected_gle, repost_future_gle=False, from_repost=True)
else:
_delete_gl_entries(voucher_type, voucher_no)
@@ -328,10 +395,19 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
return gl_entries
def get_warehouse_account():
warehouse_account = frappe._dict()
if not frappe.flags.warehouse_account_map or frappe.flags.in_test:
warehouse_account = frappe._dict()
for d in frappe.db.sql("""select warehouse, name, account_currency from tabAccount
where account_type = 'Stock' and (warehouse is not null and warehouse != ''
and is_group != 1) and is_group=0 """, as_dict=1):
for d in frappe.db.sql("""select
warehouse, name, account_currency
from
tabAccount
where
account_type = 'Stock'
and (warehouse is not null and warehouse != '')
and is_group=0 """, as_dict=1):
warehouse_account.setdefault(d.warehouse, d)
return warehouse_account
frappe.flags.warehouse_account_map = warehouse_account
return frappe.flags.warehouse_account_map

View File

@@ -366,8 +366,9 @@ class calculate_taxes_and_totals(object):
# discount amount rounding loss adjustment if no taxes
if (not taxes or self.doc.apply_discount_on == "Net Total") \
and i == len(self.doc.get("items")) - 1:
discount_amount_loss = flt(self.doc.total - net_total - self.doc.discount_amount,
discount_amount_loss = flt(self.doc.net_total - net_total - self.doc.discount_amount,
self.doc.precision("net_total"))
item.net_amount = flt(item.net_amount + discount_amount_loss,
item.precision("net_amount"))
@@ -452,13 +453,15 @@ class calculate_taxes_and_totals(object):
elif self.doc.doctype == "Purchase Invoice":
self.doc.outstanding_amount = flt(total_amount_to_pay, self.doc.precision("outstanding_amount"))
def calculate_paid_amount(self):
paid_amount = base_paid_amount = 0.0
for payment in self.doc.get('payments'):
payment.base_amount = flt(payment.amount * self.doc.conversion_rate)
paid_amount += payment.amount
base_paid_amount += payment.base_amount
if self.doc.is_pos:
for payment in self.doc.get('payments'):
payment.base_amount = flt(payment.amount * self.doc.conversion_rate)
paid_amount += payment.amount
base_paid_amount += payment.base_amount
self.doc.paid_amount = flt(paid_amount, self.doc.precision("paid_amount"))
self.doc.base_paid_amount = flt(base_paid_amount, self.doc.precision("base_paid_amount"))
@@ -467,7 +470,7 @@ class calculate_taxes_and_totals(object):
self.doc.change_amount = 0.0
self.doc.base_change_amount = 0.0
if self.doc.paid_amount > self.doc.grand_total:
self.doc.change_amount = flt(self.doc.paid_amount - self.doc.grand_total +
self.doc.change_amount = flt(self.doc.paid_amount - self.doc.grand_total +
self.doc.write_off_amount, self.doc.precision("change_amount"))
self.doc.base_change_amount = flt(self.doc.base_paid_amount - self.doc.base_grand_total +
@@ -483,7 +486,7 @@ class calculate_taxes_and_totals(object):
def calculate_margin(self, item):
total_margin = 0.0
if item.price_list_rate:
if item.pricing_rule and not self.doc.ignore_pricing_rule:
if item.pricing_rule and not self.doc.ignore_pricing_rule:
pricing_rule = frappe.get_doc('Pricing Rule', item.pricing_rule)
item.margin_type = pricing_rule.margin_type
item.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
@@ -492,4 +495,4 @@ class calculate_taxes_and_totals(object):
margin_value = item.margin_rate_or_amount if item.margin_type == 'Amount' else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100
total_margin = flt(item.price_list_rate) + flt(margin_value)
return total_margin
return total_margin

View File

@@ -147,9 +147,9 @@ def period_wise_columns_query(filters, trans):
else:
pwc = [_(filters.get("fiscal_year")) + " ("+_("Qty") + "):Float:120",
_(filters.get("fiscal_year")) + " ("+ _("Amt") + "):Currency:120"]
query_details = " SUM(t2.qty), SUM(t2.base_net_amount),"
query_details = " SUM(t2.stock_qty), SUM(t2.base_net_amount),"
query_details += 'SUM(t2.qty), SUM(t2.base_net_amount)'
query_details += 'SUM(t2.stock_qty), SUM(t2.base_net_amount)'
return pwc, query_details
def get_period_wise_columns(bet_dates, period, pwc):
@@ -161,7 +161,7 @@ def get_period_wise_columns(bet_dates, period, pwc):
_(get_mon(bet_dates[0])) + "-" + _(get_mon(bet_dates[1])) + " (" + _("Amt") + "):Currency:120"]
def get_period_wise_query(bet_dates, trans_date, query_details):
query_details += """SUM(IF(t1.%(trans_date)s BETWEEN '%(sd)s' AND '%(ed)s', t2.qty, NULL)),
query_details += """SUM(IF(t1.%(trans_date)s BETWEEN '%(sd)s' AND '%(ed)s', t2.stock_qty, NULL)),
SUM(IF(t1.%(trans_date)s BETWEEN '%(sd)s' AND '%(ed)s', t2.base_net_amount, NULL)),
""" % {"trans_date": trans_date, "sd": bet_dates[0],"ed": bet_dates[1]}
return query_details

View File

@@ -100,11 +100,12 @@ def post_process(doctype, data):
def get_customers_suppliers(doctype, user):
meta = frappe.get_meta(doctype)
contacts = frappe.get_all("Contact", fields=["customer", "supplier", "email_id"],
filters={"email_id": user})
contacts = frappe.db.sql(""" select `tabContact`.email_id, `tabDynamic Link`.link_doctype, `tabDynamic Link`.link_name
from `tabContact`, `tabDynamic Link` where
`tabContact`.name = `tabDynamic Link`.parent and `tabContact`.email_id =%s """, user, as_dict=1)
customers = [c.customer for c in contacts if c.customer] if meta.get_field("customer") else None
suppliers = [c.supplier for c in contacts if c.supplier] if meta.get_field("supplier") else None
customers = [c.link_name for c in contacts if c.link_doctype == 'Customer'] if meta.get_field("customer") else None
suppliers = [c.link_name for c in contacts if c.link_doctype == 'Supplier'] if meta.get_field("supplier") else None
return customers, suppliers