Merge branch 'develop' into updated-requirements
diff --git a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py
index 716bef3..43acded 100644
--- a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py
+++ b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py
@@ -93,7 +93,8 @@
fields = ['posting_date', 'debit', 'credit'],
filters = [
dict(posting_date = ('<', to_date)),
- dict(account = ('in', child_accounts))
+ dict(account = ('in', child_accounts)),
+ dict(voucher_type = ('!=', 'Period Closing Voucher'))
],
order_by = 'posting_date asc')
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 0ebca8b..fefd36a 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -991,10 +991,8 @@
continue
for serial_no in item.serial_no.split("\n"):
- if serial_no and frappe.db.exists('Serial No', serial_no):
- sno = frappe.get_doc('Serial No', serial_no)
- sno.sales_invoice = invoice
- sno.db_update()
+ if serial_no and frappe.db.get_value('Serial No', serial_no, 'item_code') == item.item_code:
+ frappe.db.set_value('Serial No', serial_no, 'sales_invoice', invoice)
def validate_serial_numbers(self):
"""
@@ -1040,8 +1038,9 @@
continue
for serial_no in item.serial_no.split("\n"):
- sales_invoice = frappe.db.get_value("Serial No", serial_no, "sales_invoice")
- if sales_invoice and self.name != sales_invoice:
+ sales_invoice, item_code = frappe.db.get_value("Serial No", serial_no,
+ ["sales_invoice", "item_code"])
+ if sales_invoice and item_code == item.item_code and self.name != sales_invoice:
sales_invoice_company = frappe.db.get_value("Sales Invoice", sales_invoice, "company")
if sales_invoice_company == self.company:
frappe.throw(_("Serial Number: {0} is already referenced in Sales Invoice: {1}"
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index d4dac72..38f283c 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -163,9 +163,16 @@
.format(account), StockAccountInvalidTransaction)
elif account_bal != stock_bal:
- frappe.throw(_("Account Balance ({0}) and Stock Value ({1}) is out of sync for account {2} and linked warehouse ({3}). Please create adjustment Journal Entry for amount {4}.")
- .format(account_bal, stock_bal, account, comma_and(warehouse_list), stock_bal - account_bal),
- StockValueAndAccountBalanceOutOfSync)
+ error_reason = _("Account Balance ({0}) and Stock Value ({1}) is out of sync for account {2} and it's linked warehouses.").format(
+ account_bal, stock_bal, frappe.bold(account))
+ error_resolution = _("Please create adjustment Journal Entry for amount {0} ").format(frappe.bold(stock_bal - account_bal))
+ button_text = _("Make Adjustment Entry")
+
+ frappe.throw("""{0}<br></br>{1}<br></br>
+ <div style="text-align:right;">
+ <button class="btn btn-primary" onclick="frappe.new_doc('Journal Entry')">{2}</button>
+ </div>""".format(error_reason, error_resolution, button_text),
+ StockValueAndAccountBalanceOutOfSync, title=_('Account Balance Out Of Sync'))
def validate_cwip_accounts(gl_map):
cwip_enabled = cint(frappe.get_cached_value("Company",
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index bcbd427..14906f2 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -188,7 +188,11 @@
self.data.append(row)
def set_invoice_details(self, row):
- row.update(self.invoice_details.get(row.voucher_no, {}))
+ invoice_details = self.invoice_details.get(row.voucher_no, {})
+ if row.due_date:
+ invoice_details.pop("due_date", None)
+ row.update(invoice_details)
+
if row.voucher_type == 'Sales Invoice':
if self.filters.show_delivery_notes:
self.set_delivery_notes(row)
diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
index b90a7a9..8955830 100644
--- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
+++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
@@ -36,6 +36,9 @@
self.filters.report_date) or {}
for party, party_dict in iteritems(self.party_total):
+ if party_dict.outstanding <= 0:
+ continue
+
row = frappe._dict()
row.party = party
diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.js b/erpnext/accounts/report/balance_sheet/balance_sheet.js
index 4bc29da..8c11514 100644
--- a/erpnext/accounts/report/balance_sheet/balance_sheet.js
+++ b/erpnext/accounts/report/balance_sheet/balance_sheet.js
@@ -2,7 +2,7 @@
// License: GNU General Public License v3. See license.txt
frappe.require("assets/erpnext/js/financial_statements.js", function() {
- frappe.query_reports["Balance Sheet"] = erpnext.financial_statements;
+ frappe.query_reports["Balance Sheet"] = $.extend({}, erpnext.financial_statements);
frappe.query_reports["Balance Sheet"]["filters"].push({
"fieldname": "accumulated_values",
diff --git a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py
index bb9045c..3e51933 100644
--- a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py
+++ b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py
@@ -51,27 +51,25 @@
self.create_task(disease_doc.treatment_task, self.name, start_date)
def create_project(self, period, crop_tasks):
- project = frappe.new_doc("Project")
- project.update({
+ project = frappe.get_doc({
+ "doctype": "Project",
"project_name": self.title,
"expected_start_date": self.start_date,
"expected_end_date": add_days(self.start_date, period - 1)
- })
- project.insert()
+ }).insert()
return project.name
def create_task(self, crop_tasks, project_name, start_date):
for crop_task in crop_tasks:
- task = frappe.new_doc("Task")
- task.update({
+ frappe.get_doc({
+ "doctype": "Task",
"subject": crop_task.get("task_name"),
"priority": crop_task.get("priority"),
"project": project_name,
"exp_start_date": add_days(start_date, crop_task.get("start_day") - 1),
"exp_end_date": add_days(start_date, crop_task.get("end_day") - 1)
- })
- task.insert()
+ }).insert()
def reload_linked_analysis(self):
linked_doctypes = ['Soil Texture', 'Soil Analysis', 'Plant Analysis']
diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json
index 8fda330..6882f6a 100644
--- a/erpnext/assets/doctype/asset/asset.json
+++ b/erpnext/assets/doctype/asset/asset.json
@@ -33,6 +33,7 @@
"available_for_use_date",
"column_break_18",
"calculate_depreciation",
+ "allow_monthly_depreciation",
"is_existing_asset",
"opening_accumulated_depreciation",
"number_of_depreciations_booked",
@@ -216,8 +217,7 @@
{
"fieldname": "available_for_use_date",
"fieldtype": "Date",
- "label": "Available-for-use Date",
- "reqd": 1
+ "label": "Available-for-use Date"
},
{
"fieldname": "column_break_18",
@@ -450,12 +450,19 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "calculate_depreciation",
+ "fieldname": "allow_monthly_depreciation",
+ "fieldtype": "Check",
+ "label": "Allow Monthly Depreciation"
}
],
"idx": 72,
"image_field": "image",
"is_submittable": 1,
- "modified": "2019-10-07 15:34:30.976208",
+ "modified": "2019-10-22 15:47:36.050828",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 94e6f61..d1f8c1a 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -6,7 +6,7 @@
import frappe, erpnext, math, json
from frappe import _
from six import string_types
-from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff, add_days
+from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff, month_diff, add_days
from frappe.model.document import Document
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.assets.doctype.asset.depreciation \
@@ -149,19 +149,31 @@
schedule_date = add_months(d.depreciation_start_date,
n * cint(d.frequency_of_depreciation))
+ # schedule date will be a year later from start date
+ # so monthly schedule date is calculated by removing 11 months from it
+ monthly_schedule_date = add_months(schedule_date, - d.frequency_of_depreciation + 1)
+
# For first row
if has_pro_rata and n==0:
- depreciation_amount, days = get_pro_rata_amt(d, depreciation_amount,
+ depreciation_amount, days, months = get_pro_rata_amt(d, depreciation_amount,
self.available_for_use_date, d.depreciation_start_date)
+
+ # For first depr schedule date will be the start date
+ # so monthly schedule date is calculated by removing month difference between use date and start date
+ monthly_schedule_date = add_months(d.depreciation_start_date, - months + 1)
+
# For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
to_date = add_months(self.available_for_use_date,
n * cint(d.frequency_of_depreciation))
- depreciation_amount, days = get_pro_rata_amt(d,
+ depreciation_amount, days, months = get_pro_rata_amt(d,
depreciation_amount, schedule_date, to_date)
+ monthly_schedule_date = add_months(schedule_date, 1)
+
schedule_date = add_days(schedule_date, days)
+ last_schedule_date = schedule_date
if not depreciation_amount: continue
value_after_depreciation -= flt(depreciation_amount,
@@ -175,13 +187,50 @@
skip_row = True
if depreciation_amount > 0:
- self.append("schedules", {
- "schedule_date": schedule_date,
- "depreciation_amount": depreciation_amount,
- "depreciation_method": d.depreciation_method,
- "finance_book": d.finance_book,
- "finance_book_id": d.idx
- })
+ # With monthly depreciation, each depreciation is divided by months remaining until next date
+ if self.allow_monthly_depreciation:
+ # month range is 1 to 12
+ # In pro rata case, for first and last depreciation, month range would be different
+ month_range = months \
+ if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \
+ else d.frequency_of_depreciation
+
+ for r in range(month_range):
+ if (has_pro_rata and n == 0):
+ # For first entry of monthly depr
+ if r == 0:
+ days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date)
+ per_day_amt = depreciation_amount / days
+ depreciation_amount_for_current_month = per_day_amt * days_until_first_depr
+ depreciation_amount -= depreciation_amount_for_current_month
+ date = monthly_schedule_date
+ amount = depreciation_amount_for_current_month
+ else:
+ date = add_months(monthly_schedule_date, r)
+ amount = depreciation_amount / (month_range - 1)
+ elif (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) and r == cint(month_range) - 1:
+ # For last entry of monthly depr
+ date = last_schedule_date
+ amount = depreciation_amount / month_range
+ else:
+ date = add_months(monthly_schedule_date, r)
+ amount = depreciation_amount / month_range
+
+ self.append("schedules", {
+ "schedule_date": date,
+ "depreciation_amount": amount,
+ "depreciation_method": d.depreciation_method,
+ "finance_book": d.finance_book,
+ "finance_book_id": d.idx
+ })
+ else:
+ self.append("schedules", {
+ "schedule_date": schedule_date,
+ "depreciation_amount": depreciation_amount,
+ "depreciation_method": d.depreciation_method,
+ "finance_book": d.finance_book,
+ "finance_book_id": d.idx
+ })
def check_is_pro_rata(self, row):
has_pro_rata = False
@@ -588,9 +637,10 @@
def get_pro_rata_amt(row, depreciation_amount, from_date, to_date):
days = date_diff(to_date, from_date)
+ months = month_diff(to_date, from_date)
total_days = get_total_days(to_date, row.frequency_of_depreciation)
- return (depreciation_amount * flt(days)) / flt(total_days), days
+ return (depreciation_amount * flt(days)) / flt(total_days), days, months
def get_total_days(date, frequency):
period_start_date = add_months(date,
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
index 9ad06f9..2f0cfa6 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
@@ -134,7 +134,7 @@
if (args.search_type === "Tag" && args.tag) {
return frappe.call({
type: "GET",
- method: "frappe.desk.tags.get_tagged_docs",
+ method: "frappe.desk.doctype.tag.tag.get_tagged_docs",
args: {
"doctype": "Supplier",
"tag": args.tag
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
index a10ce46..95db33b 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -344,13 +344,9 @@
@frappe.whitelist()
def get_supplier_tag():
- data = frappe.db.sql("select _user_tags from `tabSupplier`")
+ if not frappe.cache().hget("Supplier", "Tags"):
+ filters = {"document_type": "Supplier"}
+ tags = list(set([tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag]))
+ frappe.cache().hset("Supplier", "Tags", tags)
- tags = []
- for tag in data:
- tags += filter(bool, tag[0].split(","))
-
- tags = list(set(tags))
-
- return tags
-
+ return frappe.cache().hget("Supplier", "Tags")
diff --git a/erpnext/hr/doctype/department_approver/department_approver.py b/erpnext/hr/doctype/department_approver/department_approver.py
index 9f2f201..d6b66da 100644
--- a/erpnext/hr/doctype/department_approver/department_approver.py
+++ b/erpnext/hr/doctype/department_approver/department_approver.py
@@ -19,14 +19,20 @@
approvers = []
department_details = {}
department_list = []
- employee_department = filters.get("department") or frappe.get_value("Employee", filters.get("employee"), "department")
+ employee = frappe.get_value("Employee", filters.get("employee"), ["department", "leave_approver"], as_dict=True)
+ if employee.leave_approver:
+ approver = frappe.db.get_value("User", employee.leave_approver, ['name', 'first_name', 'last_name'])
+ approvers.append(approver)
+ return approvers
+
+ employee_department = filters.get("department") or employee.department
if employee_department:
department_details = frappe.db.get_value("Department", {"name": employee_department}, ["lft", "rgt"], as_dict=True)
if department_details:
department_list = frappe.db.sql("""select name from `tabDepartment` where lft <= %s
and rgt >= %s
and disabled=0
- order by lft desc""", (department_details.lft, department_details.rgt), as_list = True)
+ order by lft desc""", (department_details.lft, department_details.rgt), as_list=True)
if filters.get("doctype") == "Leave Application":
parentfield = "leave_approvers"
@@ -41,4 +47,4 @@
and approver.parentfield = %s
and approver.approver=user.name""",(d, "%" + txt + "%", parentfield), as_list=True)
- return approvers
\ No newline at end of file
+ return approvers
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index e1e5e80..0e66305 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -55,11 +55,11 @@
self.reload()
def on_cancel(self):
+ self.create_leave_ledger_entry(submit=False)
self.status = "Cancelled"
# notify leave applier about cancellation
self.notify_employee()
self.cancel_attendance()
- self.create_leave_ledger_entry(submit=False)
def validate_applicable_after(self):
if self.leave_type:
@@ -351,6 +351,9 @@
pass
def create_leave_ledger_entry(self, submit=True):
+ if self.status != 'Approved':
+ return
+
expiry_date = get_allocation_expiry(self.employee, self.leave_type,
self.to_date, self.from_date)
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index 5198937..3b24d0f 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -3,6 +3,11 @@
frappe.ui.form.on('Production Plan', {
setup: function(frm) {
+ frm.custom_make_buttons = {
+ 'Work Order': 'Work Order',
+ 'Material Request': 'Material Request',
+ };
+
frm.fields_dict['po_items'].grid.get_field('warehouse').get_query = function(doc) {
return {
filters: {
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index cdbce33..107c79b 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -395,6 +395,11 @@
}
});
}
+ },
+
+ additional_operating_cost: function(frm) {
+ erpnext.work_order.calculate_cost(frm.doc);
+ erpnext.work_order.calculate_total_cost(frm);
}
});
@@ -534,8 +539,7 @@
},
calculate_total_cost: function(frm) {
- var variable_cost = frm.doc.actual_operating_cost ?
- flt(frm.doc.actual_operating_cost) : flt(frm.doc.planned_operating_cost);
+ let variable_cost = flt(frm.doc.actual_operating_cost) || flt(frm.doc.planned_operating_cost);
frm.set_value("total_operating_cost", (flt(frm.doc.additional_operating_cost) + variable_cost));
},
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index ae4d9be..089cb80 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -216,14 +216,24 @@
self.db_set(fieldname, qty)
from erpnext.selling.doctype.sales_order.sales_order import update_produced_qty_in_so_item
- update_produced_qty_in_so_item(self.sales_order_item)
+
+ if self.sales_order and self.sales_order_item:
+ update_produced_qty_in_so_item(self.sales_order, self.sales_order_item)
if self.production_plan:
self.update_production_plan_status()
def update_production_plan_status(self):
production_plan = frappe.get_doc('Production Plan', self.production_plan)
- production_plan.run_method("update_produced_qty", self.produced_qty, self.production_plan_item)
+ produced_qty = 0
+ if self.production_plan_item:
+ total_qty = frappe.get_all("Work Order", fields = "sum(produced_qty) as produced_qty",
+ filters = {'docstatus': 1, 'production_plan': self.production_plan,
+ 'production_plan_item': self.production_plan_item}, as_list=1)
+
+ produced_qty = total_qty[0][0] if total_qty else 0
+
+ production_plan.run_method("update_produced_qty", produced_qty, self.production_plan_item)
def on_submit(self):
if not self.wip_warehouse:
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 0155b27..9e4dc12 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -638,11 +638,11 @@
erpnext.patches.v12_0.rename_bank_account_field_in_journal_entry_account
erpnext.patches.v12_0.create_default_energy_point_rules
erpnext.patches.v12_0.set_produced_qty_field_in_sales_order_for_work_order
-erpnext.patches.v12_0.generate_leave_ledger_entries
erpnext.patches.v12_0.set_default_shopify_app_type
erpnext.patches.v12_0.set_cwip_and_delete_asset_settings
erpnext.patches.v12_0.set_expense_account_in_landed_cost_voucher_taxes
erpnext.patches.v12_0.replace_accounting_with_accounts_in_home_settings
erpnext.patches.v12_0.set_payment_entry_status
erpnext.patches.v12_0.update_owner_fields_in_acc_dimension_custom_fields
-erpnext.patches.v12_0.set_default_for_add_taxes_from_item_tax_template
\ No newline at end of file
+erpnext.patches.v12_0.set_default_for_add_taxes_from_item_tax_template
+erpnext.patches.v12_0.remove_denied_leaves_from_leave_ledger
\ No newline at end of file
diff --git a/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py b/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py
index 412f320..f25b9ea 100644
--- a/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py
+++ b/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py
@@ -1,20 +1,30 @@
import frappe
import json
from six import iteritems
+from frappe.model.naming import make_autoname
def execute():
if "tax_type" not in frappe.db.get_table_columns("Item Tax"):
return
old_item_taxes = {}
item_tax_templates = {}
- rename_template_to_untitled = []
+
+ frappe.reload_doc("accounts", "doctype", "item_tax_template_detail", force=1)
+ frappe.reload_doc("accounts", "doctype", "item_tax_template", force=1)
+ existing_templates = frappe.db.sql("""select template.name, details.tax_type, details.tax_rate
+ from `tabItem Tax Template` template, `tabItem Tax Template Detail` details
+ where details.parent=template.name
+ """, as_dict=1)
+
+ if len(existing_templates):
+ for d in existing_templates:
+ item_tax_templates.setdefault(d.name, {})
+ item_tax_templates[d.name][d.tax_type] = d.tax_rate
for d in frappe.db.sql("""select parent as item_code, tax_type, tax_rate from `tabItem Tax`""", as_dict=1):
old_item_taxes.setdefault(d.item_code, [])
old_item_taxes[d.item_code].append(d)
- frappe.reload_doc("accounts", "doctype", "item_tax_template_detail", force=1)
- frappe.reload_doc("accounts", "doctype", "item_tax_template", force=1)
frappe.reload_doc("stock", "doctype", "item", force=1)
frappe.reload_doc("stock", "doctype", "item_tax", force=1)
frappe.reload_doc("selling", "doctype", "quotation_item", force=1)
@@ -27,6 +37,8 @@
frappe.reload_doc("accounts", "doctype", "purchase_invoice_item", force=1)
frappe.reload_doc("accounts", "doctype", "accounts_settings", force=1)
+ frappe.db.auto_commit_on_many_writes = True
+
# for each item that have item tax rates
for item_code in old_item_taxes.keys():
# make current item's tax map
@@ -34,8 +46,7 @@
for d in old_item_taxes[item_code]:
item_tax_map[d.tax_type] = d.tax_rate
- item_tax_template_name = get_item_tax_template(item_tax_templates, rename_template_to_untitled,
- item_tax_map, item_code)
+ item_tax_template_name = get_item_tax_template(item_tax_templates, item_tax_map, item_code)
# update the item tax table
item = frappe.get_doc("Item", item_code)
@@ -49,35 +60,33 @@
'Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice',
'Supplier Quotation', 'Purchase Order', 'Purchase Receipt', 'Purchase Invoice'
]
+
for dt in doctypes:
for d in frappe.db.sql("""select name, parent, item_code, item_tax_rate from `tab{0} Item`
- where ifnull(item_tax_rate, '') not in ('', '{{}}')""".format(dt), as_dict=1):
+ where ifnull(item_tax_rate, '') not in ('', '{{}}')
+ and item_tax_template is NULL""".format(dt), as_dict=1):
item_tax_map = json.loads(d.item_tax_rate)
- item_tax_template = get_item_tax_template(item_tax_templates, rename_template_to_untitled,
+ item_tax_template_name = get_item_tax_template(item_tax_templates,
item_tax_map, d.item_code, d.parent)
- frappe.db.set_value(dt + " Item", d.name, "item_tax_template", item_tax_template)
+ frappe.db.set_value(dt + " Item", d.name, "item_tax_template", item_tax_template_name)
- idx = 1
- for oldname in rename_template_to_untitled:
- frappe.rename_doc("Item Tax Template", oldname, "Untitled {}".format(idx))
- idx += 1
+ frappe.db.auto_commit_on_many_writes = False
settings = frappe.get_single("Accounts Settings")
settings.add_taxes_from_item_tax_template = 0
settings.determine_address_tax_category_from = "Billing Address"
settings.save()
-def get_item_tax_template(item_tax_templates, rename_template_to_untitled, item_tax_map, item_code, parent=None):
+def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parent=None):
# search for previously created item tax template by comparing tax maps
for template, item_tax_template_map in iteritems(item_tax_templates):
if item_tax_map == item_tax_template_map:
- if not parent:
- rename_template_to_untitled.append(template)
return template
# if no item tax template found, create one
item_tax_template = frappe.new_doc("Item Tax Template")
- item_tax_template.title = "{}--{}".format(parent, item_code) if parent else "Item-{}".format(item_code)
+ item_tax_template.title = make_autoname("Item Tax Template-.####")
+
for tax_type, tax_rate in iteritems(item_tax_map):
if not frappe.db.exists("Account", tax_type):
parts = tax_type.strip().split(" - ")
diff --git a/erpnext/patches/v12_0/remove_denied_leaves_from_leave_ledger.py b/erpnext/patches/v12_0/remove_denied_leaves_from_leave_ledger.py
new file mode 100644
index 0000000..7859606
--- /dev/null
+++ b/erpnext/patches/v12_0/remove_denied_leaves_from_leave_ledger.py
@@ -0,0 +1,28 @@
+# Copyright (c) 2018, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.utils import getdate, today
+
+def execute():
+ ''' Delete leave ledger entry created
+ via leave applications with status != Approved '''
+ if not frappe.db.a_row_exists("Leave Ledger Entry"):
+ return
+
+ leave_application_list = get_denied_leave_application_list()
+ if leave_application_list:
+ delete_denied_leaves_from_leave_ledger_entry(leave_application_list)
+
+def get_denied_leave_application_list():
+ return frappe.db.sql_list(''' Select name from `tabLeave Application` where status <> 'Approved' ''')
+
+def delete_denied_leaves_from_leave_ledger_entry(leave_application_list):
+ if leave_application_list:
+ frappe.db.sql(''' Delete
+ FROM `tabLeave Ledger Entry`
+ WHERE
+ transaction_type = 'Leave Application'
+ AND transaction_name in (%s) ''' % (', '.join(['%s'] * len(leave_application_list))), #nosec
+ tuple(leave_application_list))
\ No newline at end of file
diff --git a/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py b/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py
index 3d07fe5..5842e9e 100644
--- a/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py
+++ b/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py
@@ -9,13 +9,12 @@
if frappe.db.exists("DocType","Asset Settings"):
frappe.reload_doctype("Company")
- cwip_value = frappe.db.sql(""" SELECT value FROM `tabSingles` WHERE doctype='Asset Settings'
- and field='disable_cwip_accounting' """, as_dict=1)
+ cwip_value = frappe.db.get_single_value("Asset Settings","disable_cwip_accounting")
companies = [x['name'] for x in frappe.get_all("Company", "name")]
for company in companies:
- enable_cwip_accounting = cint(not cint(cwip_value[0]['value']))
- frappe.set_value("Company", company, "enable_cwip_accounting", enable_cwip_accounting)
+ enable_cwip_accounting = cint(not cint(cwip_value))
+ frappe.db.set_value("Company", company, "enable_cwip_accounting", enable_cwip_accounting)
frappe.db.sql(
""" DELETE FROM `tabSingles` where doctype = 'Asset Settings' """)
diff --git a/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py b/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py
index 44d8fa7..0702673 100644
--- a/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py
+++ b/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py
@@ -3,8 +3,12 @@
from erpnext.selling.doctype.sales_order.sales_order import update_produced_qty_in_so_item
def execute():
- frappe.reload_doctype('Sales Order Item')
- frappe.reload_doctype('Sales Order')
- sales_order_items = frappe.db.get_all('Sales Order Item', ['name'])
- for so_item in sales_order_items:
- update_produced_qty_in_so_item(so_item.get('name'))
\ No newline at end of file
+ frappe.reload_doctype('Sales Order Item')
+ frappe.reload_doctype('Sales Order')
+
+ for d in frappe.get_all('Work Order',
+ fields = ['sales_order', 'sales_order_item'],
+ filters={'sales_order': ('!=', ''), 'sales_order_item': ('!=', '')}):
+
+ # update produced qty in sales order
+ update_produced_qty_in_so_item(d.sales_order, d.sales_order_item)
\ No newline at end of file
diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py
index 90e9f05..54fce8d 100755
--- a/erpnext/projects/doctype/task/task.py
+++ b/erpnext/projects/doctype/task/task.py
@@ -10,6 +10,7 @@
from frappe.utils import add_days, cstr, date_diff, get_link_to_form, getdate
from frappe.utils.nestedset import NestedSet
from frappe.desk.form.assign_to import close_all_assignments, clear
+from frappe.utils import date_diff
class CircularReferenceError(frappe.ValidationError): pass
class EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError): pass
@@ -28,16 +29,29 @@
def validate(self):
self.validate_dates()
+ self.validate_parent_project_dates()
self.validate_progress()
self.validate_status()
self.update_depends_on()
def validate_dates(self):
if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date):
- frappe.throw(_("'Expected Start Date' can not be greater than 'Expected End Date'"))
+ frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \
+ frappe.bold("Expected End Date")))
if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date):
- frappe.throw(_("'Actual Start Date' can not be greater than 'Actual End Date'"))
+ frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \
+ frappe.bold("Actual End Date")))
+
+ def validate_parent_project_dates(self):
+ if not self.project or frappe.flags.in_test:
+ return
+
+ expected_end_date = getdate(frappe.db.get_value("Project", self.project, "expected_end_date"))
+
+ if expected_end_date:
+ validate_project_dates(expected_end_date, self, "exp_start_date", "exp_end_date", "Expected")
+ validate_project_dates(expected_end_date, self, "act_start_date", "act_end_date", "Actual")
def validate_status(self):
if self.status!=self.get_db_value("status") and self.status == "Completed":
@@ -255,3 +269,10 @@
def on_doctype_update():
frappe.db.add_index("Task", ["lft", "rgt"])
+
+def validate_project_dates(project_end_date, task, task_start, task_end, actual_or_expected_date):
+ if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0:
+ frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date))
+
+ if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0:
+ frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date))
\ No newline at end of file
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index c4c3c0f..e12b359 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -1038,14 +1038,18 @@
return doc
-def update_produced_qty_in_so_item(sales_order_item):
+def update_produced_qty_in_so_item(sales_order, sales_order_item):
#for multiple work orders against same sales order item
linked_wo_with_so_item = frappe.db.get_all('Work Order', ['produced_qty'], {
'sales_order_item': sales_order_item,
+ 'sales_order': sales_order,
'docstatus': 1
})
- if len(linked_wo_with_so_item) > 0:
- total_produced_qty = 0
- for wo in linked_wo_with_so_item:
- total_produced_qty += flt(wo.get('produced_qty'))
- frappe.db.set_value('Sales Order Item', sales_order_item, 'produced_qty', total_produced_qty)
\ No newline at end of file
+
+ total_produced_qty = 0
+ for wo in linked_wo_with_so_item:
+ total_produced_qty += flt(wo.get('produced_qty'))
+
+ if not total_produced_qty and frappe.flags.in_patch: return
+
+ frappe.db.set_value('Sales Order Item', sales_order_item, 'produced_qty', total_produced_qty)
\ No newline at end of file
diff --git a/erpnext/setup/doctype/currency_exchange/currency_exchange.py b/erpnext/setup/doctype/currency_exchange/currency_exchange.py
index 60d367a..6480f60 100644
--- a/erpnext/setup/doctype/currency_exchange/currency_exchange.py
+++ b/erpnext/setup/doctype/currency_exchange/currency_exchange.py
@@ -14,10 +14,14 @@
purpose = ""
if not self.date:
self.date = nowdate()
+
+ # If both selling and buying enabled
+ purpose = "Selling-Buying"
if cint(self.for_buying)==0 and cint(self.for_selling)==1:
purpose = "Selling"
if cint(self.for_buying)==1 and cint(self.for_selling)==0:
purpose = "Buying"
+
self.name = '{0}-{1}-{2}{3}'.format(formatdate(get_datetime_str(self.date), "yyyy-MM-dd"),
self.from_currency, self.to_currency, ("-" + purpose) if purpose else "")
diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
index 857f666..c5c01c5 100644
--- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
+++ b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
@@ -11,7 +11,9 @@
def save_new_records(test_records):
for record in test_records:
- purpose = str("")
+ # If both selling and buying enabled
+ purpose = "Selling-Buying"
+
if cint(record.get("for_buying"))==0 and cint(record.get("for_selling"))==1:
purpose = "Selling"
if cint(record.get("for_buying"))==1 and cint(record.get("for_selling"))==0:
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 98a8c59..ca2741c 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -52,9 +52,10 @@
def _changed(item):
item_dict = get_stock_balance_for(item.item_code, item.warehouse,
self.posting_date, self.posting_time, batch_no=item.batch_no)
- if (((item.qty is None or item.qty==item_dict.get("qty")) and
- (item.valuation_rate is None or item.valuation_rate==item_dict.get("rate")) and not item.serial_no)
- or (item.serial_no and item.serial_no == item_dict.get("serial_nos"))):
+
+ if ((item.qty is None or item.qty==item_dict.get("qty")) and
+ (item.valuation_rate is None or item.valuation_rate==item_dict.get("rate")) and
+ (not item.serial_no or (item.serial_no == item_dict.get("serial_nos")) )):
return False
else:
# set default as current rates
@@ -182,9 +183,11 @@
from erpnext.stock.stock_ledger import get_previous_sle
sl_entries = []
+ has_serial_no = False
for row in self.items:
item = frappe.get_doc("Item", row.item_code)
if item.has_serial_no or item.has_batch_no:
+ has_serial_no = True
self.get_sle_for_serialized_items(row, sl_entries)
else:
previous_sle = get_previous_sle({
@@ -212,8 +215,14 @@
sl_entries.append(self.get_sle_for_items(row))
if sl_entries:
+ if has_serial_no:
+ sl_entries = self.merge_similar_item_serial_nos(sl_entries)
+
self.make_sl_entries(sl_entries)
+ if has_serial_no and sl_entries:
+ self.update_valuation_rate_for_serial_no()
+
def get_sle_for_serialized_items(self, row, sl_entries):
from erpnext.stock.stock_ledger import get_previous_sle
@@ -275,8 +284,18 @@
# update valuation rate
self.update_valuation_rate_for_serial_nos(row, serial_nos)
+ def update_valuation_rate_for_serial_no(self):
+ for d in self.items:
+ if not d.serial_no: continue
+
+ serial_nos = get_serial_nos(d.serial_no)
+ self.update_valuation_rate_for_serial_nos(d, serial_nos)
+
def update_valuation_rate_for_serial_nos(self, row, serial_nos):
valuation_rate = row.valuation_rate if self.docstatus == 1 else row.current_valuation_rate
+ if valuation_rate is None:
+ return
+
for d in serial_nos:
frappe.db.set_value("Serial No", d, 'purchase_rate', valuation_rate)
@@ -321,11 +340,17 @@
where voucher_type=%s and voucher_no=%s""", (self.doctype, self.name))
sl_entries = []
+
+ has_serial_no = False
for row in self.items:
if row.serial_no or row.batch_no or row.current_serial_no:
+ has_serial_no = True
self.get_sle_for_serialized_items(row, sl_entries)
if sl_entries:
+ if has_serial_no:
+ sl_entries = self.merge_similar_item_serial_nos(sl_entries)
+
sl_entries.reverse()
allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock")
self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock)
@@ -339,6 +364,35 @@
"posting_time": self.posting_time
})
+ def merge_similar_item_serial_nos(self, sl_entries):
+ # If user has put the same item in multiple row with different serial no
+ new_sl_entries = []
+ merge_similar_entries = {}
+
+ for d in sl_entries:
+ if not d.serial_no or d.actual_qty < 0:
+ new_sl_entries.append(d)
+ continue
+
+ key = (d.item_code, d.warehouse)
+ if key not in merge_similar_entries:
+ merge_similar_entries[key] = d
+ elif d.serial_no:
+ data = merge_similar_entries[key]
+ data.actual_qty += d.actual_qty
+ data.qty_after_transaction += d.qty_after_transaction
+
+ data.valuation_rate = (data.valuation_rate + d.valuation_rate) / data.actual_qty
+ data.serial_no += '\n' + d.serial_no
+
+ if data.incoming_rate:
+ data.incoming_rate = (data.incoming_rate + d.incoming_rate) / data.actual_qty
+
+ for key, value in merge_similar_entries.items():
+ new_sl_entries.append(value)
+
+ return new_sl_entries
+
def get_gl_entries(self, warehouse_account=None):
if not self.cost_center:
msgprint(_("Please enter Cost Center"), raise_exception=1)
@@ -456,7 +510,7 @@
}
serial_nos_list = [serial_no.get("name")
- for serial_no in get_available_serial_nos(item_code, warehouse)]
+ for serial_no in get_available_serial_nos(args)]
qty = len(serial_nos_list)
serial_nos = '\n'.join(serial_nos_list)
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index d762917..2c6c953 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -293,9 +293,11 @@
row, key, value = data
row[key] = value
-def get_available_serial_nos(item_code, warehouse):
- return frappe.get_all("Serial No", filters = {'item_code': item_code,
- 'warehouse': warehouse, 'delivery_document_no': ''}) or []
+def get_available_serial_nos(args):
+ return frappe.db.sql(""" SELECT name from `tabSerial No`
+ WHERE item_code = %(item_code)s and warehouse = %(warehouse)s
+ and timestamp(purchase_date, purchase_time) <= timestamp(%(posting_date)s, %(posting_time)s)
+ """, args, as_dict=1)
def add_additional_uom_columns(columns, result, include_uom, conversion_factors):
if not include_uom or not conversion_factors: