Merge branch 'v12-pre-release' into version-12

This commit is contained in:
Nabin Hait
2019-11-25 15:11:24 +05:30
244 changed files with 6247 additions and 7184 deletions

View File

@@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides from erpnext.hooks import regional_overrides
from frappe.utils import getdate from frappe.utils import getdate
__version__ = '12.1.8' __version__ = '12.2.0'
def get_default_company(user=None): def get_default_company(user=None):
'''Get default company for user''' '''Get default company for user'''

View File

@@ -3,6 +3,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, json import frappe, json
from frappe import _
from frappe.utils import add_to_date, date_diff, getdate, nowdate, get_last_day, formatdate from frappe.utils import add_to_date, date_diff, getdate, nowdate, get_last_day, formatdate
from erpnext.accounts.report.general_ledger.general_ledger import execute from erpnext.accounts.report.general_ledger.general_ledger import execute
from frappe.core.page.dashboard.dashboard import cache_source, get_from_date_from_timespan from frappe.core.page.dashboard.dashboard import cache_source, get_from_date_from_timespan
@@ -18,12 +19,20 @@ def get(chart_name = None, chart = None, no_cache = None, from_date = None, to_d
else: else:
chart = frappe._dict(frappe.parse_json(chart)) chart = frappe._dict(frappe.parse_json(chart))
timespan = chart.timespan timespan = chart.timespan
if chart.timespan == 'Select Date Range':
from_date = chart.from_date
to_date = chart.to_date
timegrain = chart.time_interval timegrain = chart.time_interval
filters = frappe.parse_json(chart.filters_json) filters = frappe.parse_json(chart.filters_json)
account = filters.get("account") account = filters.get("account")
company = filters.get("company") company = filters.get("company")
if not account and chart:
frappe.throw(_("Account is not set for the dashboard chart {0}").format(chart))
if not to_date: if not to_date:
to_date = nowdate() to_date = nowdate()
if not from_date: if not from_date:
@@ -84,7 +93,8 @@ def get_gl_entries(account, to_date):
fields = ['posting_date', 'debit', 'credit'], fields = ['posting_date', 'debit', 'credit'],
filters = [ filters = [
dict(posting_date = ('<', to_date)), 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') order_by = 'posting_date asc')

View File

@@ -174,6 +174,8 @@ def make_gl_entries(doc, credit_account, debit_account, against,
# GL Entry for crediting the amount in the deferred expense # GL Entry for crediting the amount in the deferred expense
from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.general_ledger import make_gl_entries
if amount == 0: return
gl_entries = [] gl_entries = []
gl_entries.append( gl_entries.append(
doc.get_gl_dict({ doc.get_gl_dict({

View File

@@ -117,7 +117,7 @@ class Account(NestedSet):
if not parent_acc_name_map: return if not parent_acc_name_map: return
self.create_account_for_child_company(parent_acc_name_map, descendants) self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name)
def validate_group_or_ledger(self): def validate_group_or_ledger(self):
if self.get("__islocal"): if self.get("__islocal"):
@@ -159,7 +159,7 @@ class Account(NestedSet):
if frappe.db.get_value("GL Entry", {"account": self.name}): if frappe.db.get_value("GL Entry", {"account": self.name}):
frappe.throw(_("Currency can not be changed after making entries using some other currency")) frappe.throw(_("Currency can not be changed after making entries using some other currency"))
def create_account_for_child_company(self, parent_acc_name_map, descendants): def create_account_for_child_company(self, parent_acc_name_map, descendants, parent_acc_name):
for company in descendants: for company in descendants:
if not parent_acc_name_map.get(company): if not parent_acc_name_map.get(company):
frappe.throw(_("While creating account for child Company {0}, parent account {1} not found. Please create the parent account in corresponding COA") frappe.throw(_("While creating account for child Company {0}, parent account {1} not found. Please create the parent account in corresponding COA")

View File

@@ -160,7 +160,7 @@ def _make_test_records(verbose):
["_Test Payable USD", "Current Liabilities", 0, "Payable", "USD"] ["_Test Payable USD", "Current Liabilities", 0, "Payable", "USD"]
] ]
for company, abbr in [["_Test Company", "_TC"], ["_Test Company 1", "_TC1"]]: for company, abbr in [["_Test Company", "_TC"], ["_Test Company 1", "_TC1"], ["_Test Company with perpetual inventory", "TCP1"]]:
test_objects = make_test_objects("Account", [{ test_objects = make_test_objects("Account", [{
"doctype": "Account", "doctype": "Account",
"account_name": account_name, "account_name": account_name,

View File

@@ -24,6 +24,11 @@ class AccountingDimension(Document):
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type) msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
frappe.throw(msg) frappe.throw(msg)
exists = frappe.db.get_value("Accounting Dimension", {'document_type': self.document_type}, ['name'])
if exists and self.is_new():
frappe.throw("Document Type already used as a dimension")
def after_insert(self): def after_insert(self):
if frappe.flags.in_test: if frappe.flags.in_test:
make_dimension_in_accounting_doctypes(doc=self) make_dimension_in_accounting_doctypes(doc=self)
@@ -60,7 +65,8 @@ def make_dimension_in_accounting_doctypes(doc):
"label": doc.label, "label": doc.label,
"fieldtype": "Link", "fieldtype": "Link",
"options": doc.document_type, "options": doc.document_type,
"insert_after": insert_after_field "insert_after": insert_after_field,
"owner": "Administrator"
} }
if doctype == "Budget": if doctype == "Budget":

View File

@@ -15,8 +15,8 @@ class AccountsSettings(Document):
frappe.clear_cache() frappe.clear_cache()
def validate(self): def validate(self):
for f in ["add_taxes_from_item_tax_template"]: frappe.db.set_default("add_taxes_from_item_tax_template",
frappe.db.set_default(f, self.get(f, "")) self.get("add_taxes_from_item_tax_template", 0))
self.validate_stale_days() self.validate_stale_days()
self.enable_payment_schedule_in_print() self.enable_payment_schedule_in_print()

View File

@@ -2,6 +2,15 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Coupon Code', { frappe.ui.form.on('Coupon Code', {
setup: function(frm) {
frm.set_query("pricing_rule", function() {
return {
filters: [
["Pricing Rule","coupon_code_based", "=", "1"]
]
};
});
},
coupon_name:function(frm){ coupon_name:function(frm){
if (frm.doc.__islocal===1) { if (frm.doc.__islocal===1) {
frm.trigger("make_coupon_code"); frm.trigger("make_coupon_code");

View File

@@ -24,6 +24,7 @@
], ],
"fields": [ "fields": [
{ {
"description": "e.g. \"Summer Holiday 2019 Offer 20\"",
"fieldname": "coupon_name", "fieldname": "coupon_name",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Coupon Name", "label": "Coupon Name",
@@ -50,7 +51,7 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"description": "To be used to get discount", "description": "unique e.g. SAVE20 To be used to get discount",
"fieldname": "coupon_code", "fieldname": "coupon_code",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Coupon Code", "label": "Coupon Code",
@@ -62,12 +63,13 @@
"fieldname": "pricing_rule", "fieldname": "pricing_rule",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Pricing Rule", "label": "Pricing Rule",
"options": "Pricing Rule" "options": "Pricing Rule",
"reqd": 1
}, },
{ {
"fieldname": "uses", "fieldname": "uses",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Uses" "label": "Validity and Usage"
}, },
{ {
"fieldname": "valid_from", "fieldname": "valid_from",
@@ -113,7 +115,7 @@
"read_only": 1 "read_only": 1
} }
], ],
"modified": "2019-10-15 14:12:22.686986", "modified": "2019-10-19 14:48:14.602481",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Coupon Code", "name": "Coupon Code",

View File

@@ -570,7 +570,7 @@ $.extend(erpnext.journal_entry, {
}, },
{fieldtype: "Date", fieldname: "posting_date", label: __("Date"), reqd: 1, {fieldtype: "Date", fieldname: "posting_date", label: __("Date"), reqd: 1,
default: frm.doc.posting_date}, default: frm.doc.posting_date},
{fieldtype: "Small Text", fieldname: "user_remark", label: __("User Remark"), reqd: 1}, {fieldtype: "Small Text", fieldname: "user_remark", label: __("User Remark")},
{fieldtype: "Select", fieldname: "naming_series", label: __("Series"), reqd: 1, {fieldtype: "Select", fieldname: "naming_series", label: __("Series"), reqd: 1,
options: naming_series_options, default: naming_series_default}, options: naming_series_options, default: naming_series_default},
] ]

View File

@@ -8,10 +8,12 @@ import unittest
from frappe.utils import today, cint, flt, getdate from frappe.utils import today, cint, flt, getdate
from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points
from erpnext.accounts.party import get_dashboard_info from erpnext.accounts.party import get_dashboard_info
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
class TestLoyaltyProgram(unittest.TestCase): class TestLoyaltyProgram(unittest.TestCase):
@classmethod @classmethod
def setUpClass(self): def setUpClass(self):
set_perpetual_inventory(0)
# create relevant item, customer, loyalty program, etc # create relevant item, customer, loyalty program, etc
create_records() create_records()

View File

@@ -32,8 +32,10 @@ class OpeningInvoiceCreationTool(Document):
}) })
invoices_summary.update({company: _summary}) invoices_summary.update({company: _summary})
paid_amount.append(invoice.paid_amount) if invoice.paid_amount:
outstanding_amount.append(invoice.outstanding_amount) paid_amount.append(invoice.paid_amount)
if invoice.outstanding_amount:
outstanding_amount.append(invoice.outstanding_amount)
if paid_amount or outstanding_amount: if paid_amount or outstanding_amount:
max_count.update({ max_count.update({

View File

@@ -554,7 +554,7 @@ frappe.ui.form.on('Payment Entry', {
frappe.flags.allocate_payment_amount = true; frappe.flags.allocate_payment_amount = true;
frm.events.validate_filters_data(frm, filters); frm.events.validate_filters_data(frm, filters);
frm.events.get_outstanding_documents(frm, filters); frm.events.get_outstanding_documents(frm, filters);
}, __("Filters"), __("Get Outstanding Invoices")); }, __("Filters"), __("Get Outstanding Documents"));
}, },
validate_filters_data: function(frm, filters) { validate_filters_data: function(frm, filters) {

View File

@@ -62,6 +62,7 @@
"dimension_col_break", "dimension_col_break",
"cost_center", "cost_center",
"section_break_12", "section_break_12",
"status",
"remarks", "remarks",
"column_break_16", "column_break_16",
"letter_head", "letter_head",
@@ -563,10 +564,18 @@
{ {
"fieldname": "dimension_col_break", "fieldname": "dimension_col_break",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"default": "Draft",
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "\nDraft\nSubmitted\nCancelled",
"read_only": 1
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"modified": "2019-05-27 15:53:21.108857", "modified": "2019-11-06 12:59:43.151721",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry", "name": "Payment Entry",

View File

@@ -61,6 +61,7 @@ class PaymentEntry(AccountsController):
self.validate_duplicate_entry() self.validate_duplicate_entry()
self.validate_allocated_amount() self.validate_allocated_amount()
self.ensure_supplier_is_not_blocked() self.ensure_supplier_is_not_blocked()
self.set_status()
def on_submit(self): def on_submit(self):
self.setup_party_account_field() self.setup_party_account_field()
@@ -70,6 +71,7 @@ class PaymentEntry(AccountsController):
self.update_outstanding_amounts() self.update_outstanding_amounts()
self.update_advance_paid() self.update_advance_paid()
self.update_expense_claim() self.update_expense_claim()
self.set_status()
def on_cancel(self): def on_cancel(self):
@@ -79,6 +81,7 @@ class PaymentEntry(AccountsController):
self.update_advance_paid() self.update_advance_paid()
self.update_expense_claim() self.update_expense_claim()
self.delink_advance_entry_references() self.delink_advance_entry_references()
self.set_status()
def update_outstanding_amounts(self): def update_outstanding_amounts(self):
self.set_missing_ref_details(force=True) self.set_missing_ref_details(force=True)
@@ -275,6 +278,14 @@ class PaymentEntry(AccountsController):
frappe.throw(_("Against Journal Entry {0} does not have any unmatched {1} entry") frappe.throw(_("Against Journal Entry {0} does not have any unmatched {1} entry")
.format(d.reference_name, dr_or_cr)) .format(d.reference_name, dr_or_cr))
def set_status(self):
if self.docstatus == 2:
self.status = 'Cancelled'
elif self.docstatus == 1:
self.status = 'Submitted'
else:
self.status = 'Draft'
def set_amounts(self): def set_amounts(self):
self.set_amounts_in_company_currency() self.set_amounts_in_company_currency()
self.set_total_allocated_amount() self.set_total_allocated_amount()

View File

@@ -90,7 +90,8 @@ class PaymentReconciliation(Document):
FROM `tab{doc}`, `tabGL Entry` FROM `tab{doc}`, `tabGL Entry`
WHERE WHERE
(`tab{doc}`.name = `tabGL Entry`.against_voucher or `tab{doc}`.name = `tabGL Entry`.voucher_no) (`tab{doc}`.name = `tabGL Entry`.against_voucher or `tab{doc}`.name = `tabGL Entry`.voucher_no)
and `tab{doc}`.is_return = 1 and `tabGL Entry`.against_voucher_type = %(voucher_type)s and `tab{doc}`.is_return = 1 and `tab{doc}`.return_against IS NULL
and `tabGL Entry`.against_voucher_type = %(voucher_type)s
and `tab{doc}`.docstatus = 1 and `tabGL Entry`.party = %(party)s and `tab{doc}`.docstatus = 1 and `tabGL Entry`.party = %(party)s
and `tabGL Entry`.party_type = %(party_type)s and `tabGL Entry`.account = %(account)s and `tabGL Entry`.party_type = %(party_type)s and `tabGL Entry`.account = %(account)s
GROUP BY `tab{doc}`.name GROUP BY `tab{doc}`.name

View File

@@ -331,15 +331,15 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
}) })
}, },
asset: function(frm, cdt, cdn) { item_code: function(frm, cdt, cdn) {
var row = locals[cdt][cdn]; var row = locals[cdt][cdn];
if(row.asset) { if(row.item_code) {
frappe.call({ frappe.call({
method: "erpnext.assets.doctype.asset_category.asset_category.get_asset_category_account", method: "erpnext.assets.doctype.asset_category.asset_category.get_asset_category_account",
args: { args: {
"asset": row.asset, "item": row.item_code,
"fieldname": "fixed_asset_account", "fieldname": "fixed_asset_account",
"account": row.expense_account "company": frm.doc.company
}, },
callback: function(r, rt) { callback: function(r, rt) {
frappe.model.set_value(cdt, cdn, "expense_account", r.message); frappe.model.set_value(cdt, cdn, "expense_account", r.message);
@@ -430,19 +430,7 @@ cur_frm.fields_dict['select_print_heading'].get_query = function(doc, cdt, cdn)
cur_frm.set_query("expense_account", "items", function(doc) { cur_frm.set_query("expense_account", "items", function(doc) {
return { return {
query: "erpnext.controllers.queries.get_expense_account", query: "erpnext.controllers.queries.get_expense_account",
filters: {'company': doc.company} filters: {'company': doc.company }
}
});
cur_frm.set_query("asset", "items", function(doc, cdt, cdn) {
var d = locals[cdt][cdn];
return {
filters: {
'item_code': d.item_code,
'docstatus': 1,
'company': doc.company,
'status': 'Submitted'
}
} }
}); });

View File

@@ -18,13 +18,14 @@ from erpnext.accounts.general_ledger import make_gl_entries, merge_similar_entri
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_disabled from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from six import iteritems from six import iteritems
from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_party, update_linked_doc,\ from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_party, update_linked_doc,\
unlink_inter_company_doc unlink_inter_company_doc
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
from erpnext.accounts.deferred_revenue import validate_service_stop_date from erpnext.accounts.deferred_revenue import validate_service_stop_date
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import get_item_account_wise_additional_cost
form_grid_templates = { form_grid_templates = {
"items": "templates/form_grid/item_grid.html" "items": "templates/form_grid/item_grid.html"
@@ -97,7 +98,6 @@ class PurchaseInvoice(BuyingController):
self.set_against_expense_account() self.set_against_expense_account()
self.validate_write_off_account() self.validate_write_off_account()
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items") self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items")
self.validate_fixed_asset()
self.create_remarks() self.create_remarks()
self.set_status() self.set_status()
self.validate_purchase_receipt_if_update_stock() self.validate_purchase_receipt_if_update_stock()
@@ -225,6 +225,8 @@ class PurchaseInvoice(BuyingController):
# in case of auto inventory accounting, # in case of auto inventory accounting,
# expense account is always "Stock Received But Not Billed" for a stock item # expense account is always "Stock Received But Not Billed" for a stock item
# except epening entry, drop-ship entry and fixed asset items # except epening entry, drop-ship entry and fixed asset items
if item.item_code:
asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category")
if auto_accounting_for_stock and item.item_code in stock_items \ if auto_accounting_for_stock and item.item_code in stock_items \
and self.is_opening == 'No' and not item.is_fixed_asset \ and self.is_opening == 'No' and not item.is_fixed_asset \
@@ -235,12 +237,8 @@ class PurchaseInvoice(BuyingController):
item.expense_account = warehouse_account[item.warehouse]["account"] item.expense_account = warehouse_account[item.warehouse]["account"]
else: else:
item.expense_account = stock_not_billed_account item.expense_account = stock_not_billed_account
elif item.is_fixed_asset and is_cwip_accounting_disabled(): elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category):
if not item.asset: item.expense_account = get_asset_category_account('fixed_asset_account', item=item.item_code,
frappe.throw(_("Row {0}: asset is required for item {1}")
.format(item.idx, item.item_code))
item.expense_account = get_asset_category_account(item.asset, 'fixed_asset_account',
company = self.company) company = self.company)
elif item.is_fixed_asset and item.pr_detail: elif item.is_fixed_asset and item.pr_detail:
item.expense_account = asset_received_but_not_billed item.expense_account = asset_received_but_not_billed
@@ -391,7 +389,8 @@ class PurchaseInvoice(BuyingController):
self.make_supplier_gl_entry(gl_entries) self.make_supplier_gl_entry(gl_entries)
self.make_item_gl_entries(gl_entries) self.make_item_gl_entries(gl_entries)
if not is_cwip_accounting_disabled():
if self.check_asset_cwip_enabled():
self.get_asset_gl_entry(gl_entries) self.get_asset_gl_entry(gl_entries)
self.make_tax_gl_entries(gl_entries) self.make_tax_gl_entries(gl_entries)
@@ -404,6 +403,15 @@ class PurchaseInvoice(BuyingController):
return gl_entries return gl_entries
def check_asset_cwip_enabled(self):
# Check if there exists any item with cwip accounting enabled in it's asset category
for item in self.get("items"):
if item.item_code and item.is_fixed_asset:
asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category")
if is_cwip_accounting_enabled(asset_category):
return 1
return 0
def make_supplier_gl_entry(self, gl_entries): def make_supplier_gl_entry(self, gl_entries):
# Checked both rounding_adjustment and rounded_total # Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introcution of posting GLE based on rounded total # because rounded_total had value even before introcution of posting GLE based on rounded total
@@ -436,15 +444,23 @@ class PurchaseInvoice(BuyingController):
if self.update_stock and self.auto_accounting_for_stock: if self.update_stock and self.auto_accounting_for_stock:
warehouse_account = get_warehouse_account_map(self.company) warehouse_account = get_warehouse_account_map(self.company)
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
voucher_wise_stock_value = {} voucher_wise_stock_value = {}
if self.update_stock: if self.update_stock:
for d in frappe.get_all('Stock Ledger Entry', for d in frappe.get_all('Stock Ledger Entry',
fields = ["voucher_detail_no", "stock_value_difference"], filters={'voucher_no': self.name}): fields = ["voucher_detail_no", "stock_value_difference"], filters={'voucher_no': self.name}):
voucher_wise_stock_value.setdefault(d.voucher_detail_no, d.stock_value_difference) voucher_wise_stock_value.setdefault(d.voucher_detail_no, d.stock_value_difference)
valuation_tax_accounts = [d.account_head for d in self.get("taxes")
if d.category in ('Valuation', 'Total and Valuation')
and flt(d.base_tax_amount_after_discount_amount)]
for item in self.get("items"): for item in self.get("items"):
if flt(item.base_net_amount): if flt(item.base_net_amount):
account_currency = get_account_currency(item.expense_account) account_currency = get_account_currency(item.expense_account)
if item.item_code:
asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category")
if self.update_stock and self.auto_accounting_for_stock and item.item_code in stock_items: if self.update_stock and self.auto_accounting_for_stock and item.item_code in stock_items:
# warehouse account # warehouse account
@@ -463,15 +479,16 @@ class PurchaseInvoice(BuyingController):
) )
# Amount added through landed-cost-voucher # Amount added through landed-cost-voucher
if flt(item.landed_cost_voucher_amount): if landed_cost_entries:
gl_entries.append(self.get_gl_dict({ for account, amount in iteritems(landed_cost_entries[(item.item_code, item.name)]):
"account": expenses_included_in_valuation, gl_entries.append(self.get_gl_dict({
"against": item.expense_account, "account": account,
"cost_center": item.cost_center, "against": item.expense_account,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"), "cost_center": item.cost_center,
"credit": flt(item.landed_cost_voucher_amount), "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"project": item.project "credit": flt(amount),
}, item=item)) "project": item.project
}, item=item))
# sub-contracting warehouse # sub-contracting warehouse
if flt(item.rm_supp_cost): if flt(item.rm_supp_cost):
@@ -486,31 +503,61 @@ class PurchaseInvoice(BuyingController):
"remarks": self.get("remarks") or _("Accounting Entry for Stock"), "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(item.rm_supp_cost) "credit": flt(item.rm_supp_cost)
}, warehouse_account[self.supplier_warehouse]["account_currency"], item=item)) }, warehouse_account[self.supplier_warehouse]["account_currency"], item=item))
elif not item.is_fixed_asset or (item.is_fixed_asset and is_cwip_accounting_disabled()):
elif not item.is_fixed_asset or (item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category)):
expense_account = (item.expense_account expense_account = (item.expense_account
if (not item.enable_deferred_expense or self.is_return) else item.deferred_expense_account) if (not item.enable_deferred_expense or self.is_return) else item.deferred_expense_account)
gl_entries.append( if not item.is_fixed_asset:
self.get_gl_dict({ amount = flt(item.base_net_amount, item.precision("base_net_amount"))
else:
amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount"))
gl_entries.append(self.get_gl_dict({
"account": expense_account, "account": expense_account,
"against": self.supplier, "against": self.supplier,
"debit": flt(item.base_net_amount, item.precision("base_net_amount")), "debit": amount,
"debit_in_account_currency": (flt(item.base_net_amount,
item.precision("base_net_amount")) if account_currency==self.company_currency
else flt(item.net_amount, item.precision("net_amount"))),
"cost_center": item.cost_center, "cost_center": item.cost_center,
"project": item.project "project": item.project
}, account_currency, item=item) }, account_currency, item=item))
)
# If asset is bought through this document and not linked to PR
if self.update_stock and item.landed_cost_voucher_amount:
expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation")
# Amount added through landed-cost-voucher
gl_entries.append(self.get_gl_dict({
"account": expenses_included_in_asset_valuation,
"against": expense_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(item.landed_cost_voucher_amount),
"project": item.project
}, item=item))
gl_entries.append(self.get_gl_dict({
"account": expense_account,
"against": expenses_included_in_asset_valuation,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": flt(item.landed_cost_voucher_amount),
"project": item.project
}, item=item))
# update gross amount of asset bought through this document
assets = frappe.db.get_all('Asset',
filters={ 'purchase_invoice': self.name, 'item_code': item.item_code }
)
for asset in assets:
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate))
if self.auto_accounting_for_stock and self.is_opening == "No" and \ if self.auto_accounting_for_stock and self.is_opening == "No" and \
item.item_code in stock_items and item.item_tax_amount: item.item_code in stock_items and item.item_tax_amount:
# Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt
if item.purchase_receipt: if item.purchase_receipt and valuation_tax_accounts:
negative_expense_booked_in_pr = frappe.db.sql("""select name from `tabGL Entry` negative_expense_booked_in_pr = frappe.db.sql("""select name from `tabGL Entry`
where voucher_type='Purchase Receipt' and voucher_no=%s and account=%s""", where voucher_type='Purchase Receipt' and voucher_no=%s and account in %s""",
(item.purchase_receipt, self.expenses_included_in_valuation)) (item.purchase_receipt, valuation_tax_accounts))
if not negative_expense_booked_in_pr: if not negative_expense_booked_in_pr:
gl_entries.append( gl_entries.append(
@@ -527,27 +574,27 @@ class PurchaseInvoice(BuyingController):
item.precision("item_tax_amount")) item.precision("item_tax_amount"))
def get_asset_gl_entry(self, gl_entries): def get_asset_gl_entry(self, gl_entries):
arbnb_account = self.get_company_default("asset_received_but_not_billed")
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
for item in self.get("items"): for item in self.get("items"):
if item.is_fixed_asset: if item.is_fixed_asset:
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
asset_amount = flt(item.net_amount) + flt(item.item_tax_amount/self.conversion_rate) asset_amount = flt(item.net_amount) + flt(item.item_tax_amount/self.conversion_rate)
base_asset_amount = flt(item.base_net_amount + item.item_tax_amount) base_asset_amount = flt(item.base_net_amount + item.item_tax_amount)
if (not item.expense_account or frappe.db.get_value('Account', item_exp_acc_type = frappe.db.get_value('Account', item.expense_account, 'account_type')
item.expense_account, 'account_type') not in ['Asset Received But Not Billed', 'Fixed Asset']): if (not item.expense_account or item_exp_acc_type not in ['Asset Received But Not Billed', 'Fixed Asset']):
arbnb_account = self.get_company_default("asset_received_but_not_billed")
item.expense_account = arbnb_account item.expense_account = arbnb_account
if not self.update_stock: if not self.update_stock:
asset_rbnb_currency = get_account_currency(item.expense_account) arbnb_currency = get_account_currency(item.expense_account)
gl_entries.append(self.get_gl_dict({ gl_entries.append(self.get_gl_dict({
"account": item.expense_account, "account": item.expense_account,
"against": self.supplier, "against": self.supplier,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"), "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"debit": base_asset_amount, "debit": base_asset_amount,
"debit_in_account_currency": (base_asset_amount "debit_in_account_currency": (base_asset_amount
if asset_rbnb_currency == self.company_currency else asset_amount), if arbnb_currency == self.company_currency else asset_amount),
"cost_center": item.cost_center "cost_center": item.cost_center
}, item=item)) }, item=item))
@@ -564,8 +611,7 @@ class PurchaseInvoice(BuyingController):
item.item_tax_amount / self.conversion_rate) item.item_tax_amount / self.conversion_rate)
}, item=item)) }, item=item))
else: else:
cwip_account = get_asset_account("capital_work_in_progress_account", cwip_account = get_asset_account("capital_work_in_progress_account", company = self.company)
item.asset, company = self.company)
cwip_account_currency = get_account_currency(cwip_account) cwip_account_currency = get_account_currency(cwip_account)
gl_entries.append(self.get_gl_dict({ gl_entries.append(self.get_gl_dict({
@@ -591,6 +637,36 @@ class PurchaseInvoice(BuyingController):
item.item_tax_amount / self.conversion_rate) item.item_tax_amount / self.conversion_rate)
}, item=item)) }, item=item))
# When update stock is checked
# Assets are bought through this document then it will be linked to this document
if self.update_stock:
if flt(item.landed_cost_voucher_amount):
gl_entries.append(self.get_gl_dict({
"account": eiiav_account,
"against": cwip_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(item.landed_cost_voucher_amount),
"project": item.project
}, item=item))
gl_entries.append(self.get_gl_dict({
"account": cwip_account,
"against": eiiav_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": flt(item.landed_cost_voucher_amount),
"project": item.project
}, item=item))
# update gross amount of assets bought through this document
assets = frappe.db.get_all('Asset',
filters={ 'purchase_invoice': self.name, 'item_code': item.item_code }
)
for asset in assets:
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate))
return gl_entries return gl_entries
def make_stock_adjustment_entry(self, gl_entries, item, voucher_wise_stock_value, account_currency): def make_stock_adjustment_entry(self, gl_entries, item, voucher_wise_stock_value, account_currency):
@@ -641,14 +717,14 @@ class PurchaseInvoice(BuyingController):
if account_currency==self.company_currency \ if account_currency==self.company_currency \
else tax.tax_amount_after_discount_amount, else tax.tax_amount_after_discount_amount,
"cost_center": tax.cost_center "cost_center": tax.cost_center
}, account_currency) }, account_currency, item=tax)
) )
# accumulate valuation tax # accumulate valuation tax
if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount): if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount):
if self.auto_accounting_for_stock and not tax.cost_center: if self.auto_accounting_for_stock and not tax.cost_center:
frappe.throw(_("Cost Center is required in row {0} in Taxes table for type {1}").format(tax.idx, _(tax.category))) frappe.throw(_("Cost Center is required in row {0} in Taxes table for type {1}").format(tax.idx, _(tax.category)))
valuation_tax.setdefault(tax.cost_center, 0) valuation_tax.setdefault(tax.name, 0)
valuation_tax[tax.cost_center] += \ valuation_tax[tax.name] += \
(tax.add_deduct_tax == "Add" and 1 or -1) * flt(tax.base_tax_amount_after_discount_amount) (tax.add_deduct_tax == "Add" and 1 or -1) * flt(tax.base_tax_amount_after_discount_amount)
if self.is_opening == "No" and self.negative_expense_to_be_booked and valuation_tax: if self.is_opening == "No" and self.negative_expense_to_be_booked and valuation_tax:
@@ -658,36 +734,38 @@ class PurchaseInvoice(BuyingController):
total_valuation_amount = sum(valuation_tax.values()) total_valuation_amount = sum(valuation_tax.values())
amount_including_divisional_loss = self.negative_expense_to_be_booked amount_including_divisional_loss = self.negative_expense_to_be_booked
i = 1 i = 1
for cost_center, amount in iteritems(valuation_tax): for tax in self.get("taxes"):
if i == len(valuation_tax): if valuation_tax.get(tax.name):
applicable_amount = amount_including_divisional_loss if i == len(valuation_tax):
else: applicable_amount = amount_including_divisional_loss
applicable_amount = self.negative_expense_to_be_booked * (amount / total_valuation_amount) else:
amount_including_divisional_loss -= applicable_amount applicable_amount = self.negative_expense_to_be_booked * (valuation_tax[tax.name] / total_valuation_amount)
amount_including_divisional_loss -= applicable_amount
gl_entries.append( gl_entries.append(
self.get_gl_dict({ self.get_gl_dict({
"account": self.expenses_included_in_valuation, "account": tax.account_head,
"cost_center": cost_center, "cost_center": tax.cost_center,
"against": self.supplier, "against": self.supplier,
"credit": applicable_amount, "credit": applicable_amount,
"remarks": self.remarks or "Accounting Entry for Stock" "remarks": self.remarks or _("Accounting Entry for Stock"),
}) }, item=tax)
) )
i += 1 i += 1
if self.auto_accounting_for_stock and self.update_stock and valuation_tax: if self.auto_accounting_for_stock and self.update_stock and valuation_tax:
for cost_center, amount in iteritems(valuation_tax): for tax in self.get("taxes"):
gl_entries.append( if valuation_tax.get(tax.name):
self.get_gl_dict({ gl_entries.append(
"account": self.expenses_included_in_valuation, self.get_gl_dict({
"cost_center": cost_center, "account": tax.account_head,
"against": self.supplier, "cost_center": tax.cost_center,
"credit": amount, "against": self.supplier,
"remarks": self.remarks or "Accounting Entry for Stock" "credit": valuation_tax[tax.name],
}) "remarks": self.remarks or "Accounting Entry for Stock"
) }, item=tax)
)
def make_payment_gl_entries(self, gl_entries): def make_payment_gl_entries(self, gl_entries):
# Make Cash GL Entries # Make Cash GL Entries

View File

@@ -6,8 +6,8 @@ frappe.listview_settings['Purchase Invoice'] = {
add_fields: ["supplier", "supplier_name", "base_grand_total", "outstanding_amount", "due_date", "company", add_fields: ["supplier", "supplier_name", "base_grand_total", "outstanding_amount", "due_date", "company",
"currency", "is_return", "release_date", "on_hold"], "currency", "is_return", "release_date", "on_hold"],
get_indicator: function(doc) { get_indicator: function(doc) {
if(flt(doc.outstanding_amount) < 0 && doc.docstatus == 1) { if( (flt(doc.outstanding_amount) <= 0) && doc.docstatus == 1 && doc.status == 'Debit Note Issued') {
return [__("Debit Note Issued"), "darkgrey", "outstanding_amount,<,0"] return [__("Debit Note Issued"), "darkgrey", "outstanding_amount,<=,0"];
} else if(flt(doc.outstanding_amount) > 0 && doc.docstatus==1) { } else if(flt(doc.outstanding_amount) > 0 && doc.docstatus==1) {
if(cint(doc.on_hold) && !doc.release_date) { if(cint(doc.on_hold) && !doc.release_date) {
return [__("On Hold"), "darkgrey"]; return [__("On Hold"), "darkgrey"];

View File

@@ -10,7 +10,7 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_ent
from frappe.utils import cint, flt, today, nowdate, add_days from frappe.utils import cint, flt, today, nowdate, add_days
import frappe.defaults import frappe.defaults
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory, \ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory, \
test_records as pr_test_records test_records as pr_test_records, make_purchase_receipt, get_taxes
from erpnext.controllers.accounts_controller import get_payment_terms from erpnext.controllers.accounts_controller import get_payment_terms
from erpnext.exceptions import InvalidCurrency from erpnext.exceptions import InvalidCurrency
from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction
@@ -57,16 +57,11 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual([d.debit, d.credit], expected_gl_entries.get(d.account)) self.assertEqual([d.debit, d.credit], expected_gl_entries.get(d.account))
def test_gl_entries_with_perpetual_inventory(self): def test_gl_entries_with_perpetual_inventory(self):
pi = frappe.copy_doc(test_records[1]) pi = make_purchase_invoice(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1", get_taxes_and_charges=True, qty=10)
set_perpetual_inventory(1, pi.company)
self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pi.company)), 1) self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pi.company)), 1)
pi.insert()
pi.submit()
self.check_gle_for_pi(pi.name) self.check_gle_for_pi(pi.name)
set_perpetual_inventory(0, pi.company)
def test_terms_added_after_save(self): def test_terms_added_after_save(self):
pi = frappe.copy_doc(test_records[1]) pi = frappe.copy_doc(test_records[1])
pi.insert() pi.insert()
@@ -196,32 +191,33 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(pi.on_hold, 0) self.assertEqual(pi.on_hold, 0)
def test_gl_entries_with_perpetual_inventory_against_pr(self): def test_gl_entries_with_perpetual_inventory_against_pr(self):
pr = frappe.copy_doc(pr_test_records[0])
set_perpetual_inventory(1, pr.company)
self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pr.company)), 1)
pr.submit()
pi = frappe.copy_doc(test_records[1]) pr = make_purchase_receipt(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", get_taxes_and_charges=True,)
for d in pi.get("items"):
self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pr.company)), 1)
pi = make_purchase_invoice(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1", get_taxes_and_charges=True, qty=10,do_not_save= "True")
for d in pi.items:
d.purchase_receipt = pr.name d.purchase_receipt = pr.name
pi.insert() pi.insert()
pi.submit() pi.submit()
self.check_gle_for_pi(pi.name) self.check_gle_for_pi(pi.name)
set_perpetual_inventory(0, pr.company)
def check_gle_for_pi(self, pi): def check_gle_for_pi(self, pi):
gl_entries = frappe.db.sql("""select account, debit, credit gl_entries = frappe.db.sql("""select account, sum(debit) as debit, sum(credit) as credit
from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s
order by account asc""", pi, as_dict=1) group by account""", pi, as_dict=1)
self.assertTrue(gl_entries) self.assertTrue(gl_entries)
expected_values = dict((d[0], d) for d in [ expected_values = dict((d[0], d) for d in [
["_Test Payable - _TC", 0, 720], ["Creditors - TCP1", 0, 720],
["Stock Received But Not Billed - _TC", 500.0, 0], ["Stock Received But Not Billed - TCP1", 500.0, 0],
["_Test Account Shipping Charges - _TC", 100.0, 0], ["_Test Account Shipping Charges - TCP1", 100.0, 0.0],
["_Test Account VAT - _TC", 120.0, 0], ["_Test Account VAT - TCP1", 120.0, 0]
]) ])
for i, gle in enumerate(gl_entries): for i, gle in enumerate(gl_entries):
@@ -524,10 +520,9 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertFalse(gle) self.assertFalse(gle)
def test_purchase_invoice_update_stock_gl_entry_with_perpetual_inventory(self): def test_purchase_invoice_update_stock_gl_entry_with_perpetual_inventory(self):
set_perpetual_inventory()
pi = make_purchase_invoice(update_stock=1, posting_date=frappe.utils.nowdate(), pi = make_purchase_invoice(update_stock=1, posting_date=frappe.utils.nowdate(),
posting_time=frappe.utils.nowtime()) posting_time=frappe.utils.nowtime(), cash_bank_account="Cash - TCP1", company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
gl_entries = frappe.db.sql("""select account, account_currency, debit, credit, gl_entries = frappe.db.sql("""select account, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency debit_in_account_currency, credit_in_account_currency
@@ -548,9 +543,9 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(expected_gl_entries[gle.account][2], gle.credit) self.assertEqual(expected_gl_entries[gle.account][2], gle.credit)
def test_purchase_invoice_for_is_paid_and_update_stock_gl_entry_with_perpetual_inventory(self): def test_purchase_invoice_for_is_paid_and_update_stock_gl_entry_with_perpetual_inventory(self):
set_perpetual_inventory()
pi = make_purchase_invoice(update_stock=1, posting_date=frappe.utils.nowdate(), pi = make_purchase_invoice(update_stock=1, posting_date=frappe.utils.nowdate(),
posting_time=frappe.utils.nowtime(), cash_bank_account="Cash - _TC", is_paid=1) posting_time=frappe.utils.nowtime(), cash_bank_account="Cash - TCP1", is_paid=1, company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
gl_entries = frappe.db.sql("""select account, account_currency, sum(debit) as debit, gl_entries = frappe.db.sql("""select account, account_currency, sum(debit) as debit,
sum(credit) as credit, debit_in_account_currency, credit_in_account_currency sum(credit) as credit, debit_in_account_currency, credit_in_account_currency
@@ -563,7 +558,7 @@ class TestPurchaseInvoice(unittest.TestCase):
expected_gl_entries = dict((d[0], d) for d in [ expected_gl_entries = dict((d[0], d) for d in [
[pi.credit_to, 250.0, 250.0], [pi.credit_to, 250.0, 250.0],
[stock_in_hand_account, 250.0, 0.0], [stock_in_hand_account, 250.0, 0.0],
["Cash - _TC", 0.0, 250.0] ["Cash - TCP1", 0.0, 250.0]
]) ])
for i, gle in enumerate(gl_entries): for i, gle in enumerate(gl_entries):
@@ -630,6 +625,7 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(pi.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2)) self.assertEqual(pi.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2))
def test_rejected_serial_no(self): def test_rejected_serial_no(self):
set_perpetual_inventory(0)
pi = make_purchase_invoice(item_code="_Test Serialized Item With Series", received_qty=2, qty=1, pi = make_purchase_invoice(item_code="_Test Serialized Item With Series", received_qty=2, qty=1,
rejected_qty=1, rate=500, update_stock=1, rejected_qty=1, rate=500, update_stock=1,
rejected_warehouse = "_Test Rejected Warehouse - _TC") rejected_warehouse = "_Test Rejected Warehouse - _TC")
@@ -881,7 +877,7 @@ def make_purchase_invoice(**args):
pi.is_return = args.is_return pi.is_return = args.is_return
pi.return_against = args.return_against pi.return_against = args.return_against
pi.is_subcontracted = args.is_subcontracted or "No" pi.is_subcontracted = args.is_subcontracted or "No"
pi.supplier_warehouse = "_Test Warehouse 1 - _TC" pi.supplier_warehouse = args.supplier_warehouse or "_Test Warehouse 1 - _TC"
pi.append("items", { pi.append("items", {
"item_code": args.item or args.item_code or "_Test Item", "item_code": args.item or args.item_code or "_Test Item",
@@ -890,14 +886,21 @@ def make_purchase_invoice(**args):
"received_qty": args.received_qty or 0, "received_qty": args.received_qty or 0,
"rejected_qty": args.rejected_qty or 0, "rejected_qty": args.rejected_qty or 0,
"rate": args.rate or 50, "rate": args.rate or 50,
'expense_account': args.expense_account or '_Test Account Cost for Goods Sold - _TC',
"conversion_factor": 1.0, "conversion_factor": 1.0,
"serial_no": args.serial_no, "serial_no": args.serial_no,
"stock_uom": "_Test UOM", "stock_uom": "_Test UOM",
"cost_center": "_Test Cost Center - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC",
"project": args.project, "project": args.project,
"rejected_warehouse": args.rejected_warehouse or "", "rejected_warehouse": args.rejected_warehouse or "",
"rejected_serial_no": args.rejected_serial_no or "" "rejected_serial_no": args.rejected_serial_no or ""
}) })
if args.get_taxes_and_charges:
taxes = get_taxes()
for tax in taxes:
pi.append("taxes", tax)
if not args.do_not_save: if not args.do_not_save:
pi.insert() pi.insert()
if not args.do_not_submit: if not args.do_not_submit:

View File

@@ -71,8 +71,8 @@
"expense_account", "expense_account",
"col_break5", "col_break5",
"is_fixed_asset", "is_fixed_asset",
"asset",
"asset_location", "asset_location",
"asset_category",
"deferred_expense_section", "deferred_expense_section",
"deferred_expense_account", "deferred_expense_account",
"service_stop_date", "service_stop_date",
@@ -116,6 +116,8 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"fetch_from": "item_code.item_name",
"fetch_if_empty": 1,
"fieldname": "item_name", "fieldname": "item_name",
"fieldtype": "Data", "fieldtype": "Data",
"in_global_search": 1, "in_global_search": 1,
@@ -414,6 +416,7 @@
"print_hide": 1 "print_hide": 1
}, },
{ {
"depends_on": "eval:!doc.is_fixed_asset",
"fieldname": "batch_no", "fieldname": "batch_no",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Batch No", "label": "Batch No",
@@ -425,12 +428,14 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"depends_on": "eval:!doc.is_fixed_asset",
"fieldname": "serial_no", "fieldname": "serial_no",
"fieldtype": "Text", "fieldtype": "Text",
"label": "Serial No", "label": "Serial No",
"no_copy": 1 "no_copy": 1
}, },
{ {
"depends_on": "eval:!doc.is_fixed_asset",
"fieldname": "rejected_serial_no", "fieldname": "rejected_serial_no",
"fieldtype": "Text", "fieldtype": "Text",
"label": "Rejected Serial No", "label": "Rejected Serial No",
@@ -615,6 +620,7 @@
}, },
{ {
"default": "0", "default": "0",
"fetch_from": "item_code.is_fixed_asset",
"fieldname": "is_fixed_asset", "fieldname": "is_fixed_asset",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1, "hidden": 1,
@@ -623,14 +629,6 @@
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{
"depends_on": "is_fixed_asset",
"fieldname": "asset",
"fieldtype": "Link",
"label": "Asset",
"no_copy": 1,
"options": "Asset"
},
{ {
"depends_on": "is_fixed_asset", "depends_on": "is_fixed_asset",
"fieldname": "asset_location", "fieldname": "asset_location",
@@ -676,7 +674,7 @@
"fieldname": "pr_detail", "fieldname": "pr_detail",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "PR Detail", "label": "Purchase Receipt Detail",
"no_copy": 1, "no_copy": 1,
"oldfieldname": "pr_detail", "oldfieldname": "pr_detail",
"oldfieldtype": "Data", "oldfieldtype": "Data",
@@ -754,11 +752,21 @@
"fieldtype": "Data", "fieldtype": "Data",
"label": "Manufacturer Part Number", "label": "Manufacturer Part Number",
"read_only": 1 "read_only": 1
},
{
"depends_on": "is_fixed_asset",
"fetch_from": "item_code.asset_category",
"fieldname": "asset_category",
"fieldtype": "Data",
"in_preview": 1,
"label": "Asset Category",
"options": "Asset Category",
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"modified": "2019-09-17 22:32:05.984240", "modified": "2019-11-21 16:27:52.043744",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@@ -402,14 +402,21 @@ def make_invoice(doc_list={}, email_queue_list={}, customers_list={}):
for docs in doc_list: for docs in doc_list:
for name, doc in iteritems(docs): for name, doc in iteritems(docs):
if not frappe.db.exists('Sales Invoice', {'offline_pos_name': name}): if not frappe.db.exists('Sales Invoice', {'offline_pos_name': name}):
validate_records(doc) if isinstance(doc, dict):
si_doc = frappe.new_doc('Sales Invoice') validate_records(doc)
si_doc.offline_pos_name = name si_doc = frappe.new_doc('Sales Invoice')
si_doc.update(doc) si_doc.offline_pos_name = name
si_doc.set_posting_time = 1 si_doc.update(doc)
si_doc.customer = get_customer_id(doc) si_doc.set_posting_time = 1
si_doc.due_date = doc.get('posting_date') si_doc.customer = get_customer_id(doc)
name_list = submit_invoice(si_doc, name, doc, name_list) si_doc.due_date = doc.get('posting_date')
name_list = submit_invoice(si_doc, name, doc, name_list)
else:
doc.due_date = doc.get('posting_date')
doc.customer = get_customer_id(doc)
doc.set_posting_time = 1
doc.offline_pos_name = name
name_list = submit_invoice(doc, name, doc, name_list)
else: else:
name_list.append(name) name_list.append(name)

View File

@@ -136,6 +136,16 @@ class SalesInvoice(SellingController):
if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points: if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points:
validate_loyalty_points(self, self.loyalty_points) validate_loyalty_points(self, self.loyalty_points)
def validate_fixed_asset(self):
for d in self.get("items"):
if d.is_fixed_asset and d.meta.get_field("asset") and d.asset:
asset = frappe.get_doc("Asset", d.asset)
if self.doctype == "Sales Invoice" and self.docstatus == 1:
if self.update_stock:
frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale"))
elif asset.status in ("Scrapped", "Cancelled", "Sold"):
frappe.throw(_("Row #{0}: Asset {1} cannot be submitted, it is already {2}").format(d.idx, d.asset, asset.status))
def before_save(self): def before_save(self):
set_account_for_mode_of_payment(self) set_account_for_mode_of_payment(self)
@@ -686,7 +696,6 @@ class SalesInvoice(SellingController):
def make_gl_entries(self, gl_entries=None, repost_future_gle=True, from_repost=False): def make_gl_entries(self, gl_entries=None, repost_future_gle=True, from_repost=False):
auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company) auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
if not gl_entries: if not gl_entries:
gl_entries = self.get_gl_entries() gl_entries = self.get_gl_entries()
@@ -992,10 +1001,8 @@ class SalesInvoice(SellingController):
continue continue
for serial_no in item.serial_no.split("\n"): for serial_no in item.serial_no.split("\n"):
if serial_no and frappe.db.exists('Serial No', serial_no): if serial_no and frappe.db.get_value('Serial No', serial_no, 'item_code') == item.item_code:
sno = frappe.get_doc('Serial No', serial_no) frappe.db.set_value('Serial No', serial_no, 'sales_invoice', invoice)
sno.sales_invoice = invoice
sno.db_update()
def validate_serial_numbers(self): def validate_serial_numbers(self):
""" """
@@ -1041,8 +1048,9 @@ class SalesInvoice(SellingController):
continue continue
for serial_no in item.serial_no.split("\n"): for serial_no in item.serial_no.split("\n"):
sales_invoice = frappe.db.get_value("Serial No", serial_no, "sales_invoice") sales_invoice, item_code = frappe.db.get_value("Serial No", serial_no,
if sales_invoice and self.name != sales_invoice: ["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") sales_invoice_company = frappe.db.get_value("Sales Invoice", sales_invoice, "company")
if sales_invoice_company == self.company: if sales_invoice_company == self.company:
frappe.throw(_("Serial Number: {0} is already referenced in Sales Invoice: {1}" frappe.throw(_("Serial Number: {0} is already referenced in Sales Invoice: {1}"
@@ -1231,7 +1239,8 @@ class SalesInvoice(SellingController):
self.status = "Unpaid and Discounted" self.status = "Unpaid and Discounted"
elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) >= getdate(nowdate()): elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) >= getdate(nowdate()):
self.status = "Unpaid" self.status = "Unpaid"
elif flt(self.outstanding_amount) < 0 and self.is_return==0 and frappe.db.get_value('Sales Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}): #Check if outstanding amount is 0 due to credit note issued against invoice
elif flt(self.outstanding_amount) <= 0 and self.is_return == 0 and frappe.db.get_value('Sales Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}):
self.status = "Credit Note Issued" self.status = "Credit Note Issued"
elif self.is_return == 1: elif self.is_return == 1:
self.status = "Return" self.status = "Return"

View File

@@ -68,8 +68,6 @@
"selling_price_list": "_Test Price List", "selling_price_list": "_Test Price List",
"territory": "_Test Territory" "territory": "_Test Territory"
}, },
{ {
"company": "_Test Company", "company": "_Test Company",
"conversion_rate": 1.0, "conversion_rate": 1.0,
@@ -276,7 +274,6 @@
"uom": "_Test UOM 1", "uom": "_Test UOM 1",
"conversion_factor": 1, "conversion_factor": 1,
"stock_uom": "_Test UOM 1" "stock_uom": "_Test UOM 1"
}, },
{ {
"cost_center": "_Test Cost Center - _TC", "cost_center": "_Test Cost Center - _TC",

View File

@@ -20,6 +20,9 @@ from erpnext.stock.doctype.item.test_item import create_item
from six import iteritems from six import iteritems
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction
from erpnext.regional.india.utils import get_ewb_data from erpnext.regional.india.utils import get_ewb_data
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
class TestSalesInvoice(unittest.TestCase): class TestSalesInvoice(unittest.TestCase):
def make(self): def make(self):
@@ -550,7 +553,6 @@ class TestSalesInvoice(unittest.TestCase):
si.get("taxes")[6].tax_amount = 2 si.get("taxes")[6].tax_amount = 2
si.insert() si.insert()
print(si.name)
expected_values = [ expected_values = [
{ {
@@ -679,56 +681,67 @@ class TestSalesInvoice(unittest.TestCase):
self.assertFalse(gle) self.assertFalse(gle)
def test_pos_gl_entry_with_perpetual_inventory(self): def test_pos_gl_entry_with_perpetual_inventory(self):
set_perpetual_inventory()
make_pos_profile() make_pos_profile()
self._insert_purchase_receipt() pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",supplier_warehouse= "Work In Progress - TCP1", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1")
pos = copy.deepcopy(test_records[1])
pos["is_pos"] = 1 pos = create_sales_invoice(company= "_Test Company with perpetual inventory", debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1", income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", cost_center = "Main - TCP1", do_not_save=True)
pos["update_stock"] = 1
pos["payments"] = [{'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 300}, pos.is_pos = 1
{'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300}] pos.update_stock = 1
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50})
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 50})
taxes = get_taxes_and_charges()
pos.taxes = []
for tax in taxes:
pos.append("taxes", tax)
si = frappe.copy_doc(pos) si = frappe.copy_doc(pos)
si.insert() si.insert()
si.submit() si.submit()
self.assertEqual(si.paid_amount, 100.0)
self.assertEqual(si.paid_amount, 600.0) self.pos_gl_entry(si, pos, 50)
self.pos_gl_entry(si, pos, 300)
def test_pos_change_amount(self): def test_pos_change_amount(self):
set_perpetual_inventory()
make_pos_profile() make_pos_profile()
self._insert_purchase_receipt() pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",supplier_warehouse= "Work In Progress - TCP1", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1")
pos = copy.deepcopy(test_records[1])
pos["is_pos"] = 1
pos["update_stock"] = 1
pos["payments"] = [{'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 300},
{'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 340}]
si = frappe.copy_doc(pos) pos = create_sales_invoice(company= "_Test Company with perpetual inventory", debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1", income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", cost_center = "Main - TCP1", do_not_save=True)
si.change_amount = 5.0
si.insert()
si.submit()
self.assertEqual(si.grand_total, 630.0) pos.is_pos = 1
self.assertEqual(si.write_off_amount, -5) pos.update_stock = 1
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50})
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 60})
pos.change_amount = 5.0
pos.insert()
pos.submit()
self.assertEqual(pos.grand_total, 100.0)
self.assertEqual(pos.write_off_amount, -5)
def test_make_pos_invoice(self): def test_make_pos_invoice(self):
from erpnext.accounts.doctype.sales_invoice.pos import make_invoice from erpnext.accounts.doctype.sales_invoice.pos import make_invoice
set_perpetual_inventory()
make_pos_profile() make_pos_profile()
self._insert_purchase_receipt() pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",supplier_warehouse= "Work In Progress - TCP1", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1")
pos = create_sales_invoice(company= "_Test Company with perpetual inventory", debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1", income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", cost_center = "Main - TCP1", do_not_save=True)
pos = copy.deepcopy(test_records[1]) pos.is_pos = 1
pos["is_pos"] = 1 pos.update_stock = 1
pos["update_stock"] = 1
pos["payments"] = [{'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 300}, pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50})
{'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 330}] pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 50})
taxes = get_taxes_and_charges()
pos.taxes = []
for tax in taxes:
pos.append("taxes", tax)
invoice_data = [{'09052016142': pos}] invoice_data = [{'09052016142': pos}]
si = make_invoice(invoice_data).get('invoice') si = make_invoice(invoice_data).get('invoice')
@@ -736,16 +749,15 @@ class TestSalesInvoice(unittest.TestCase):
sales_invoice = frappe.get_all('Sales Invoice', fields =["*"], filters = {'offline_pos_name': '09052016142', 'docstatus': 1}) sales_invoice = frappe.get_all('Sales Invoice', fields =["*"], filters = {'offline_pos_name': '09052016142', 'docstatus': 1})
si = frappe.get_doc('Sales Invoice', sales_invoice[0].name) si = frappe.get_doc('Sales Invoice', sales_invoice[0].name)
self.assertEqual(si.grand_total, 630.0)
self.pos_gl_entry(si, pos, 330) self.assertEqual(si.grand_total, 100)
self.pos_gl_entry(si, pos, 50)
def test_make_pos_invoice_in_draft(self): def test_make_pos_invoice_in_draft(self):
from erpnext.accounts.doctype.sales_invoice.pos import make_invoice from erpnext.accounts.doctype.sales_invoice.pos import make_invoice
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
set_perpetual_inventory()
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock') allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
if allow_negative_stock: if allow_negative_stock:
frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 0) frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 0)
@@ -789,7 +801,7 @@ class TestSalesInvoice(unittest.TestCase):
si.name, as_dict=1)[0] si.name, as_dict=1)[0]
self.assertTrue(sle) self.assertTrue(sle)
self.assertEqual([sle.item_code, sle.warehouse, sle.actual_qty], self.assertEqual([sle.item_code, sle.warehouse, sle.actual_qty],
["_Test Item", "_Test Warehouse - _TC", -1.0]) ['_Test FG Item', 'Stores - TCP1', -1.0])
# check gl entries # check gl entries
gl_entries = frappe.db.sql("""select account, debit, credit gl_entries = frappe.db.sql("""select account, debit, credit
@@ -797,19 +809,19 @@ class TestSalesInvoice(unittest.TestCase):
order by account asc, debit asc, credit asc""", si.name, as_dict=1) order by account asc, debit asc, credit asc""", si.name, as_dict=1)
self.assertTrue(gl_entries) self.assertTrue(gl_entries)
stock_in_hand = get_inventory_account('_Test Company') stock_in_hand = get_inventory_account('_Test Company with perpetual inventory')
expected_gl_entries = sorted([ expected_gl_entries = sorted([
[si.debit_to, 630.0, 0.0], [si.debit_to, 100.0, 0.0],
[pos["items"][0]["income_account"], 0.0, 500.0], [pos.items[0].income_account, 0.0, 89.09],
[pos["taxes"][0]["account_head"], 0.0, 80.0], ['Round Off - TCP1', 0.0, 0.01],
[pos["taxes"][1]["account_head"], 0.0, 50.0], [pos.taxes[0].account_head, 0.0, 10.69],
[pos.taxes[1].account_head, 0.0, 0.21],
[stock_in_hand, 0.0, abs(sle.stock_value_difference)], [stock_in_hand, 0.0, abs(sle.stock_value_difference)],
[pos["items"][0]["expense_account"], abs(sle.stock_value_difference), 0.0], [pos.items[0].expense_account, abs(sle.stock_value_difference), 0.0],
[si.debit_to, 0.0, 300.0], [si.debit_to, 0.0, 50.0],
[si.debit_to, 0.0, cash_amount], [si.debit_to, 0.0, cash_amount],
["_Test Bank - _TC", 300.0, 0.0], ["_Test Bank - TCP1", 50, 0.0],
["Cash - _TC", cash_amount, 0.0] ["Cash - TCP1", cash_amount, 0.0]
]) ])
for i, gle in enumerate(sorted(gl_entries, key=lambda gle: gle.account)): for i, gle in enumerate(sorted(gl_entries, key=lambda gle: gle.account)):
@@ -823,9 +835,9 @@ class TestSalesInvoice(unittest.TestCase):
self.assertFalse(gle) self.assertFalse(gle)
set_perpetual_inventory(0)
frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Profile`")
si.delete()
def test_pos_si_without_payment(self): def test_pos_si_without_payment(self):
set_perpetual_inventory() set_perpetual_inventory()
@@ -1008,7 +1020,6 @@ class TestSalesInvoice(unittest.TestCase):
""" """
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
se = make_serialized_item() se = make_serialized_item()
@@ -1023,14 +1034,17 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(si.get("items")[0].serial_no, dn.get("items")[0].serial_no) self.assertEqual(si.get("items")[0].serial_no, dn.get("items")[0].serial_no)
def test_return_sales_invoice(self): def test_return_sales_invoice(self):
set_perpetual_inventory() make_stock_entry(item_code="_Test Item", target="Stores - TCP1", qty=50, basic_rate=100)
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
actual_qty_0 = get_qty_after_transaction() actual_qty_0 = get_qty_after_transaction(item_code = "_Test Item", warehouse = "Stores - TCP1")
si = create_sales_invoice(qty=5, rate=500, update_stock=1) si = create_sales_invoice(qty = 5, rate=500, update_stock=1, company= "_Test Company with perpetual inventory", debit_to="Debtors - TCP1", item_code= "_Test Item", warehouse="Stores - TCP1", income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", cost_center = "Main - TCP1")
actual_qty_1 = get_qty_after_transaction(item_code = "_Test Item", warehouse = "Stores - TCP1")
frappe.db.commit()
actual_qty_1 = get_qty_after_transaction()
self.assertEqual(actual_qty_0 - 5, actual_qty_1) self.assertEqual(actual_qty_0 - 5, actual_qty_1)
# outgoing_rate # outgoing_rate
@@ -1038,10 +1052,9 @@ class TestSalesInvoice(unittest.TestCase):
"voucher_no": si.name}, "stock_value_difference") / 5 "voucher_no": si.name}, "stock_value_difference") / 5
# return entry # return entry
si1 = create_sales_invoice(is_return=1, return_against=si.name, qty=-2, rate=500, update_stock=1) si1 = create_sales_invoice(is_return=1, return_against=si.name, qty=-2, rate=500, update_stock=1, company= "_Test Company with perpetual inventory", debit_to="Debtors - TCP1", item_code= "_Test Item", warehouse="Stores - TCP1", income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", cost_center = "Main - TCP1")
actual_qty_2 = get_qty_after_transaction()
actual_qty_2 = get_qty_after_transaction(item_code = "_Test Item", warehouse = "Stores - TCP1")
self.assertEqual(actual_qty_1 + 2, actual_qty_2) self.assertEqual(actual_qty_1 + 2, actual_qty_2)
incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
@@ -1049,7 +1062,7 @@ class TestSalesInvoice(unittest.TestCase):
["incoming_rate", "stock_value_difference"]) ["incoming_rate", "stock_value_difference"])
self.assertEqual(flt(incoming_rate, 3), abs(flt(outgoing_rate, 3))) self.assertEqual(flt(incoming_rate, 3), abs(flt(outgoing_rate, 3)))
stock_in_hand_account = get_inventory_account('_Test Company', si1.items[0].warehouse) stock_in_hand_account = get_inventory_account('_Test Company with perpetual inventory', si1.items[0].warehouse)
# Check gl entry # Check gl entry
gle_warehouse_amount = frappe.db.get_value("GL Entry", {"voucher_type": "Sales Invoice", gle_warehouse_amount = frappe.db.get_value("GL Entry", {"voucher_type": "Sales Invoice",
@@ -1058,7 +1071,7 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(gle_warehouse_amount, stock_value_difference) self.assertEqual(gle_warehouse_amount, stock_value_difference)
party_credited = frappe.db.get_value("GL Entry", {"voucher_type": "Sales Invoice", party_credited = frappe.db.get_value("GL Entry", {"voucher_type": "Sales Invoice",
"voucher_no": si1.name, "account": "Debtors - _TC", "party": "_Test Customer"}, "credit") "voucher_no": si1.name, "account": "Debtors - TCP1", "party": "_Test Customer"}, "credit")
self.assertEqual(party_credited, 1000) self.assertEqual(party_credited, 1000)
@@ -1066,7 +1079,6 @@ class TestSalesInvoice(unittest.TestCase):
self.assertFalse(si1.outstanding_amount) self.assertFalse(si1.outstanding_amount)
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 1500) self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 1500)
set_perpetual_inventory(0)
def test_discount_on_net_total(self): def test_discount_on_net_total(self):
si = frappe.copy_doc(test_records[2]) si = frappe.copy_doc(test_records[2])
@@ -1524,6 +1536,8 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(si.total_taxes_and_charges, 577.05) self.assertEqual(si.total_taxes_and_charges, 577.05)
self.assertEqual(si.grand_total, 1827.05) self.assertEqual(si.grand_total, 1827.05)
def test_create_invoice_without_terms(self): def test_create_invoice_without_terms(self):
si = create_sales_invoice(do_not_save=1) si = create_sales_invoice(do_not_save=1)
self.assertFalse(si.get('payment_schedule')) self.assertFalse(si.get('payment_schedule'))
@@ -1930,4 +1944,29 @@ def get_outstanding_amount(against_voucher_type, against_voucher, account, party
if against_voucher_type == 'Purchase Invoice': if against_voucher_type == 'Purchase Invoice':
bal = bal * -1 bal = bal * -1
return bal return bal
def get_taxes_and_charges():
return [{
"account_head": "_Test Account Excise Duty - TCP1",
"charge_type": "On Net Total",
"cost_center": "Main - TCP1",
"description": "Excise Duty",
"doctype": "Sales Taxes and Charges",
"idx": 1,
"included_in_print_rate": 1,
"parentfield": "taxes",
"rate": 12
},
{
"account_head": "_Test Account Education Cess - TCP1",
"charge_type": "On Previous Row Amount",
"cost_center": "Main - TCP1",
"description": "Education Cess",
"doctype": "Sales Taxes and Charges",
"idx": 2,
"included_in_print_rate": 1,
"parentfield": "taxes",
"rate": 2,
"row_id": 1
}]

View File

@@ -21,6 +21,8 @@ frappe.ui.form.on('Share Transfer', {
erpnext.share_transfer.make_jv(frm); erpnext.share_transfer.make_jv(frm);
}); });
} }
frm.toggle_reqd("asset_account", frm.doc.transfer_type != "Transfer");
}, },
no_of_shares: (frm) => { no_of_shares: (frm) => {
if (frm.doc.rate != undefined || frm.doc.rate != null){ if (frm.doc.rate != undefined || frm.doc.rate != null){
@@ -56,6 +58,10 @@ frappe.ui.form.on('Share Transfer', {
}; };
}); });
} }
},
transfer_type: function(frm) {
frm.toggle_reqd("asset_account", frm.doc.transfer_type != "Transfer");
} }
}); });

File diff suppressed because it is too large Load Diff

View File

@@ -14,13 +14,13 @@ class TestShippingRule(unittest.TestCase):
shipping_rule.name = test_records[0].get('name') shipping_rule.name = test_records[0].get('name')
shipping_rule.get("conditions")[0].from_value = 101 shipping_rule.get("conditions")[0].from_value = 101
self.assertRaises(FromGreaterThanToError, shipping_rule.insert) self.assertRaises(FromGreaterThanToError, shipping_rule.insert)
def test_many_zero_to_values(self): def test_many_zero_to_values(self):
shipping_rule = frappe.copy_doc(test_records[0]) shipping_rule = frappe.copy_doc(test_records[0])
shipping_rule.name = test_records[0].get('name') shipping_rule.name = test_records[0].get('name')
shipping_rule.get("conditions")[0].to_value = 0 shipping_rule.get("conditions")[0].to_value = 0
self.assertRaises(ManyBlankToValuesError, shipping_rule.insert) self.assertRaises(ManyBlankToValuesError, shipping_rule.insert)
def test_overlapping_conditions(self): def test_overlapping_conditions(self):
for range_a, range_b in [ for range_a, range_b in [
((50, 150), (0, 100)), ((50, 150), (0, 100)),
@@ -38,6 +38,10 @@ class TestShippingRule(unittest.TestCase):
self.assertRaises(OverlappingConditionError, shipping_rule.insert) self.assertRaises(OverlappingConditionError, shipping_rule.insert)
def create_shipping_rule(shipping_rule_type, shipping_rule_name): def create_shipping_rule(shipping_rule_type, shipping_rule_name):
if frappe.db.exists("Shipping Rule", shipping_rule_name):
return frappe.get_doc("Shipping Rule", shipping_rule_name)
sr = frappe.new_doc("Shipping Rule") sr = frappe.new_doc("Shipping Rule")
sr.account = "_Test Account Shipping Charges - _TC" sr.account = "_Test Account Shipping Charges - _TC"
sr.calculate_based_on = "Net Total" sr.calculate_based_on = "Net Total"
@@ -70,4 +74,4 @@ def create_shipping_rule(shipping_rule_type, shipping_rule_name):
}) })
sr.insert(ignore_permissions=True) sr.insert(ignore_permissions=True)
sr.submit() sr.submit()
return sr return sr

View File

@@ -3,8 +3,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
from frappe.utils import flt, cstr, cint from frappe.utils import flt, cstr, cint, comma_and
from frappe import _ from frappe import _
from erpnext.accounts.utils import get_stock_and_account_balance
from frappe.model.meta import get_field_precision from frappe.model.meta import get_field_precision
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
@@ -12,6 +13,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g
class ClosedAccountingPeriod(frappe.ValidationError): pass class ClosedAccountingPeriod(frappe.ValidationError): pass
class StockAccountInvalidTransaction(frappe.ValidationError): pass class StockAccountInvalidTransaction(frappe.ValidationError): pass
class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError): pass
def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, update_outstanding='Yes', from_repost=False): def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, update_outstanding='Yes', from_repost=False):
if gl_map: if gl_map:
@@ -115,11 +117,9 @@ def check_if_in_list(gle, gl_map, dimensions=None):
def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False): def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
if not from_repost: if not from_repost:
validate_account_for_perpetual_inventory(gl_map)
validate_cwip_accounts(gl_map) validate_cwip_accounts(gl_map)
round_off_debit_credit(gl_map) round_off_debit_credit(gl_map)
for entry in gl_map: for entry in gl_map:
make_entry(entry, adv_adj, update_outstanding, from_repost) make_entry(entry, adv_adj, update_outstanding, from_repost)
@@ -127,6 +127,10 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
if not from_repost: if not from_repost:
validate_expense_against_budget(entry) validate_expense_against_budget(entry)
if not from_repost:
validate_account_for_perpetual_inventory(gl_map)
def make_entry(args, adv_adj, update_outstanding, from_repost=False): def make_entry(args, adv_adj, update_outstanding, from_repost=False):
args.update({"doctype": "GL Entry"}) args.update({"doctype": "GL Entry"})
gle = frappe.get_doc(args) gle = frappe.get_doc(args)
@@ -137,25 +141,66 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
gle.submit() gle.submit()
def validate_account_for_perpetual_inventory(gl_map): def validate_account_for_perpetual_inventory(gl_map):
if cint(erpnext.is_perpetual_inventory_enabled(gl_map[0].company)) \ if cint(erpnext.is_perpetual_inventory_enabled(gl_map[0].company)):
and gl_map[0].voucher_type=="Journal Entry": account_list = [gl_entries.account for gl_entries in gl_map]
aii_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount
where account_type = 'Stock' and is_group=0""")]
for entry in gl_map: aii_accounts = [d.name for d in frappe.get_all("Account",
if entry.account in aii_accounts: filters={'account_type': 'Stock', 'is_group': 0, 'company': gl_map[0].company})]
for account in account_list:
if account not in aii_accounts:
continue
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account,
gl_map[0].posting_date, gl_map[0].company)
if gl_map[0].voucher_type=="Journal Entry":
# In case of Journal Entry, there are no corresponding SL entries,
# hence deducting currency amount
account_bal -= flt(gl_map[0].debit) - flt(gl_map[0].credit)
if account_bal == stock_bal:
frappe.throw(_("Account: {0} can only be updated via Stock Transactions") frappe.throw(_("Account: {0} can only be updated via Stock Transactions")
.format(entry.account), StockAccountInvalidTransaction) .format(account), StockAccountInvalidTransaction)
elif account_bal != stock_bal:
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"),
currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency"))
diff = flt(stock_bal - account_bal, precision)
error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses.").format(
stock_bal, account_bal, frappe.bold(account))
error_resolution = _("Please create adjustment Journal Entry for amount {0} ").format(frappe.bold(diff))
stock_adjustment_account = frappe.db.get_value("Company",gl_map[0].company,"stock_adjustment_account")
db_or_cr_warehouse_account =('credit_in_account_currency' if diff < 0 else 'debit_in_account_currency')
db_or_cr_stock_adjustment_account = ('debit_in_account_currency' if diff < 0 else 'credit_in_account_currency')
journal_entry_args = {
'accounts':[
{'account': account, db_or_cr_warehouse_account : abs(diff)},
{'account': stock_adjustment_account, db_or_cr_stock_adjustment_account : abs(diff) }]
}
frappe.msgprint(msg="""{0}<br></br>{1}<br></br>""".format(error_reason, error_resolution),
raise_exception=StockValueAndAccountBalanceOutOfSync,
title=_('Values Out Of Sync'),
primary_action={
'label': _('Make Journal Entry'),
'client_action': 'erpnext.route_to_adjustment_jv',
'args': journal_entry_args
})
def validate_cwip_accounts(gl_map): def validate_cwip_accounts(gl_map):
if not cint(frappe.db.get_value("Asset Settings", None, "disable_cwip_accounting")) \ cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")])
and gl_map[0].voucher_type == "Journal Entry":
if cwip_enabled and gl_map[0].voucher_type == "Journal Entry":
cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount
where account_type = 'Capital Work in Progress' and is_group=0""")] where account_type = 'Capital Work in Progress' and is_group=0""")]
for entry in gl_map: for entry in gl_map:
if entry.account in cwip_accounts: if entry.account in cwip_accounts:
frappe.throw(_("Account: <b>{0}</b> is capital Work in progress and can not be updated by Journal Entry").format(entry.account)) frappe.throw(
_("Account: <b>{0}</b> is capital Work in progress and can not be updated by Journal Entry").format(entry.account))
def round_off_debit_credit(gl_map): def round_off_debit_credit(gl_map):
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"),

View File

@@ -139,15 +139,11 @@ erpnext.accounts.bankTransactionUpload = class bankTransactionUpload {
} }
make() { make() {
const me = this; const me = this;
frappe.upload.make({ new frappe.ui.FileUploader({
args: { method: 'erpnext.accounts.doctype.bank_transaction.bank_transaction_upload.upload_bank_statement',
method: 'erpnext.accounts.doctype.bank_transaction.bank_transaction_upload.upload_bank_statement', allow_multiple: 0,
allow_multiple: 0 on_success: function(attachment, r) {
},
no_socketio: true,
sample_url: "e.g. http://example.com/somefile.csv",
callback: function(attachment, r) {
if (!r.exc && r.message) { if (!r.exc && r.message) {
me.data = r.message; me.data = r.message;
me.setup_transactions_dom(); me.setup_transactions_dom();
@@ -533,9 +529,16 @@ erpnext.accounts.ReconciliationRow = class ReconciliationRow {
frappe.db.get_doc(dt, event.value) frappe.db.get_doc(dt, event.value)
.then(doc => { .then(doc => {
let displayed_docs = [] let displayed_docs = []
let payment = []
if (dt === "Payment Entry") { if (dt === "Payment Entry") {
payment.currency = doc.payment_type == "Receive" ? doc.paid_to_account_currency : doc.paid_from_account_currency; payment.currency = doc.payment_type == "Receive" ? doc.paid_to_account_currency : doc.paid_from_account_currency;
payment.doctype = dt payment.doctype = dt
payment.posting_date = doc.posting_date;
payment.party = doc.party;
payment.reference_no = doc.reference_no;
payment.reference_date = doc.reference_date;
payment.paid_amount = doc.paid_amount;
payment.name = doc.name;
displayed_docs.push(payment); displayed_docs.push(payment);
} else if (dt === "Journal Entry") { } else if (dt === "Journal Entry") {
doc.accounts.forEach(payment => { doc.accounts.forEach(payment => {
@@ -568,11 +571,11 @@ erpnext.accounts.ReconciliationRow = class ReconciliationRow {
const details_wrapper = me.dialog.fields_dict.payment_details.$wrapper; const details_wrapper = me.dialog.fields_dict.payment_details.$wrapper;
details_wrapper.append(frappe.render_template("linked_payment_header")); details_wrapper.append(frappe.render_template("linked_payment_header"));
displayed_docs.forEach(values => { displayed_docs.forEach(payment => {
details_wrapper.append(frappe.render_template("linked_payment_row", values)); details_wrapper.append(frappe.render_template("linked_payment_row", payment));
}) })
}) })
} }
} }
} }

View File

@@ -79,13 +79,20 @@ frappe.query_reports["Accounts Receivable"] = {
"options": "Customer", "options": "Customer",
on_change: () => { on_change: () => {
var customer = frappe.query_report.get_filter_value('customer'); var customer = frappe.query_report.get_filter_value('customer');
var company = frappe.query_report.get_filter_value('company');
if (customer) { if (customer) {
frappe.db.get_value('Customer', customer, ["tax_id", "customer_name", "credit_limit", "payment_terms"], function(value) { frappe.db.get_value('Customer', customer, ["tax_id", "customer_name", "payment_terms"], function(value) {
frappe.query_report.set_filter_value('tax_id', value["tax_id"]); frappe.query_report.set_filter_value('tax_id', value["tax_id"]);
frappe.query_report.set_filter_value('customer_name', value["customer_name"]); frappe.query_report.set_filter_value('customer_name', value["customer_name"]);
frappe.query_report.set_filter_value('credit_limit', value["credit_limit"]);
frappe.query_report.set_filter_value('payment_terms', value["payment_terms"]); frappe.query_report.set_filter_value('payment_terms', value["payment_terms"]);
}); });
frappe.db.get_value('Customer Credit Limit', {'parent': customer, 'company': company},
["credit_limit"], function(value) {
if (value) {
frappe.query_report.set_filter_value('credit_limit', value["credit_limit"]);
}
}, "Customer");
} else { } else {
frappe.query_report.set_filter_value('tax_id', ""); frappe.query_report.set_filter_value('tax_id', "");
frappe.query_report.set_filter_value('customer_name', ""); frappe.query_report.set_filter_value('customer_name', "");

View File

@@ -188,7 +188,11 @@ class ReceivablePayableReport(object):
self.data.append(row) self.data.append(row)
def set_invoice_details(self, 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 row.voucher_type == 'Sales Invoice':
if self.filters.show_delivery_notes: if self.filters.show_delivery_notes:
self.set_delivery_notes(row) self.set_delivery_notes(row)

View File

@@ -36,6 +36,9 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.filters.report_date) or {} self.filters.report_date) or {}
for party, party_dict in iteritems(self.party_total): for party, party_dict in iteritems(self.party_total):
if party_dict.outstanding <= 0:
continue
row = frappe._dict() row = frappe._dict()
row.party = party row.party = party

View File

@@ -2,7 +2,7 @@
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
frappe.require("assets/erpnext/js/financial_statements.js", function() { 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({ frappe.query_reports["Balance Sheet"]["filters"].push({
"fieldname": "accumulated_values", "fieldname": "accumulated_values",

View File

@@ -69,7 +69,7 @@ def get_columns(filters):
for year in fiscal_year: for year in fiscal_year:
for from_date, to_date in get_period_date_ranges(filters["period"], year[0]): for from_date, to_date in get_period_date_ranges(filters["period"], year[0]):
if filters["period"] == "Yearly": if filters["period"] == "Yearly":
labels = [_("Budget") + " " + str(year[0]), _("Actual ") + " " + str(year[0]), _("Varaiance ") + " " + str(year[0])] labels = [_("Budget") + " " + str(year[0]), _("Actual ") + " " + str(year[0]), _("Variance ") + " " + str(year[0])]
for label in labels: for label in labels:
columns.append(label+":Float:150") columns.append(label+":Float:150")
else: else:

View File

@@ -76,8 +76,7 @@ def get_data(filters):
accumulate_values_into_parents(accounts, accounts_by_name) accumulate_values_into_parents(accounts, accounts_by_name)
data = prepare_data(accounts, filters, total_row, parent_children_map, company_currency) data = prepare_data(accounts, filters, total_row, parent_children_map, company_currency)
data = filter_out_zero_value_rows(data, parent_children_map, data = filter_out_zero_value_rows(data, parent_children_map, show_zero_values=filters.get("show_zero_values"))
show_zero_values=filters.get("show_zero_values"))
return data return data
@@ -187,33 +186,11 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters,
d["closing_debit"] = d["opening_debit"] + d["debit"] d["closing_debit"] = d["opening_debit"] + d["debit"]
d["closing_credit"] = d["opening_credit"] + d["credit"] d["closing_credit"] = d["opening_credit"] + d["credit"]
total_row["debit"] += d["debit"]
total_row["credit"] += d["credit"]
if d["root_type"] == "Asset" or d["root_type"] == "Equity" or d["root_type"] == "Expense": prepare_opening_closing(d)
d["opening_debit"] -= d["opening_credit"]
d["closing_debit"] -= d["closing_credit"]
# For opening for field in value_fields:
check_opening_closing_has_negative_value(d, "opening_debit", "opening_credit") total_row[field] += d[field]
# For closing
check_opening_closing_has_negative_value(d, "closing_debit", "closing_credit")
if d["root_type"] == "Liability" or d["root_type"] == "Income":
d["opening_credit"] -= d["opening_debit"]
d["closing_credit"] -= d["closing_debit"]
# For opening
check_opening_closing_has_negative_value(d, "opening_credit", "opening_debit")
# For closing
check_opening_closing_has_negative_value(d, "closing_credit", "closing_debit")
total_row["opening_debit"] += d["opening_debit"]
total_row["closing_debit"] += d["closing_debit"]
total_row["opening_credit"] += d["opening_credit"]
total_row["closing_credit"] += d["closing_credit"]
return total_row return total_row
@@ -227,6 +204,10 @@ def prepare_data(accounts, filters, total_row, parent_children_map, company_curr
data = [] data = []
for d in accounts: for d in accounts:
# Prepare opening closing for group account
if parent_children_map.get(d.account):
prepare_opening_closing(d)
has_value = False has_value = False
row = { row = {
"account": d.name, "account": d.name,
@@ -313,11 +294,16 @@ def get_columns():
} }
] ]
def check_opening_closing_has_negative_value(d, dr_or_cr, switch_to_column): def prepare_opening_closing(row):
# If opening debit has negetive value then move it to opening credit and vice versa. dr_or_cr = "debit" if row["root_type"] in ["Asset", "Equity", "Expense"] else "credit"
reverse_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit"
if d[dr_or_cr] < 0: for col_type in ["opening", "closing"]:
d[switch_to_column] = abs(d[dr_or_cr]) valid_col = col_type + "_" + dr_or_cr
d[dr_or_cr] = 0.0 reverse_col = col_type + "_" + reverse_dr_or_cr
else: row[valid_col] -= row[reverse_col]
d[switch_to_column] = 0.0 if row[valid_col] < 0:
row[reverse_col] = abs(row[valid_col])
row[valid_col] = 0.0
else:
row[reverse_col] = 0.0

View File

@@ -13,6 +13,10 @@ from six import iteritems
# imported to enable erpnext.accounts.utils.get_account_currency # imported to enable erpnext.accounts.utils.get_account_currency
from erpnext.accounts.doctype.account.account import get_account_currency from erpnext.accounts.doctype.account.account import get_account_currency
from erpnext.stock.utils import get_stock_value_on
from erpnext.stock import get_warehouse_account_map
class FiscalYearError(frappe.ValidationError): pass class FiscalYearError(frappe.ValidationError): pass
@frappe.whitelist() @frappe.whitelist()
@@ -560,23 +564,23 @@ def fix_total_debit_credit():
(dr_or_cr, dr_or_cr, '%s', '%s', '%s', dr_or_cr), (dr_or_cr, dr_or_cr, '%s', '%s', '%s', dr_or_cr),
(d.diff, d.voucher_type, d.voucher_no)) (d.diff, d.voucher_type, d.voucher_no))
def get_stock_and_account_difference(account_list=None, posting_date=None, company=None): def get_stock_and_account_balance(account=None, posting_date=None, company=None):
from erpnext.stock.utils import get_stock_value_on
from erpnext.stock import get_warehouse_account_map
if not posting_date: posting_date = nowdate() if not posting_date: posting_date = nowdate()
difference = {}
warehouse_account = get_warehouse_account_map(company) warehouse_account = get_warehouse_account_map(company)
for warehouse, account_data in iteritems(warehouse_account): account_balance = get_balance_on(account, posting_date, in_account_currency=False)
if account_data.get('account') in account_list:
account_balance = get_balance_on(account_data.get('account'), posting_date, in_account_currency=False)
stock_value = get_stock_value_on(warehouse, posting_date)
if abs(flt(stock_value) - flt(account_balance)) > 0.005:
difference.setdefault(account_data.get('account'), flt(stock_value) - flt(account_balance))
return difference related_warehouses = [wh for wh, wh_details in warehouse_account.items()
if wh_details.account == account and not wh_details.is_group]
total_stock_value = 0.0
for warehouse in related_warehouses:
value = get_stock_value_on(warehouse, posting_date)
total_stock_value += value
precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses
def get_currency_precision(): def get_currency_precision():
precision = cint(frappe.db.get_default("currency_precision")) precision = cint(frappe.db.get_default("currency_precision"))
@@ -626,7 +630,7 @@ def get_held_invoices(party_type, party):
'select name from `tabPurchase Invoice` where release_date IS NOT NULL and release_date > CURDATE()', 'select name from `tabPurchase Invoice` where release_date IS NOT NULL and release_date > CURDATE()',
as_dict=1 as_dict=1
) )
held_invoices = [d['name'] for d in held_invoices] held_invoices = set([d['name'] for d in held_invoices])
return held_invoices return held_invoices
@@ -635,14 +639,19 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters
outstanding_invoices = [] outstanding_invoices = []
precision = frappe.get_precision("Sales Invoice", "outstanding_amount") or 2 precision = frappe.get_precision("Sales Invoice", "outstanding_amount") or 2
if erpnext.get_party_account_type(party_type) == 'Receivable': if account:
root_type = frappe.get_cached_value("Account", account, "root_type")
party_account_type = "Receivable" if root_type == "Asset" else "Payable"
else:
party_account_type = erpnext.get_party_account_type(party_type)
if party_account_type == 'Receivable':
dr_or_cr = "debit_in_account_currency - credit_in_account_currency" dr_or_cr = "debit_in_account_currency - credit_in_account_currency"
payment_dr_or_cr = "credit_in_account_currency - debit_in_account_currency" payment_dr_or_cr = "credit_in_account_currency - debit_in_account_currency"
else: else:
dr_or_cr = "credit_in_account_currency - debit_in_account_currency" dr_or_cr = "credit_in_account_currency - debit_in_account_currency"
payment_dr_or_cr = "debit_in_account_currency - credit_in_account_currency" payment_dr_or_cr = "debit_in_account_currency - credit_in_account_currency"
invoice = 'Sales Invoice' if erpnext.get_party_account_type(party_type) == 'Receivable' else 'Purchase Invoice'
held_invoices = get_held_invoices(party_type, party) held_invoices = get_held_invoices(party_type, party)
invoice_list = frappe.db.sql(""" invoice_list = frappe.db.sql("""
@@ -661,7 +670,6 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters
group by voucher_type, voucher_no group by voucher_type, voucher_no
order by posting_date, name""".format( order by posting_date, name""".format(
dr_or_cr=dr_or_cr, dr_or_cr=dr_or_cr,
invoice = invoice,
condition=condition or "" condition=condition or ""
), { ), {
"party_type": party_type, "party_type": party_type,

View File

@@ -51,27 +51,25 @@ class CropCycle(Document):
self.create_task(disease_doc.treatment_task, self.name, start_date) self.create_task(disease_doc.treatment_task, self.name, start_date)
def create_project(self, period, crop_tasks): def create_project(self, period, crop_tasks):
project = frappe.new_doc("Project") project = frappe.get_doc({
project.update({ "doctype": "Project",
"project_name": self.title, "project_name": self.title,
"expected_start_date": self.start_date, "expected_start_date": self.start_date,
"expected_end_date": add_days(self.start_date, period - 1) "expected_end_date": add_days(self.start_date, period - 1)
}) }).insert()
project.insert()
return project.name return project.name
def create_task(self, crop_tasks, project_name, start_date): def create_task(self, crop_tasks, project_name, start_date):
for crop_task in crop_tasks: for crop_task in crop_tasks:
task = frappe.new_doc("Task") frappe.get_doc({
task.update({ "doctype": "Task",
"subject": crop_task.get("task_name"), "subject": crop_task.get("task_name"),
"priority": crop_task.get("priority"), "priority": crop_task.get("priority"),
"project": project_name, "project": project_name,
"exp_start_date": add_days(start_date, crop_task.get("start_day") - 1), "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) "exp_end_date": add_days(start_date, crop_task.get("end_day") - 1)
}) }).insert()
task.insert()
def reload_linked_analysis(self): def reload_linked_analysis(self):
linked_doctypes = ['Soil Texture', 'Soil Analysis', 'Plant Analysis'] linked_doctypes = ['Soil Texture', 'Soil Analysis', 'Plant Analysis']

View File

@@ -41,6 +41,39 @@ frappe.ui.form.on('Asset', {
}); });
}, },
setup: function(frm) {
frm.make_methods = {
'Asset Movement': () => {
frappe.call({
method: "erpnext.assets.doctype.asset.asset.make_asset_movement",
freeze: true,
args:{
"assets": [{ name: cur_frm.doc.name }]
},
callback: function (r) {
if (r.message) {
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
}
});
},
}
frm.set_query("purchase_receipt", (doc) => {
return {
query: "erpnext.controllers.queries.get_purchase_receipts",
filters: { item_code: doc.item_code }
}
});
frm.set_query("purchase_invoice", (doc) => {
return {
query: "erpnext.controllers.queries.get_purchase_invoices",
filters: { item_code: doc.item_code }
}
});
},
refresh: function(frm) { refresh: function(frm) {
frappe.ui.form.trigger("Asset", "is_existing_asset"); frappe.ui.form.trigger("Asset", "is_existing_asset");
frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1); frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1);
@@ -78,11 +111,6 @@ frappe.ui.form.on('Asset', {
}); });
} }
if (frm.doc.status=='Submitted' && !frm.doc.is_existing_asset && !frm.doc.purchase_invoice) {
frm.add_custom_button(__("Purchase Invoice"), function() {
frm.trigger("make_purchase_invoice");
}, __('Create'));
}
if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) { if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) {
frm.add_custom_button(__("Asset Maintenance"), function() { frm.add_custom_button(__("Asset Maintenance"), function() {
frm.trigger("create_asset_maintenance"); frm.trigger("create_asset_maintenance");
@@ -104,11 +132,36 @@ frappe.ui.form.on('Asset', {
frm.trigger("setup_chart"); frm.trigger("setup_chart");
} }
frm.trigger("toggle_reference_doc");
if (frm.doc.docstatus == 0) { if (frm.doc.docstatus == 0) {
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation); frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
} }
}, },
toggle_reference_doc: function(frm) {
if (frm.doc.purchase_receipt && frm.doc.purchase_invoice && frm.doc.docstatus === 1) {
frm.set_df_property('purchase_invoice', 'read_only', 1);
frm.set_df_property('purchase_receipt', 'read_only', 1);
}
else if (frm.doc.purchase_receipt) {
// if purchase receipt link is set then set PI disabled
frm.toggle_reqd('purchase_invoice', 0);
frm.set_df_property('purchase_invoice', 'read_only', 1);
}
else if (frm.doc.purchase_invoice) {
// if purchase invoice link is set then set PR disabled
frm.toggle_reqd('purchase_receipt', 0);
frm.set_df_property('purchase_receipt', 'read_only', 1);
}
else {
frm.toggle_reqd('purchase_receipt', 1);
frm.set_df_property('purchase_receipt', 'read_only', 0);
frm.toggle_reqd('purchase_invoice', 1);
frm.set_df_property('purchase_invoice', 'read_only', 0);
}
},
make_journal_entry: function(frm) { make_journal_entry: function(frm) {
frappe.call({ frappe.call({
method: "erpnext.assets.doctype.asset.asset.make_journal_entry", method: "erpnext.assets.doctype.asset.asset.make_journal_entry",
@@ -176,21 +229,25 @@ frappe.ui.form.on('Asset', {
item_code: function(frm) { item_code: function(frm) {
if(frm.doc.item_code) { if(frm.doc.item_code) {
frappe.call({ frm.trigger('set_finance_book');
method: "erpnext.assets.doctype.asset.asset.get_item_details",
args: {
item_code: frm.doc.item_code,
asset_category: frm.doc.asset_category
},
callback: function(r, rt) {
if(r.message) {
frm.set_value('finance_books', r.message);
}
}
})
} }
}, },
set_finance_book: function(frm) {
frappe.call({
method: "erpnext.assets.doctype.asset.asset.get_item_details",
args: {
item_code: frm.doc.item_code,
asset_category: frm.doc.asset_category
},
callback: function(r, rt) {
if(r.message) {
frm.set_value('finance_books', r.message);
}
}
})
},
available_for_use_date: function(frm) { available_for_use_date: function(frm) {
$.each(frm.doc.finance_books || [], function(i, d) { $.each(frm.doc.finance_books || [], function(i, d) {
if(!d.depreciation_start_date) d.depreciation_start_date = frm.doc.available_for_use_date; if(!d.depreciation_start_date) d.depreciation_start_date = frm.doc.available_for_use_date;
@@ -203,33 +260,18 @@ frappe.ui.form.on('Asset', {
}, },
opening_accumulated_depreciation: function(frm) { opening_accumulated_depreciation: function(frm) {
erpnext.asset.set_accululated_depreciation(frm); erpnext.asset.set_accumulated_depreciation(frm);
}, },
make_schedules_editable: function(frm) { make_schedules_editable: function(frm) {
var is_editable = frm.doc.finance_books.filter(d => d.depreciation_method == "Manual").length > 0 if (frm.doc.finance_books) {
? true : false; var is_editable = frm.doc.finance_books.filter(d => d.depreciation_method == "Manual").length > 0
? true : false;
frm.toggle_enable("schedules", is_editable); frm.toggle_enable("schedules", is_editable);
frm.fields_dict["schedules"].grid.toggle_enable("schedule_date", is_editable); frm.fields_dict["schedules"].grid.toggle_enable("schedule_date", is_editable);
frm.fields_dict["schedules"].grid.toggle_enable("depreciation_amount", is_editable); frm.fields_dict["schedules"].grid.toggle_enable("depreciation_amount", is_editable);
}, }
make_purchase_invoice: function(frm) {
frappe.call({
args: {
"asset": frm.doc.name,
"item_code": frm.doc.item_code,
"gross_purchase_amount": frm.doc.gross_purchase_amount,
"company": frm.doc.company,
"posting_date": frm.doc.purchase_date
},
method: "erpnext.assets.doctype.asset.asset.make_purchase_invoice",
callback: function(r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
}
})
}, },
make_sales_invoice: function(frm) { make_sales_invoice: function(frm) {
@@ -282,17 +324,6 @@ frappe.ui.form.on('Asset', {
}, },
calculate_depreciation: function(frm) { calculate_depreciation: function(frm) {
frappe.db.get_value("Asset Settings", {'name':"Asset Settings"}, 'schedule_based_on_fiscal_year', (data) => {
if (data.schedule_based_on_fiscal_year == 1) {
frm.set_df_property("depreciation_method", "options", "\nStraight Line\nManual");
frm.toggle_reqd("available_for_use_date", true);
frm.toggle_display("frequency_of_depreciation", false);
frappe.db.get_value("Fiscal Year", {'name': frappe.sys_defaults.fiscal_year}, "year_end_date", (data) => {
frm.set_value("next_depreciation_date", data.year_end_date);
})
}
})
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation); frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
}, },
@@ -302,6 +333,65 @@ frappe.ui.form.on('Asset', {
}) })
}, },
purchase_receipt: function(frm) {
frm.trigger('toggle_reference_doc');
if (frm.doc.purchase_receipt) {
if (frm.doc.item_code) {
frappe.db.get_doc('Purchase Receipt', frm.doc.purchase_receipt).then(pr_doc => {
frm.set_value('company', pr_doc.company);
frm.set_value('purchase_date', pr_doc.posting_date);
const item = pr_doc.items.find(item => item.item_code === frm.doc.item_code);
if (!item) {
frm.set_value('purchase_receipt', '');
frappe.msgprint({
title: __('Invalid Purchase Receipt'),
message: __("The selected Purchase Receipt doesn't contains selected Asset Item."),
indicator: 'red'
});
}
frm.set_value('gross_purchase_amount', item.base_net_rate);
frm.set_value('location', item.asset_location);
});
} else {
frm.set_value('purchase_receipt', '');
frappe.msgprint({
title: __('Not Allowed'),
message: __("Please select Item Code first")
});
}
}
},
purchase_invoice: function(frm) {
frm.trigger('toggle_reference_doc');
if (frm.doc.purchase_invoice) {
if (frm.doc.item_code) {
frappe.db.get_doc('Purchase Invoice', frm.doc.purchase_invoice).then(pi_doc => {
frm.set_value('company', pi_doc.company);
frm.set_value('purchase_date', pi_doc.posting_date);
const item = pi_doc.items.find(item => item.item_code === frm.doc.item_code);
if (!item) {
frm.set_value('purchase_invoice', '');
frappe.msgprint({
title: __('Invalid Purchase Invoice'),
message: __("The selected Purchase Invoice doesn't contains selected Asset Item."),
indicator: 'red'
});
}
frm.set_value('gross_purchase_amount', item.base_net_rate);
frm.set_value('location', item.asset_location);
});
} else {
frm.set_value('purchase_invoice', '');
frappe.msgprint({
title: __('Not Allowed'),
message: __("Please select Item Code first")
});
}
}
},
set_depreciation_rate: function(frm, row) { set_depreciation_rate: function(frm, row) {
if (row.total_number_of_depreciations && row.frequency_of_depreciation if (row.total_number_of_depreciations && row.frequency_of_depreciation
&& row.expected_value_after_useful_life) { && row.expected_value_after_useful_life) {
@@ -371,12 +461,12 @@ frappe.ui.form.on('Depreciation Schedule', {
}, },
depreciation_amount: function(frm, cdt, cdn) { depreciation_amount: function(frm, cdt, cdn) {
erpnext.asset.set_accululated_depreciation(frm); erpnext.asset.set_accumulated_depreciation(frm);
} }
}) })
erpnext.asset.set_accululated_depreciation = function(frm) { erpnext.asset.set_accumulated_depreciation = function(frm) {
if(frm.doc.depreciation_method != "Manual") return; if(frm.doc.depreciation_method != "Manual") return;
var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation); var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation);
@@ -415,92 +505,19 @@ erpnext.asset.restore_asset = function(frm) {
}) })
}; };
erpnext.asset.transfer_asset = function(frm) { erpnext.asset.transfer_asset = function() {
var dialog = new frappe.ui.Dialog({ frappe.call({
title: __("Transfer Asset"), method: "erpnext.assets.doctype.asset.asset.make_asset_movement",
fields: [ freeze: true,
{ args:{
"label": __("Target Location"), "assets": [{ name: cur_frm.doc.name }],
"fieldname": "target_location", "purpose": "Transfer"
"fieldtype": "Link", },
"options": "Location", callback: function (r) {
"get_query": function () { if (r.message) {
return { var doc = frappe.model.sync(r.message)[0];
filters: [ frappe.set_route("Form", doc.doctype, doc.name);
["Location", "is_group", "=", 0]
]
}
},
"reqd": 1
},
{
"label": __("Select Serial No"),
"fieldname": "serial_nos",
"fieldtype": "Link",
"options": "Serial No",
"get_query": function () {
return {
filters: {
'asset': frm.doc.name
}
}
},
"onchange": function() {
let val = this.get_value();
if (val) {
let serial_nos = dialog.get_value("serial_no") || val;
if (serial_nos) {
serial_nos = serial_nos.split('\n');
serial_nos.push(val);
const unique_sn = serial_nos.filter(function(elem, index, self) {
return index === self.indexOf(elem);
});
dialog.set_value("serial_no", unique_sn.join('\n'));
dialog.set_value("serial_nos", "");
}
}
}
},
{
"label": __("Serial No"),
"fieldname": "serial_no",
"read_only": 1,
"fieldtype": "Small Text"
},
{
"label": __("Date"),
"fieldname": "transfer_date",
"fieldtype": "Datetime",
"reqd": 1,
"default": frappe.datetime.now_datetime()
} }
] }
}); });
dialog.set_primary_action(__("Transfer"), function() {
var args = dialog.get_values();
if(!args) return;
dialog.hide();
return frappe.call({
type: "GET",
method: "erpnext.assets.doctype.asset.asset.transfer_asset",
args: {
args: {
"asset": frm.doc.name,
"transaction_date": args.transfer_date,
"source_location": frm.doc.location,
"target_location": args.target_location,
"serial_no": args.serial_no,
"company": frm.doc.company
}
},
freeze: true,
callback: function(r) {
cur_frm.reload_doc();
}
})
});
dialog.show();
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe, erpnext, math, json import frappe, erpnext, math, json
from frappe import _ from frappe import _
from six import string_types 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 frappe.model.document import Document
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.assets.doctype.asset.depreciation \ from erpnext.assets.doctype.asset.depreciation \
@@ -18,6 +18,7 @@ from erpnext.controllers.accounts_controller import AccountsController
class Asset(AccountsController): class Asset(AccountsController):
def validate(self): def validate(self):
self.validate_asset_values() self.validate_asset_values()
self.validate_asset_and_reference()
self.validate_item() self.validate_item()
self.set_missing_values() self.set_missing_values()
self.prepare_depreciation_data() self.prepare_depreciation_data()
@@ -29,9 +30,12 @@ class Asset(AccountsController):
def on_submit(self): def on_submit(self):
self.validate_in_use_date() self.validate_in_use_date()
self.set_status() self.set_status()
self.update_stock_movement() self.make_asset_movement()
if not self.booked_fixed_asset and not is_cwip_accounting_disabled(): if not self.booked_fixed_asset and is_cwip_accounting_enabled(self.asset_category):
self.make_gl_entries() self.make_gl_entries()
def before_cancel(self):
self.cancel_auto_gen_movement()
def on_cancel(self): def on_cancel(self):
self.validate_cancellation() self.validate_cancellation()
@@ -39,6 +43,18 @@ class Asset(AccountsController):
self.set_status() self.set_status()
delete_gl_entries(voucher_type='Asset', voucher_no=self.name) delete_gl_entries(voucher_type='Asset', voucher_no=self.name)
self.db_set('booked_fixed_asset', 0) self.db_set('booked_fixed_asset', 0)
def validate_asset_and_reference(self):
if self.purchase_invoice or self.purchase_receipt:
reference_doc = 'Purchase Invoice' if self.purchase_invoice else 'Purchase Receipt'
reference_name = self.purchase_invoice or self.purchase_receipt
reference_doc = frappe.get_doc(reference_doc, reference_name)
if reference_doc.get('company') != self.company:
frappe.throw(_("Company of asset {0} and purchase document {1} doesn't matches.").format(self.name, reference_doc.get('name')))
if self.is_existing_asset and self.purchase_invoice:
frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name))
def prepare_depreciation_data(self): def prepare_depreciation_data(self):
if self.calculate_depreciation: if self.calculate_depreciation:
@@ -76,10 +92,13 @@ class Asset(AccountsController):
self.set('finance_books', finance_books) self.set('finance_books', finance_books)
def validate_asset_values(self): def validate_asset_values(self):
if not self.asset_category:
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
if not flt(self.gross_purchase_amount): if not flt(self.gross_purchase_amount):
frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError) frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError)
if not is_cwip_accounting_disabled(): if is_cwip_accounting_enabled(self.asset_category):
if not self.is_existing_asset and not (self.purchase_receipt or self.purchase_invoice): if not self.is_existing_asset and not (self.purchase_receipt or self.purchase_invoice):
frappe.throw(_("Please create purchase receipt or purchase invoice for the item {0}"). frappe.throw(_("Please create purchase receipt or purchase invoice for the item {0}").
format(self.item_code)) format(self.item_code))
@@ -105,6 +124,38 @@ class Asset(AccountsController):
if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date): if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date):
frappe.throw(_("Available-for-use Date should be after purchase date")) frappe.throw(_("Available-for-use Date should be after purchase date"))
def cancel_auto_gen_movement(self):
movements = frappe.db.sql(
"""SELECT asm.name, asm.docstatus
FROM `tabAsset Movement` asm, `tabAsset Movement Item` asm_item
WHERE asm_item.parent=asm.name and asm_item.asset=%s and asm.docstatus=1""", self.name, as_dict=1)
if len(movements) > 1:
frappe.throw(_('Asset has multiple Asset Movement Entries which has to be \
cancelled manually to cancel this asset.'))
movement = frappe.get_doc('Asset Movement', movements[0].get('name'))
movement.flags.ignore_validate = True
movement.cancel()
def make_asset_movement(self):
reference_doctype = 'Purchase Receipt' if self.purchase_receipt else 'Purchase Invoice'
reference_docname = self.purchase_receipt or self.purchase_invoice
assets = [{
'asset': self.name,
'asset_name': self.asset_name,
'target_location': self.location,
'to_employee': self.custodian
}]
asset_movement = frappe.get_doc({
'doctype': 'Asset Movement',
'assets': assets,
'purpose': 'Receipt',
'company': self.company,
'transaction_date': getdate(nowdate()),
'reference_doctype': reference_doctype,
'reference_name': reference_docname
}).insert()
asset_movement.submit()
def set_depreciation_rate(self): def set_depreciation_rate(self):
for d in self.get("finance_books"): for d in self.get("finance_books"):
d.rate_of_depreciation = flt(self.get_depreciation_rate(d, on_validate=True), d.rate_of_depreciation = flt(self.get_depreciation_rate(d, on_validate=True),
@@ -145,19 +196,31 @@ class Asset(AccountsController):
schedule_date = add_months(d.depreciation_start_date, schedule_date = add_months(d.depreciation_start_date,
n * cint(d.frequency_of_depreciation)) 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 # For first row
if has_pro_rata and n==0: 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) 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 # For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
to_date = add_months(self.available_for_use_date, to_date = add_months(self.available_for_use_date,
n * cint(d.frequency_of_depreciation)) 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) depreciation_amount, schedule_date, to_date)
monthly_schedule_date = add_months(schedule_date, 1)
schedule_date = add_days(schedule_date, days) schedule_date = add_days(schedule_date, days)
last_schedule_date = schedule_date
if not depreciation_amount: continue if not depreciation_amount: continue
value_after_depreciation -= flt(depreciation_amount, value_after_depreciation -= flt(depreciation_amount,
@@ -171,13 +234,50 @@ class Asset(AccountsController):
skip_row = True skip_row = True
if depreciation_amount > 0: if depreciation_amount > 0:
self.append("schedules", { # With monthly depreciation, each depreciation is divided by months remaining until next date
"schedule_date": schedule_date, if self.allow_monthly_depreciation:
"depreciation_amount": depreciation_amount, # month range is 1 to 12
"depreciation_method": d.depreciation_method, # In pro rata case, for first and last depreciation, month range would be different
"finance_book": d.finance_book, month_range = months \
"finance_book_id": d.idx 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): def check_is_pro_rata(self, row):
has_pro_rata = False has_pro_rata = False
@@ -196,7 +296,9 @@ class Asset(AccountsController):
.format(row.idx)) .format(row.idx))
if not row.depreciation_start_date: if not row.depreciation_start_date:
frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx)) if not self.available_for_use_date:
frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx))
row.depreciation_start_date = self.available_for_use_date
if not self.is_existing_asset: if not self.is_existing_asset:
self.opening_accumulated_depreciation = 0 self.opening_accumulated_depreciation = 0
@@ -345,22 +447,13 @@ class Asset(AccountsController):
if d.finance_book == self.default_finance_book: if d.finance_book == self.default_finance_book:
return cint(d.idx) - 1 return cint(d.idx) - 1
def update_stock_movement(self):
asset_movement = frappe.db.get_value('Asset Movement',
{'asset': self.name, 'reference_name': self.purchase_receipt, 'docstatus': 0}, 'name')
if asset_movement:
doc = frappe.get_doc('Asset Movement', asset_movement)
doc.naming_series = 'ACC-ASM-.YYYY.-'
doc.submit()
def make_gl_entries(self): def make_gl_entries(self):
gl_entries = [] gl_entries = []
if ((self.purchase_receipt or (self.purchase_invoice and if ((self.purchase_receipt \
frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock'))) or (self.purchase_invoice and frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock')))
and self.purchase_receipt_amount and self.available_for_use_date <= nowdate()): and self.purchase_receipt_amount and self.available_for_use_date <= nowdate()):
fixed_aseet_account = get_asset_category_account(self.name, 'fixed_asset_account', fixed_asset_account = get_asset_category_account('fixed_asset_account', asset=self.name,
asset_category = self.asset_category, company = self.company) asset_category = self.asset_category, company = self.company)
cwip_account = get_asset_account("capital_work_in_progress_account", cwip_account = get_asset_account("capital_work_in_progress_account",
@@ -368,7 +461,7 @@ class Asset(AccountsController):
gl_entries.append(self.get_gl_dict({ gl_entries.append(self.get_gl_dict({
"account": cwip_account, "account": cwip_account,
"against": fixed_aseet_account, "against": fixed_asset_account,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"), "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"posting_date": self.available_for_use_date, "posting_date": self.available_for_use_date,
"credit": self.purchase_receipt_amount, "credit": self.purchase_receipt_amount,
@@ -377,7 +470,7 @@ class Asset(AccountsController):
})) }))
gl_entries.append(self.get_gl_dict({ gl_entries.append(self.get_gl_dict({
"account": fixed_aseet_account, "account": fixed_asset_account,
"against": cwip_account, "against": cwip_account,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"), "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"posting_date": self.available_for_use_date, "posting_date": self.available_for_use_date,
@@ -424,7 +517,7 @@ def update_maintenance_status():
asset.set_status('Out of Order') asset.set_status('Out of Order')
def make_post_gl_entry(): def make_post_gl_entry():
if is_cwip_accounting_disabled(): if not is_cwip_accounting_enabled(self.asset_category):
return return
assets = frappe.db.sql_list(""" select name from `tabAsset` assets = frappe.db.sql_list(""" select name from `tabAsset`
@@ -438,25 +531,6 @@ def get_asset_naming_series():
meta = frappe.get_meta('Asset') meta = frappe.get_meta('Asset')
return meta.get_field("naming_series").options return meta.get_field("naming_series").options
@frappe.whitelist()
def make_purchase_invoice(asset, item_code, gross_purchase_amount, company, posting_date):
pi = frappe.new_doc("Purchase Invoice")
pi.company = company
pi.currency = frappe.get_cached_value('Company', company, "default_currency")
pi.set_posting_time = 1
pi.posting_date = posting_date
pi.append("items", {
"item_code": item_code,
"is_fixed_asset": 1,
"asset": asset,
"expense_account": get_asset_category_account(asset, 'fixed_asset_account'),
"qty": 1,
"price_list_rate": gross_purchase_amount,
"rate": gross_purchase_amount
})
pi.set_missing_values()
return pi
@frappe.whitelist() @frappe.whitelist()
def make_sales_invoice(asset, item_code, company, serial_no=None): def make_sales_invoice(asset, item_code, company, serial_no=None):
si = frappe.new_doc("Sales Invoice") si = frappe.new_doc("Sales Invoice")
@@ -531,7 +605,7 @@ def get_item_details(item_code, asset_category):
def get_asset_account(account_name, asset=None, asset_category=None, company=None): def get_asset_account(account_name, asset=None, asset_category=None, company=None):
account = None account = None
if asset: if asset:
account = get_asset_category_account(asset, account_name, account = get_asset_category_account(account_name, asset=asset,
asset_category = asset_category, company = company) asset_category = asset_category, company = company)
if not account: if not account:
@@ -574,17 +648,43 @@ def make_journal_entry(asset_name):
return je return je
def is_cwip_accounting_disabled(): @frappe.whitelist()
return cint(frappe.db.get_single_value("Asset Settings", "disable_cwip_accounting")) def make_asset_movement(assets, purpose=None):
import json
from six import string_types
if isinstance(assets, string_types):
assets = json.loads(assets)
if len(assets) == 0:
frappe.throw(_('Atleast one asset has to be selected.'))
asset_movement = frappe.new_doc("Asset Movement")
asset_movement.quantity = len(assets)
for asset in assets:
asset = frappe.get_doc('Asset', asset.get('name'))
asset_movement.company = asset.get('company')
asset_movement.append("assets", {
'asset': asset.get('name'),
'source_location': asset.get('location'),
'from_employee': asset.get('custodian')
})
if asset_movement.get('assets'):
return asset_movement.as_dict()
def is_cwip_accounting_enabled(asset_category):
return cint(frappe.db.get_value("Asset Category", asset_category, "enable_cwip_accounting"))
def get_pro_rata_amt(row, depreciation_amount, from_date, to_date): def get_pro_rata_amt(row, depreciation_amount, from_date, to_date):
days = date_diff(to_date, from_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) 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): def get_total_days(date, frequency):
period_start_date = add_months(date, period_start_date = add_months(date,
cint(frequency) * -1) cint(frequency) * -1)
return date_diff(date, period_start_date) return date_diff(date, period_start_date)

View File

@@ -30,8 +30,24 @@ frappe.listview_settings['Asset'] = {
} else if (doc.status === "Draft") { } else if (doc.status === "Draft") {
return [__("Draft"), "red", "status,=,Draft"]; return [__("Draft"), "red", "status,=,Draft"];
} }
},
onload: function(me) {
me.page.add_action_item('Make Asset Movement', function() {
const assets = me.get_checked_items();
frappe.call({
method: "erpnext.assets.doctype.asset.asset.make_asset_movement",
freeze: true,
args:{
"assets": assets
},
callback: function (r) {
if (r.message) {
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
}
});
});
}, },
} }

View File

@@ -7,14 +7,13 @@ import frappe
import unittest import unittest
from frappe.utils import cstr, nowdate, getdate, flt, get_last_day, add_days, add_months from frappe.utils import cstr, nowdate, getdate, flt, get_last_day, add_days, add_months
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries, scrap_asset, restore_asset from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries, scrap_asset, restore_asset
from erpnext.assets.doctype.asset.asset import make_sales_invoice, make_purchase_invoice from erpnext.assets.doctype.asset.asset import make_sales_invoice
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as make_invoice from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as make_invoice
class TestAsset(unittest.TestCase): class TestAsset(unittest.TestCase):
def setUp(self): def setUp(self):
set_depreciation_settings_in_company() set_depreciation_settings_in_company()
remove_prorated_depreciation_schedule()
create_asset_data() create_asset_data()
frappe.db.sql("delete from `tabTax Rule`") frappe.db.sql("delete from `tabTax Rule`")
@@ -40,15 +39,15 @@ class TestAsset(unittest.TestCase):
}) })
asset.submit() asset.submit()
pi = make_purchase_invoice(asset.name, asset.item_code, asset.gross_purchase_amount, pi = make_invoice(pr.name)
asset.company, asset.purchase_date)
pi.supplier = "_Test Supplier" pi.supplier = "_Test Supplier"
pi.insert() pi.insert()
pi.submit() pi.submit()
asset.load_from_db() asset.load_from_db()
self.assertEqual(asset.supplier, "_Test Supplier") self.assertEqual(asset.supplier, "_Test Supplier")
self.assertEqual(asset.purchase_date, getdate(purchase_date)) self.assertEqual(asset.purchase_date, getdate(purchase_date))
self.assertEqual(asset.purchase_invoice, pi.name) # Asset won't have reference to PI when purchased through PR
self.assertEqual(asset.purchase_receipt, pr.name)
expected_gle = ( expected_gle = (
("Asset Received But Not Billed - _TC", 100000.0, 0.0), ("Asset Received But Not Billed - _TC", 100000.0, 0.0),
@@ -61,20 +60,23 @@ class TestAsset(unittest.TestCase):
self.assertEqual(gle, expected_gle) self.assertEqual(gle, expected_gle)
pi.cancel() pi.cancel()
asset.cancel()
asset.load_from_db() asset.load_from_db()
self.assertEqual(asset.supplier, None) pr.load_from_db()
self.assertEqual(asset.purchase_invoice, None) pr.cancel()
self.assertEqual(asset.docstatus, 2)
self.assertFalse(frappe.db.get_value("GL Entry", self.assertFalse(frappe.db.get_value("GL Entry",
{"voucher_type": "Purchase Invoice", "voucher_no": pi.name})) {"voucher_type": "Purchase Invoice", "voucher_no": pi.name}))
def test_is_fixed_asset_set(self): def test_is_fixed_asset_set(self):
asset = create_asset(is_existing_asset = 1)
doc = frappe.new_doc('Purchase Invoice') doc = frappe.new_doc('Purchase Invoice')
doc.supplier = '_Test Supplier' doc.supplier = '_Test Supplier'
doc.append('items', { doc.append('items', {
'item_code': 'Macbook Pro', 'item_code': 'Macbook Pro',
'qty': 1 'qty': 1,
'asset': asset.name
}) })
doc.set_missing_values() doc.set_missing_values()
@@ -200,7 +202,6 @@ class TestAsset(unittest.TestCase):
self.assertEqual(schedules, expected_schedules) self.assertEqual(schedules, expected_schedules)
def test_schedule_for_prorated_straight_line_method(self): def test_schedule_for_prorated_straight_line_method(self):
set_prorated_depreciation_schedule()
pr = make_purchase_receipt(item_code="Macbook Pro", pr = make_purchase_receipt(item_code="Macbook Pro",
qty=1, rate=100000.0, location="Test Location") qty=1, rate=100000.0, location="Test Location")
@@ -233,8 +234,6 @@ class TestAsset(unittest.TestCase):
self.assertEqual(schedules, expected_schedules) self.assertEqual(schedules, expected_schedules)
remove_prorated_depreciation_schedule()
def test_depreciation(self): def test_depreciation(self):
pr = make_purchase_receipt(item_code="Macbook Pro", pr = make_purchase_receipt(item_code="Macbook Pro",
qty=1, rate=100000.0, location="Test Location") qty=1, rate=100000.0, location="Test Location")
@@ -484,9 +483,6 @@ class TestAsset(unittest.TestCase):
self.assertTrue(asset.finance_books[0].expected_value_after_useful_life >= asset_value_after_full_schedule) self.assertTrue(asset.finance_books[0].expected_value_after_useful_life >= asset_value_after_full_schedule)
def test_cwip_accounting(self): def test_cwip_accounting(self):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as make_purchase_invoice_from_pr)
pr = make_purchase_receipt(item_code="Macbook Pro", pr = make_purchase_receipt(item_code="Macbook Pro",
qty=1, rate=5000, do_not_submit=True, location="Test Location") qty=1, rate=5000, do_not_submit=True, location="Test Location")
@@ -515,13 +511,13 @@ class TestAsset(unittest.TestCase):
("CWIP Account - _TC", 5250.0, 0.0) ("CWIP Account - _TC", 5250.0, 0.0)
) )
gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` pr_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
where voucher_type='Purchase Receipt' and voucher_no = %s where voucher_type='Purchase Receipt' and voucher_no = %s
order by account""", pr.name) order by account""", pr.name)
self.assertEqual(gle, expected_gle) self.assertEqual(pr_gle, expected_gle)
pi = make_purchase_invoice_from_pr(pr.name) pi = make_invoice(pr.name)
pi.submit() pi.submit()
expected_gle = ( expected_gle = (
@@ -532,11 +528,11 @@ class TestAsset(unittest.TestCase):
("Expenses Included In Asset Valuation - _TC", 0.0, 250.0), ("Expenses Included In Asset Valuation - _TC", 0.0, 250.0),
) )
gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` pi_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
where voucher_type='Purchase Invoice' and voucher_no = %s where voucher_type='Purchase Invoice' and voucher_no = %s
order by account""", pi.name) order by account""", pi.name)
self.assertEqual(gle, expected_gle) self.assertEqual(pi_gle, expected_gle)
asset = frappe.db.get_value('Asset', asset = frappe.db.get_value('Asset',
{'purchase_receipt': pr.name, 'docstatus': 0}, 'name') {'purchase_receipt': pr.name, 'docstatus': 0}, 'name')
@@ -565,6 +561,7 @@ class TestAsset(unittest.TestCase):
where voucher_type='Asset' and voucher_no = %s where voucher_type='Asset' and voucher_no = %s
order by account""", asset_doc.name) order by account""", asset_doc.name)
self.assertEqual(gle, expected_gle) self.assertEqual(gle, expected_gle)
def test_expense_head(self): def test_expense_head(self):
@@ -575,7 +572,6 @@ class TestAsset(unittest.TestCase):
self.assertEquals('Asset Received But Not Billed - _TC', doc.items[0].expense_account) self.assertEquals('Asset Received But Not Billed - _TC', doc.items[0].expense_account)
def create_asset_data(): def create_asset_data():
if not frappe.db.exists("Asset Category", "Computers"): if not frappe.db.exists("Asset Category", "Computers"):
create_asset_category() create_asset_category()
@@ -596,15 +592,15 @@ def create_asset(**args):
asset = frappe.get_doc({ asset = frappe.get_doc({
"doctype": "Asset", "doctype": "Asset",
"asset_name": "Macbook Pro 1", "asset_name": args.asset_name or "Macbook Pro 1",
"asset_category": "Computers", "asset_category": "Computers",
"item_code": "Macbook Pro", "item_code": args.item_code or "Macbook Pro",
"company": "_Test Company", "company": args.company or"_Test Company",
"purchase_date": "2015-01-01", "purchase_date": "2015-01-01",
"calculate_depreciation": 0, "calculate_depreciation": 0,
"gross_purchase_amount": 100000, "gross_purchase_amount": 100000,
"expected_value_after_useful_life": 10000, "expected_value_after_useful_life": 10000,
"warehouse": "_Test Warehouse - _TC", "warehouse": args.warehouse or "_Test Warehouse - _TC",
"available_for_use_date": "2020-06-06", "available_for_use_date": "2020-06-06",
"location": "Test Location", "location": "Test Location",
"asset_owner": "Company", "asset_owner": "Company",
@@ -616,6 +612,9 @@ def create_asset(**args):
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
if args.submit:
asset.submit()
return asset return asset
def create_asset_category(): def create_asset_category():
@@ -623,6 +622,7 @@ def create_asset_category():
asset_category.asset_category_name = "Computers" asset_category.asset_category_name = "Computers"
asset_category.total_number_of_depreciations = 3 asset_category.total_number_of_depreciations = 3
asset_category.frequency_of_depreciation = 3 asset_category.frequency_of_depreciation = 3
asset_category.enable_cwip_accounting = 1
asset_category.append("accounts", { asset_category.append("accounts", {
"company_name": "_Test Company", "company_name": "_Test Company",
"fixed_asset_account": "_Test Fixed Asset - _TC", "fixed_asset_account": "_Test Fixed Asset - _TC",
@@ -632,6 +632,8 @@ def create_asset_category():
asset_category.insert() asset_category.insert()
def create_fixed_asset_item(): def create_fixed_asset_item():
meta = frappe.get_meta('Asset')
naming_series = meta.get_field("naming_series").options.splitlines()[0] or 'ACC-ASS-.YYYY.-'
try: try:
frappe.get_doc({ frappe.get_doc({
"doctype": "Item", "doctype": "Item",
@@ -642,7 +644,9 @@ def create_fixed_asset_item():
"item_group": "All Item Groups", "item_group": "All Item Groups",
"stock_uom": "Nos", "stock_uom": "Nos",
"is_stock_item": 0, "is_stock_item": 0,
"is_fixed_asset": 1 "is_fixed_asset": 1,
"auto_create_assets": 1,
"asset_naming_series": naming_series
}).insert() }).insert()
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
@@ -656,19 +660,4 @@ def set_depreciation_settings_in_company():
company.save() company.save()
# Enable booking asset depreciation entry automatically # Enable booking asset depreciation entry automatically
frappe.db.set_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically", 1) frappe.db.set_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically", 1)
def remove_prorated_depreciation_schedule():
asset_settings = frappe.get_doc("Asset Settings", "Asset Settings")
asset_settings.schedule_based_on_fiscal_year = 0
asset_settings.save()
frappe.db.commit()
def set_prorated_depreciation_schedule():
asset_settings = frappe.get_doc("Asset Settings", "Asset Settings")
asset_settings.schedule_based_on_fiscal_year = 1
asset_settings.number_of_days_in_fiscal_year = 360
asset_settings.save()
frappe.db.commit()

View File

@@ -1,284 +1,115 @@
{ {
"allow_copy": 0, "allow_import": 1,
"allow_guest_to_view": 0, "allow_rename": 1,
"allow_import": 1, "autoname": "field:asset_category_name",
"allow_rename": 1, "creation": "2016-03-01 17:41:39.778765",
"autoname": "field:asset_category_name", "doctype": "DocType",
"beta": 0, "document_type": "Document",
"creation": "2016-03-01 17:41:39.778765", "engine": "InnoDB",
"custom": 0, "field_order": [
"docstatus": 0, "asset_category_name",
"doctype": "DocType", "column_break_3",
"document_type": "Document", "depreciation_options",
"editable_grid": 0, "enable_cwip_accounting",
"engine": "InnoDB", "finance_book_detail",
"finance_books",
"section_break_2",
"accounts"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "asset_category_name",
"allow_on_submit": 0, "fieldtype": "Data",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Asset Category Name",
"columns": 0, "reqd": 1,
"fieldname": "asset_category_name", "unique": 1
"fieldtype": "Data", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Asset Category Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_3",
"allow_on_submit": 0, "fieldtype": "Column Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "finance_book_detail",
"allow_on_submit": 0, "fieldtype": "Section Break",
"bold": 0, "label": "Finance Book Detail"
"collapsible": 0, },
"columns": 0,
"fieldname": "finance_book_detail",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Finance Book Detail",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "finance_books",
"allow_on_submit": 0, "fieldtype": "Table",
"bold": 0, "label": "Finance Books",
"collapsible": 0, "options": "Asset Finance Book"
"columns": 0, },
"fieldname": "finance_books",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Finance Books",
"length": 0,
"no_copy": 0,
"options": "Asset Finance Book",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "section_break_2",
"allow_on_submit": 0, "fieldtype": "Section Break",
"bold": 0, "label": "Accounts"
"collapsible": 0, },
"columns": 0,
"fieldname": "section_break_2",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Accounts",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "accounts",
"allow_on_submit": 0, "fieldtype": "Table",
"bold": 0, "label": "Accounts",
"collapsible": 0, "options": "Asset Category Account",
"columns": 0, "reqd": 1
"fieldname": "accounts", },
"fieldtype": "Table", {
"hidden": 0, "fieldname": "depreciation_options",
"ignore_user_permissions": 0, "fieldtype": "Section Break",
"ignore_xss_filter": 0, "label": "Depreciation Options"
"in_filter": 0, },
"in_global_search": 0, {
"in_list_view": 0, "default": "0",
"in_standard_filter": 0, "fieldname": "enable_cwip_accounting",
"label": "Accounts", "fieldtype": "Check",
"length": 0, "label": "Enable Capital Work in Progress Accounting"
"no_copy": 0,
"options": "Asset Category Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "modified": "2019-10-11 12:19:59.759136",
"hide_heading": 0, "modified_by": "Administrator",
"hide_toolbar": 0, "module": "Assets",
"idx": 0, "name": "Asset Category",
"image_view": 0, "owner": "Administrator",
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-05-12 14:56:04.116425",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Category",
"name_case": "",
"owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0, "create": 1,
"apply_user_permissions": 0, "delete": 1,
"cancel": 0, "email": 1,
"create": 1, "export": 1,
"delete": 1, "import": 1,
"email": 1, "print": 1,
"export": 1, "read": 1,
"if_owner": 0, "report": 1,
"import": 1, "role": "Accounts User",
"permlevel": 0, "share": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0, "create": 1,
"apply_user_permissions": 0, "delete": 1,
"cancel": 0, "email": 1,
"create": 1, "export": 1,
"delete": 1, "import": 1,
"email": 1, "print": 1,
"export": 1, "read": 1,
"if_owner": 0, "report": 1,
"import": 1, "role": "Accounts Manager",
"permlevel": 0, "share": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0, "create": 1,
"apply_user_permissions": 0, "delete": 1,
"cancel": 0, "email": 1,
"create": 1, "export": 1,
"delete": 1, "print": 1,
"email": 1, "read": 1,
"export": 1, "report": 1,
"if_owner": 0, "role": "Quality Manager",
"import": 0, "share": 1,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Quality Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0, "show_name_in_global_search": 1,
"read_only": 0, "sort_field": "modified",
"read_only_onload": 0, "sort_order": "DESC"
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0
} }

View File

@@ -10,14 +10,20 @@ from frappe.model.document import Document
class AssetCategory(Document): class AssetCategory(Document):
def validate(self): def validate(self):
self.validate_finance_books()
def validate_finance_books(self):
for d in self.finance_books: for d in self.finance_books:
for field in ("Total Number of Depreciations", "Frequency of Depreciation"): for field in ("Total Number of Depreciations", "Frequency of Depreciation"):
if cint(d.get(frappe.scrub(field)))<1: if cint(d.get(frappe.scrub(field)))<1:
frappe.throw(_("Row {0}: {1} must be greater than 0").format(d.idx, field), frappe.MandatoryError) frappe.throw(_("Row {0}: {1} must be greater than 0").format(d.idx, field), frappe.MandatoryError)
@frappe.whitelist() @frappe.whitelist()
def get_asset_category_account(asset, fieldname, account=None, asset_category = None, company = None): def get_asset_category_account(fieldname, item=None, asset=None, account=None, asset_category = None, company = None):
if not asset_category and company: if item and frappe.db.get_value("Item", item, "is_fixed_asset"):
asset_category = frappe.db.get_value("Item", item, ["asset_category"])
elif not asset_category or not company:
if account: if account:
if frappe.db.get_value("Account", account, "account_type") != "Fixed Asset": if frappe.db.get_value("Account", account, "account_type") != "Fixed Asset":
account=None account=None

View File

@@ -73,8 +73,10 @@ def create_asset_data():
'doctype': 'Location', 'doctype': 'Location',
'location_name': 'Test Location' 'location_name': 'Test Location'
}).insert() }).insert()
if not frappe.db.exists("Item", "Photocopier"): if not frappe.db.exists("Item", "Photocopier"):
meta = frappe.get_meta('Asset')
naming_series = meta.get_field("naming_series").options
frappe.get_doc({ frappe.get_doc({
"doctype": "Item", "doctype": "Item",
"item_code": "Photocopier", "item_code": "Photocopier",
@@ -83,7 +85,9 @@ def create_asset_data():
"company": "_Test Company", "company": "_Test Company",
"is_fixed_asset": 1, "is_fixed_asset": 1,
"is_stock_item": 0, "is_stock_item": 0,
"asset_category": "Equipment" "asset_category": "Equipment",
"auto_create_assets": 1,
"asset_naming_series": naming_series
}).insert() }).insert()
def create_maintenance_team(): def create_maintenance_team():

View File

@@ -2,27 +2,101 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Asset Movement', { frappe.ui.form.on('Asset Movement', {
select_serial_no: function(frm) { setup: (frm) => {
if (frm.doc.select_serial_no) { frm.set_query("to_employee", "assets", (doc) => {
let serial_no = frm.doc.serial_no
? frm.doc.serial_no + '\n' + frm.doc.select_serial_no : frm.doc.select_serial_no;
frm.set_value("serial_no", serial_no);
frm.set_value("quantity", serial_no.split('\n').length);
}
},
serial_no: function(frm) {
const qty = frm.doc.serial_no ? frm.doc.serial_no.split('\n').length : 0;
frm.set_value("quantity", qty);
},
setup: function(frm) {
frm.set_query("select_serial_no", function() {
return { return {
filters: { filters: {
"asset": frm.doc.asset company: doc.company
} }
}; };
})
frm.set_query("from_employee", "assets", (doc) => {
return {
filters: {
company: doc.company
}
};
})
frm.set_query("reference_name", (doc) => {
return {
filters: {
company: doc.company,
docstatus: 1
}
};
})
frm.set_query("reference_doctype", () => {
return {
filters: {
name: ["in", ["Purchase Receipt", "Purchase Invoice"]]
}
};
}),
frm.set_query("asset", "assets", () => {
return {
filters: {
status: ["not in", ["Draft"]]
}
}
})
},
onload: (frm) => {
frm.trigger('set_required_fields');
},
purpose: (frm) => {
frm.trigger('set_required_fields');
},
set_required_fields: (frm, cdt, cdn) => {
let fieldnames_to_be_altered;
if (frm.doc.purpose === 'Transfer') {
fieldnames_to_be_altered = {
target_location: { read_only: 0, reqd: 1 },
source_location: { read_only: 1, reqd: 1 },
from_employee: { read_only: 1, reqd: 0 },
to_employee: { read_only: 1, reqd: 0 }
};
}
else if (frm.doc.purpose === 'Receipt') {
fieldnames_to_be_altered = {
target_location: { read_only: 0, reqd: 1 },
source_location: { read_only: 1, reqd: 0 },
from_employee: { read_only: 0, reqd: 1 },
to_employee: { read_only: 1, reqd: 0 }
};
}
else if (frm.doc.purpose === 'Issue') {
fieldnames_to_be_altered = {
target_location: { read_only: 1, reqd: 0 },
source_location: { read_only: 1, reqd: 1 },
from_employee: { read_only: 1, reqd: 0 },
to_employee: { read_only: 0, reqd: 1 }
};
}
Object.keys(fieldnames_to_be_altered).forEach(fieldname => {
let property_to_be_altered = fieldnames_to_be_altered[fieldname];
Object.keys(property_to_be_altered).forEach(property => {
let value = property_to_be_altered[property];
frm.set_df_property(fieldname, property, value, cdn, 'assets');
});
}); });
frm.refresh_field('assets');
} }
}); });
frappe.ui.form.on('Asset Movement Item', {
asset: function(frm, cdt, cdn) {
// on manual entry of an asset auto sets their source location / employee
const asset_name = locals[cdt][cdn].asset;
if (asset_name){
frappe.db.get_doc('Asset', asset_name).then((asset_doc) => {
if(asset_doc.location) frappe.model.set_value(cdt, cdn, 'source_location', asset_doc.location);
if(asset_doc.custodian) frappe.model.set_value(cdt, cdn, 'from_employee', asset_doc.custodian);
}).catch((err) => {
console.log(err); // eslint-disable-line
});
}
}
});

View File

@@ -1,26 +1,19 @@
{ {
"allow_import": 1, "allow_import": 1,
"autoname": "naming_series:", "autoname": "format:ACC-ASM-{YYYY}-{#####}",
"creation": "2016-04-25 18:00:23.559973", "creation": "2016-04-25 18:00:23.559973",
"doctype": "DocType", "doctype": "DocType",
"engine": "InnoDB",
"field_order": [ "field_order": [
"naming_series",
"company", "company",
"purpose", "purpose",
"asset",
"transaction_date",
"column_break_4", "column_break_4",
"quantity", "transaction_date",
"select_serial_no", "section_break_10",
"serial_no", "assets",
"section_break_7",
"source_location",
"target_location",
"column_break_10",
"from_employee",
"to_employee",
"reference", "reference",
"reference_doctype", "reference_doctype",
"column_break_9",
"reference_name", "reference_name",
"amended_from" "amended_from"
], ],
@@ -36,23 +29,12 @@
"reqd": 1 "reqd": 1
}, },
{ {
"default": "Transfer",
"fieldname": "purpose", "fieldname": "purpose",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Purpose", "label": "Purpose",
"options": "\nIssue\nReceipt\nTransfer", "options": "\nIssue\nReceipt\nTransfer",
"reqd": 1 "reqd": 1
}, },
{
"fieldname": "asset",
"fieldtype": "Link",
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Asset",
"options": "Asset",
"reqd": 1
},
{ {
"fieldname": "transaction_date", "fieldname": "transaction_date",
"fieldtype": "Datetime", "fieldtype": "Datetime",
@@ -65,56 +47,7 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"fieldname": "quantity", "collapsible": 1,
"fieldtype": "Float",
"label": "Quantity"
},
{
"fieldname": "select_serial_no",
"fieldtype": "Link",
"label": "Select Serial No",
"options": "Serial No"
},
{
"fieldname": "serial_no",
"fieldtype": "Small Text",
"label": "Serial No"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break"
},
{
"fieldname": "source_location",
"fieldtype": "Link",
"label": "Source Location",
"options": "Location"
},
{
"fieldname": "target_location",
"fieldtype": "Link",
"label": "Target Location",
"options": "Location"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fieldname": "from_employee",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "From Employee",
"options": "Employee"
},
{
"fieldname": "to_employee",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "To Employee",
"options": "Employee"
},
{
"fieldname": "reference", "fieldname": "reference",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Reference" "label": "Reference"
@@ -122,18 +55,16 @@
{ {
"fieldname": "reference_doctype", "fieldname": "reference_doctype",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Reference DocType", "label": "Reference Document Type",
"no_copy": 1, "no_copy": 1,
"options": "DocType", "options": "DocType"
"read_only": 1
}, },
{ {
"fieldname": "reference_name", "fieldname": "reference_name",
"fieldtype": "Dynamic Link", "fieldtype": "Dynamic Link",
"label": "Reference Name", "label": "Reference Document Name",
"no_copy": 1, "no_copy": 1,
"options": "reference_doctype", "options": "reference_doctype"
"read_only": 1
}, },
{ {
"fieldname": "amended_from", "fieldname": "amended_from",
@@ -145,16 +76,23 @@
"read_only": 1 "read_only": 1
}, },
{ {
"default": "ACC-ASM-.YYYY.-", "fieldname": "section_break_10",
"fieldname": "naming_series", "fieldtype": "Section Break"
"fieldtype": "Select", },
"label": "Series", {
"options": "ACC-ASM-.YYYY.-", "fieldname": "assets",
"fieldtype": "Table",
"label": "Assets",
"options": "Asset Movement Item",
"reqd": 1 "reqd": 1
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"modified": "2019-09-16 16:27:53.887634", "modified": "2019-11-23 13:28:47.256935",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Movement", "name": "Asset Movement",

View File

@@ -5,101 +5,142 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from frappe.model.document import Document from frappe.model.document import Document
class AssetMovement(Document): class AssetMovement(Document):
def validate(self): def validate(self):
self.validate_asset() self.validate_asset()
self.validate_location() self.validate_location()
self.validate_employee()
def validate_asset(self): def validate_asset(self):
status, company = frappe.db.get_value("Asset", self.asset, ["status", "company"]) for d in self.assets:
if self.purpose == 'Transfer' and status in ("Draft", "Scrapped", "Sold"): status, company = frappe.db.get_value("Asset", d.asset, ["status", "company"])
frappe.throw(_("{0} asset cannot be transferred").format(status)) if self.purpose == 'Transfer' and status in ("Draft", "Scrapped", "Sold"):
frappe.throw(_("{0} asset cannot be transferred").format(status))
if company != self.company: if company != self.company:
frappe.throw(_("Asset {0} does not belong to company {1}").format(self.asset, self.company)) frappe.throw(_("Asset {0} does not belong to company {1}").format(d.asset, self.company))
if self.serial_no and len(get_serial_nos(self.serial_no)) != self.quantity: if not (d.source_location or d.target_location or d.from_employee or d.to_employee):
frappe.throw(_("Number of serial nos and quantity must be the same")) frappe.throw(_("Either location or employee must be required"))
if not(self.source_location or self.target_location or self.from_employee or self.to_employee):
frappe.throw(_("Either location or employee must be required"))
if (not self.serial_no and
frappe.db.get_value('Serial No', {'asset': self.asset}, 'name')):
frappe.throw(_("Serial no is required for the asset {0}").format(self.asset))
def validate_location(self): def validate_location(self):
if self.purpose in ['Transfer', 'Issue']: for d in self.assets:
if not self.serial_no and not (self.from_employee or self.to_employee): if self.purpose in ['Transfer', 'Issue']:
self.source_location = frappe.db.get_value("Asset", self.asset, "location") if not d.source_location:
d.source_location = frappe.db.get_value("Asset", d.asset, "location")
if self.purpose == 'Issue' and not (self.source_location or self.from_employee): if not d.source_location:
frappe.throw(_("Source Location is required for the asset {0}").format(self.asset)) frappe.throw(_("Source Location is required for the Asset {0}").format(d.asset))
if self.serial_no and self.source_location: if d.source_location:
s_nos = get_serial_nos(self.serial_no) current_location = frappe.db.get_value("Asset", d.asset, "location")
serial_nos = frappe.db.sql_list(""" select name from `tabSerial No` where location != '%s'
and name in (%s)""" %(self.source_location, ','.join(['%s'] * len(s_nos))), tuple(s_nos))
if serial_nos: if current_location != d.source_location:
frappe.throw(_("Serial nos {0} does not belongs to the location {1}"). frappe.throw(_("Asset {0} does not belongs to the location {1}").
format(','.join(serial_nos), self.source_location)) format(d.asset, d.source_location))
if self.purpose == 'Issue':
if d.target_location:
frappe.throw(_("Issuing cannot be done to a location. \
Please enter employee who has issued Asset {0}").format(d.asset), title="Incorrect Movement Purpose")
if not d.to_employee:
frappe.throw(_("Employee is required while issuing Asset {0}").format(d.asset))
if self.purpose == 'Transfer':
if d.to_employee:
frappe.throw(_("Transferring cannot be done to an Employee. \
Please enter location where Asset {0} has to be transferred").format(
d.asset), title="Incorrect Movement Purpose")
if not d.target_location:
frappe.throw(_("Target Location is required while transferring Asset {0}").format(d.asset))
if d.source_location == d.target_location:
frappe.throw(_("Source and Target Location cannot be same"))
if self.purpose == 'Receipt':
# only when asset is bought and first entry is made
if not d.source_location and not (d.target_location or d.to_employee):
frappe.throw(_("Target Location or To Employee is required while receiving Asset {0}").format(d.asset))
elif d.source_location:
# when asset is received from an employee
if d.target_location and not d.from_employee:
frappe.throw(_("From employee is required while receiving Asset {0} to a target location").format(d.asset))
if d.from_employee and not d.target_location:
frappe.throw(_("Target Location is required while receiving Asset {0} from an employee").format(d.asset))
if d.to_employee and d.target_location:
frappe.throw(_("Asset {0} cannot be received at a location and \
given to employee in a single movement").format(d.asset))
if self.source_location and self.source_location == self.target_location and self.purpose == 'Transfer': def validate_employee(self):
frappe.throw(_("Source and Target Location cannot be same")) for d in self.assets:
if d.from_employee:
current_custodian = frappe.db.get_value("Asset", d.asset, "custodian")
if self.purpose == 'Receipt' and not (self.target_location or self.to_employee): if current_custodian != d.from_employee:
frappe.throw(_("Target Location is required for the asset {0}").format(self.asset)) frappe.throw(_("Asset {0} does not belongs to the custodian {1}").
format(d.asset, d.from_employee))
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
frappe.throw(_("Employee {0} does not belongs to the company {1}").
format(d.to_employee, self.company))
def on_submit(self): def on_submit(self):
self.set_latest_location_in_asset() self.set_latest_location_in_asset()
def before_cancel(self):
self.validate_last_movement()
def on_cancel(self): def on_cancel(self):
self.set_latest_location_in_asset() self.set_latest_location_in_asset()
def validate_last_movement(self):
for d in self.assets:
auto_gen_movement_entry = frappe.db.sql(
"""
SELECT asm.name
FROM `tabAsset Movement Item` asm_item, `tabAsset Movement` asm
WHERE
asm.docstatus=1 and
asm_item.parent=asm.name and
asm_item.asset=%s and
asm.company=%s and
asm_item.source_location is NULL and
asm.purpose=%s
ORDER BY
asm.transaction_date asc
""", (d.asset, self.company, 'Receipt'), as_dict=1)
if auto_gen_movement_entry[0].get('name') == self.name:
frappe.throw(_('{0} will be cancelled automatically on asset cancellation as it was \
auto generated for Asset {1}').format(self.name, d.asset))
def set_latest_location_in_asset(self): def set_latest_location_in_asset(self):
location, employee = '', '' current_location, current_employee = '', ''
cond = "1=1" cond = "1=1"
args = { for d in self.assets:
'asset': self.asset, args = {
'company': self.company 'asset': d.asset,
} 'company': self.company
}
if self.serial_no: # latest entry corresponds to current document's location, employee when transaction date > previous dates
cond = "serial_no like %(txt)s" # In case of cancellation it corresponds to previous latest document's location, employee
args.update({ latest_movement_entry = frappe.db.sql(
'txt': "%%%s%%" % self.serial_no """
}) SELECT asm_item.target_location, asm_item.to_employee
FROM `tabAsset Movement Item` asm_item, `tabAsset Movement` asm
WHERE
asm_item.parent=asm.name and
asm_item.asset=%(asset)s and
asm.company=%(company)s and
asm.docstatus=1 and {0}
ORDER BY
asm.transaction_date desc limit 1
""".format(cond), args)
if latest_movement_entry:
current_location = latest_movement_entry[0][0]
current_employee = latest_movement_entry[0][1]
latest_movement_entry = frappe.db.sql("""select target_location, to_employee from `tabAsset Movement` frappe.db.set_value('Asset', d.asset, 'location', current_location)
where asset=%(asset)s and docstatus=1 and company=%(company)s and {0} frappe.db.set_value('Asset', d.asset, 'custodian', current_employee)
order by transaction_date desc limit 1""".format(cond), args)
if latest_movement_entry:
location = latest_movement_entry[0][0]
employee = latest_movement_entry[0][1]
elif self.purpose in ['Transfer', 'Receipt']:
movement_entry = frappe.db.sql("""select source_location, from_employee from `tabAsset Movement`
where asset=%(asset)s and docstatus=2 and company=%(company)s and {0}
order by transaction_date asc limit 1""".format(cond), args)
if movement_entry:
location = movement_entry[0][0]
employee = movement_entry[0][1]
if not self.serial_no:
frappe.db.set_value("Asset", self.asset, "location", location)
if not employee and self.purpose in ['Receipt', 'Transfer']:
employee = self.to_employee
if self.serial_no:
for d in get_serial_nos(self.serial_no):
if (location or (self.purpose == 'Issue' and self.source_location)):
frappe.db.set_value('Serial No', d, 'location', location)
if employee or self.docstatus==2 or self.purpose == 'Issue':
frappe.db.set_value('Serial No', d, 'employee', employee)

View File

@@ -5,6 +5,7 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
import erpnext
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from frappe.utils import now, nowdate, get_last_day, add_days from frappe.utils import now, nowdate, get_last_day, add_days
from erpnext.assets.doctype.asset.test_asset import create_asset_data from erpnext.assets.doctype.asset.test_asset import create_asset_data
@@ -16,7 +17,6 @@ class TestAssetMovement(unittest.TestCase):
def setUp(self): def setUp(self):
create_asset_data() create_asset_data()
make_location() make_location()
make_serialized_item()
def test_movement(self): def test_movement(self):
pr = make_purchase_receipt(item_code="Macbook Pro", pr = make_purchase_receipt(item_code="Macbook Pro",
@@ -38,68 +38,72 @@ class TestAssetMovement(unittest.TestCase):
if asset.docstatus == 0: if asset.docstatus == 0:
asset.submit() asset.submit()
# check asset movement is created
if not frappe.db.exists("Location", "Test Location 2"): if not frappe.db.exists("Location", "Test Location 2"):
frappe.get_doc({ frappe.get_doc({
'doctype': 'Location', 'doctype': 'Location',
'location_name': 'Test Location 2' 'location_name': 'Test Location 2'
}).insert() }).insert()
movement1 = create_asset_movement(asset= asset.name, purpose = 'Transfer', movement1 = create_asset_movement(purpose = 'Transfer', company = asset.company,
company=asset.company, source_location="Test Location", target_location="Test Location 2") assets = [{ 'asset': asset.name , 'source_location': 'Test Location', 'target_location': 'Test Location 2'}],
reference_doctype = 'Purchase Receipt', reference_name = pr.name)
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2") self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2")
movement2 = create_asset_movement(asset= asset.name, purpose = 'Transfer', movement2 = create_asset_movement(purpose = 'Transfer', company = asset.company,
company=asset.company, source_location = "Test Location 2", target_location="Test Location") assets = [{ 'asset': asset.name , 'source_location': 'Test Location 2', 'target_location': 'Test Location'}],
reference_doctype = 'Purchase Receipt', reference_name = pr.name)
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
movement1.cancel() movement1.cancel()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
movement2.cancel() employee = make_employee("testassetmovemp@example.com", company="_Test Company")
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") movement3 = create_asset_movement(purpose = 'Issue', company = asset.company,
assets = [{ 'asset': asset.name , 'source_location': 'Test Location', 'to_employee': employee}],
def test_movement_for_serialized_asset(self): reference_doctype = 'Purchase Receipt', reference_name = pr.name)
asset_item = "Test Serialized Asset Item"
pr = make_purchase_receipt(item_code=asset_item, rate = 1000, qty=3, location = "Mumbai") # after issuing asset should belong to an employee not at a location
asset_name = frappe.db.get_value('Asset', {'purchase_receipt': pr.name}, 'name') self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), None)
self.assertEqual(frappe.db.get_value("Asset", asset.name, "custodian"), employee)
def test_last_movement_cancellation(self):
pr = make_purchase_receipt(item_code="Macbook Pro",
qty=1, rate=100000.0, location="Test Location")
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
asset = frappe.get_doc('Asset', asset_name) asset = frappe.get_doc('Asset', asset_name)
month_end_date = get_last_day(nowdate())
asset.available_for_use_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15)
asset.calculate_depreciation = 1 asset.calculate_depreciation = 1
asset.available_for_use_date = '2020-06-06'
asset.purchase_date = '2020-06-06'
asset.append("finance_books", { asset.append("finance_books", {
"expected_value_after_useful_life": 200, "expected_value_after_useful_life": 10000,
"next_depreciation_date": "2020-12-31",
"depreciation_method": "Straight Line", "depreciation_method": "Straight Line",
"total_number_of_depreciations": 3, "total_number_of_depreciations": 3,
"frequency_of_depreciation": 10, "frequency_of_depreciation": 10,
"depreciation_start_date": month_end_date "depreciation_start_date": "2020-06-06"
}) })
asset.submit() if asset.docstatus == 0:
serial_nos = frappe.db.get_value('Asset Movement', {'reference_name': pr.name}, 'serial_no') asset.submit()
if not frappe.db.exists("Location", "Test Location 2"):
frappe.get_doc({
'doctype': 'Location',
'location_name': 'Test Location 2'
}).insert()
movement = frappe.get_doc({'doctype': 'Asset Movement', 'reference_name': pr.name })
self.assertRaises(frappe.ValidationError, movement.cancel)
mov1 = create_asset_movement(asset=asset_name, purpose = 'Transfer', movement1 = create_asset_movement(purpose = 'Transfer', company = asset.company,
company=asset.company, source_location = "Mumbai", target_location="Pune", serial_no=serial_nos) assets = [{ 'asset': asset.name , 'source_location': 'Test Location', 'target_location': 'Test Location 2'}],
self.assertEqual(mov1.target_location, "Pune") reference_doctype = 'Purchase Receipt', reference_name = pr.name)
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2")
serial_no = frappe.db.get_value('Serial No', {'asset': asset_name}, 'name') movement1.cancel()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
employee = make_employee("testassetemp@example.com")
create_asset_movement(asset=asset_name, purpose = 'Transfer',
company=asset.company, serial_no=serial_no, to_employee=employee)
self.assertEqual(frappe.db.get_value('Serial No', serial_no, 'employee'), employee)
create_asset_movement(asset=asset_name, purpose = 'Transfer', company=asset.company,
serial_no=serial_no, from_employee=employee, to_employee="_T-Employee-00001")
self.assertEqual(frappe.db.get_value('Serial No', serial_no, 'location'), "Pune")
mov4 = create_asset_movement(asset=asset_name, purpose = 'Transfer',
company=asset.company, source_location = "Pune", target_location="Nagpur", serial_no=serial_nos)
self.assertEqual(mov4.target_location, "Nagpur")
self.assertEqual(frappe.db.get_value('Serial No', serial_no, 'location'), "Nagpur")
self.assertEqual(frappe.db.get_value('Serial No', serial_no, 'employee'), "_T-Employee-00001")
def create_asset_movement(**args): def create_asset_movement(**args):
args = frappe._dict(args) args = frappe._dict(args)
@@ -109,22 +113,14 @@ def create_asset_movement(**args):
movement = frappe.new_doc("Asset Movement") movement = frappe.new_doc("Asset Movement")
movement.update({ movement.update({
"asset": args.asset, "assets": args.assets,
"transaction_date": args.transaction_date, "transaction_date": args.transaction_date,
"target_location": args.target_location,
"company": args.company, "company": args.company,
'purpose': args.purpose or 'Receipt', 'purpose': args.purpose or 'Receipt',
'serial_no': args.serial_no, 'reference_doctype': args.reference_doctype,
'quantity': len(get_serial_nos(args.serial_no)) if args.serial_no else 1, 'reference_name': args.reference_name
'from_employee': "_T-Employee-00001" or args.from_employee,
'to_employee': args.to_employee
}) })
if args.source_location:
movement.update({
'source_location': args.source_location
})
movement.insert() movement.insert()
movement.submit() movement.submit()
@@ -137,33 +133,3 @@ def make_location():
'doctype': 'Location', 'doctype': 'Location',
'location_name': location 'location_name': location
}).insert(ignore_permissions = True) }).insert(ignore_permissions = True)
def make_serialized_item():
asset_item = "Test Serialized Asset Item"
if not frappe.db.exists('Item', asset_item):
asset_category = frappe.get_all('Asset Category')
if asset_category:
asset_category = asset_category[0].name
if not asset_category:
doc = frappe.get_doc({
'doctype': 'Asset Category',
'asset_category_name': 'Test Asset Category',
'depreciation_method': 'Straight Line',
'total_number_of_depreciations': 12,
'frequency_of_depreciation': 1,
'accounts': [{
'company_name': '_Test Company',
'fixed_asset_account': '_Test Fixed Asset - _TC',
'accumulated_depreciation_account': 'Depreciation - _TC',
'depreciation_expense_account': 'Depreciation - _TC'
}]
}).insert()
asset_category = doc.name
make_item(asset_item, {'is_stock_item':0,
'stock_uom': 'Box', 'is_fixed_asset': 1, 'has_serial_no': 1,
'asset_category': asset_category, 'serial_no_series': 'ABC.###'})

View File

@@ -0,0 +1,86 @@
{
"creation": "2019-10-07 18:49:00.737806",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"asset",
"source_location",
"from_employee",
"column_break_2",
"asset_name",
"target_location",
"to_employee"
],
"fields": [
{
"fieldname": "asset",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Asset",
"options": "Asset",
"reqd": 1
},
{
"fetch_from": "asset.asset_name",
"fieldname": "asset_name",
"fieldtype": "Data",
"label": "Asset Name",
"read_only": 1
},
{
"fieldname": "source_location",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Source Location",
"options": "Location"
},
{
"fieldname": "target_location",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Target Location",
"options": "Location"
},
{
"fieldname": "from_employee",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "From Employee",
"options": "Employee"
},
{
"fieldname": "to_employee",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "To Employee",
"options": "Employee"
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "company",
"fieldtype": "Link",
"hidden": 1,
"label": "Company",
"options": "Company",
"read_only": 1
}
],
"istable": 1,
"modified": "2019-10-09 15:59:08.265141",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Movement Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -1,9 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document from frappe.model.document import Document
class AssetSettings(Document): class AssetMovementItem(Document):
pass pass

View File

@@ -1,5 +0,0 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Asset Settings', {
});

View File

@@ -1,148 +0,0 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2018-01-03 10:30:32.983381",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "depreciation_options",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Depreciation Options",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "disable_cwip_accounting",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Disable CWIP Accounting",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2019-05-26 18:31:19.930563",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Settings",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 0,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

View File

@@ -1,23 +0,0 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Asset Settings", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially([
// insert a new Asset Settings
() => frappe.tests.make('Asset Settings', [
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);
});

View File

@@ -1,9 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import unittest
class TestAssetSettings(unittest.TestCase):
pass

View File

@@ -60,7 +60,8 @@
{ {
"fieldname": "date", "fieldname": "date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Date" "label": "Date",
"reqd": 1
}, },
{ {
"fieldname": "current_asset_value", "fieldname": "current_asset_value",
@@ -110,7 +111,7 @@
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"modified": "2019-05-26 09:46:23.613412", "modified": "2019-11-22 14:09:25.800375",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Value Adjustment", "name": "Asset Value Adjustment",

View File

@@ -5,12 +5,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt, getdate, cint, date_diff from frappe.utils import flt, getdate, cint, date_diff, formatdate
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
from frappe.model.document import Document from frappe.model.document import Document
class AssetValueAdjustment(Document): class AssetValueAdjustment(Document):
def validate(self): def validate(self):
self.validate_date()
self.set_difference_amount() self.set_difference_amount()
self.set_current_asset_value() self.set_current_asset_value()
@@ -23,6 +24,12 @@ class AssetValueAdjustment(Document):
frappe.throw(_("Cancel the journal entry {0} first").format(self.journal_entry)) frappe.throw(_("Cancel the journal entry {0} first").format(self.journal_entry))
self.reschedule_depreciations(self.current_asset_value) self.reschedule_depreciations(self.current_asset_value)
def validate_date(self):
asset_purchase_date = frappe.db.get_value('Asset', self.asset, 'purchase_date')
if getdate(self.date) < getdate(asset_purchase_date):
frappe.throw(_("Asset Value Adjustment cannot be posted before Asset's purchase date <b>{0}</b>.")
.format(formatdate(asset_purchase_date)), title="Incorrect Date")
def set_difference_amount(self): def set_difference_amount(self):
self.difference_amount = flt(self.current_asset_value - self.new_asset_value) self.difference_amount = flt(self.current_asset_value - self.new_asset_value)

View File

@@ -313,7 +313,7 @@ def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=
last_purchase_details = get_last_purchase_details(item_code, name) last_purchase_details = get_last_purchase_details(item_code, name)
if last_purchase_details: if last_purchase_details:
last_purchase_rate = (last_purchase_details['base_rate'] * (flt(conversion_factor) or 1.0)) / conversion_rate last_purchase_rate = (last_purchase_details['base_net_rate'] * (flt(conversion_factor) or 1.0)) / conversion_rate
return last_purchase_rate return last_purchase_rate
else: else:
item_last_purchase_rate = frappe.get_cached_value("Item", item_code, "last_purchase_rate") item_last_purchase_rate = frappe.get_cached_value("Item", item_code, "last_purchase_rate")

View File

@@ -43,6 +43,7 @@
"base_amount", "base_amount",
"pricing_rules", "pricing_rules",
"is_free_item", "is_free_item",
"is_fixed_asset",
"section_break_29", "section_break_29",
"net_rate", "net_rate",
"net_amount", "net_amount",
@@ -699,11 +700,19 @@
"fieldtype": "Data", "fieldtype": "Data",
"label": "Manufacturer Part Number", "label": "Manufacturer Part Number",
"read_only": 1 "read_only": 1
},
{
"default": "0",
"fetch_from": "item_code.is_fixed_asset",
"fieldname": "is_fixed_asset",
"fieldtype": "Check",
"label": "Is Fixed Asset",
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"modified": "2019-09-17 22:32:34.703923", "modified": "2019-11-07 17:19:12.090355",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Item", "name": "Purchase Order Item",

View File

@@ -134,7 +134,7 @@ frappe.ui.form.on("Request for Quotation",{
if (args.search_type === "Tag" && args.tag) { if (args.search_type === "Tag" && args.tag) {
return frappe.call({ return frappe.call({
type: "GET", type: "GET",
method: "frappe.desk.tags.get_tagged_docs", method: "frappe.desk.doctype.tag.tag.get_tagged_docs",
args: { args: {
"doctype": "Supplier", "doctype": "Supplier",
"tag": args.tag "tag": args.tag

View File

@@ -344,13 +344,9 @@ def get_item_from_material_requests_based_on_supplier(source_name, target_doc =
@frappe.whitelist() @frappe.whitelist()
def get_supplier_tag(): 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 = [] tags = list(set([tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag]))
for tag in data: frappe.cache().hset("Supplier", "Tags", tags)
tags += filter(bool, tag[0].split(","))
tags = list(set(tags))
return tags
return frappe.cache().hget("Supplier", "Tags")

View File

@@ -24,12 +24,12 @@ def update_last_purchase_rate(doc, is_submit):
last_purchase_rate = None last_purchase_rate = None
if last_purchase_details and \ if last_purchase_details and \
(last_purchase_details.purchase_date > this_purchase_date): (last_purchase_details.purchase_date > this_purchase_date):
last_purchase_rate = last_purchase_details['base_rate'] last_purchase_rate = last_purchase_details['base_net_rate']
elif is_submit == 1: elif is_submit == 1:
# even if this transaction is the latest one, it should be submitted # even if this transaction is the latest one, it should be submitted
# for it to be considered for latest purchase rate # for it to be considered for latest purchase rate
if flt(d.conversion_factor): if flt(d.conversion_factor):
last_purchase_rate = flt(d.base_rate) / flt(d.conversion_factor) last_purchase_rate = flt(d.base_net_rate) / flt(d.conversion_factor)
# Check if item code is present # Check if item code is present
# Conversion factor should not be mandatory for non itemized items # Conversion factor should not be mandatory for non itemized items
elif d.item_code: elif d.item_code:

View File

@@ -0,0 +1,14 @@
# Version 12.2.0 Release Notes
### Accounting
1. Fixed Asset
- "Enable CWIP" options moved to Asset Category from Asset Settings
- Removed Asset link from Purchase Receipt Item table
- Enhanced Asset master
- Asset Movement now handles movement of multiple assets
- Introduced monthly depreciation
2. GL Entries for Landed Cost Voucher now posted directly against individual Charges account
3. Optimization of BOM Update Tool
4. Syncing of Stock and Account balance is enforced, in case of perpetual inventory
5. Rendered email template in Email Campaign

View File

@@ -21,10 +21,6 @@ def get_data():
"name": "Asset Category", "name": "Asset Category",
"onboard": 1, "onboard": 1,
}, },
{
"type": "doctype",
"name": "Asset Settings",
},
{ {
"type": "doctype", "type": "doctype",
"name": "Asset Movement", "name": "Asset Movement",

View File

@@ -718,48 +718,6 @@ class AccountsController(TransactionBase):
# at quotation / sales order level and we shouldn't stop someone # at quotation / sales order level and we shouldn't stop someone
# from creating a sales invoice if sales order is already created # from creating a sales invoice if sales order is already created
def validate_fixed_asset(self):
for d in self.get("items"):
if d.is_fixed_asset:
# if d.qty > 1:
# frappe.throw(_("Row #{0}: Qty must be 1, as item is a fixed asset. Please use separate row for multiple qty.").format(d.idx))
if d.meta.get_field("asset") and d.asset:
asset = frappe.get_doc("Asset", d.asset)
if asset.company != self.company:
frappe.throw(_("Row #{0}: Asset {1} does not belong to company {2}")
.format(d.idx, d.asset, self.company))
elif asset.item_code != d.item_code:
frappe.throw(_("Row #{0}: Asset {1} does not linked to Item {2}")
.format(d.idx, d.asset, d.item_code))
# elif asset.docstatus != 1:
# frappe.throw(_("Row #{0}: Asset {1} must be submitted").format(d.idx, d.asset))
elif self.doctype == "Purchase Invoice":
# if asset.status != "Submitted":
# frappe.throw(_("Row #{0}: Asset {1} is already {2}")
# .format(d.idx, d.asset, asset.status))
if getdate(asset.purchase_date) != getdate(self.posting_date):
frappe.throw(
_("Row #{0}: Posting Date must be same as purchase date {1} of asset {2}").format(d.idx,
asset.purchase_date,
d.asset))
elif asset.is_existing_asset:
frappe.throw(
_("Row #{0}: Purchase Invoice cannot be made against an existing asset {1}").format(
d.idx, d.asset))
elif self.docstatus == "Sales Invoice" and self.docstatus == 1:
if self.update_stock:
frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale"))
elif asset.status in ("Scrapped", "Cancelled", "Sold"):
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): def delink_advance_entries(self, linked_doc_name):
total_allocated_amount = 0 total_allocated_amount = 0
for adv in self.advances: for adv in self.advances:
@@ -1172,6 +1130,7 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"): def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
data = json.loads(trans_items) data = json.loads(trans_items)
sales_doctypes = ['Sales Order', 'Sales Invoice', 'Delivery Note', 'Quotation']
parent = frappe.get_doc(parent_doctype, parent_doctype_name) parent = frappe.get_doc(parent_doctype, parent_doctype_name)
for d in data: for d in data:
@@ -1192,8 +1151,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
frappe.throw(_("Cannot set quantity less than received quantity")) frappe.throw(_("Cannot set quantity less than received quantity"))
child_item.qty = flt(d.get("qty")) child_item.qty = flt(d.get("qty"))
precision = child_item.precision("rate") or 2
if flt(child_item.billed_amt) > (flt(d.get("rate")) * flt(d.get("qty"))): if flt(child_item.billed_amt, precision) > flt(flt(d.get("rate")) * flt(d.get("qty")), precision):
frappe.throw(_("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.") frappe.throw(_("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.")
.format(child_item.idx, child_item.item_code)) .format(child_item.idx, child_item.item_code))
else: else:
@@ -1204,18 +1164,22 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
# if rate is greater than price_list_rate, set margin # if rate is greater than price_list_rate, set margin
# or set discount # or set discount
child_item.discount_percentage = 0 child_item.discount_percentage = 0
child_item.margin_type = "Amount"
child_item.margin_rate_or_amount = flt(child_item.rate - child_item.price_list_rate, if parent_doctype in sales_doctypes:
child_item.precision("margin_rate_or_amount")) child_item.margin_type = "Amount"
child_item.rate_with_margin = child_item.rate child_item.margin_rate_or_amount = flt(child_item.rate - child_item.price_list_rate,
child_item.precision("margin_rate_or_amount"))
child_item.rate_with_margin = child_item.rate
else: else:
child_item.discount_percentage = flt((1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0, child_item.discount_percentage = flt((1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0,
child_item.precision("discount_percentage")) child_item.precision("discount_percentage"))
child_item.discount_amount = flt( child_item.discount_amount = flt(
child_item.price_list_rate) - flt(child_item.rate) child_item.price_list_rate) - flt(child_item.rate)
child_item.margin_type = ""
child_item.margin_rate_or_amount = 0 if parent_doctype in sales_doctypes:
child_item.rate_with_margin = 0 child_item.margin_type = ""
child_item.margin_rate_or_amount = 0
child_item.rate_with_margin = 0
child_item.flags.ignore_validate_update_after_submit = True child_item.flags.ignore_validate_update_after_submit = True
if new_child_flag: if new_child_flag:

View File

@@ -101,7 +101,7 @@ class BuyingController(StockController):
msgprint(_('Tax Category has been changed to "Total" because all the Items are non-stock items')) msgprint(_('Tax Category has been changed to "Total" because all the Items are non-stock items'))
def get_asset_items(self): def get_asset_items(self):
if self.doctype not in ['Purchase Invoice', 'Purchase Receipt']: if self.doctype not in ['Purchase Order', 'Purchase Invoice', 'Purchase Receipt']:
return [] return []
return [d.item_code for d in self.items if d.is_fixed_asset] return [d.item_code for d in self.items if d.is_fixed_asset]
@@ -150,25 +150,26 @@ class BuyingController(StockController):
TODO: rename item_tax_amount to valuation_tax_amount TODO: rename item_tax_amount to valuation_tax_amount
""" """
stock_items = self.get_stock_items() + self.get_asset_items() stock_and_asset_items = self.get_stock_items() + self.get_asset_items()
stock_items_qty, stock_items_amount = 0, 0 stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
last_stock_item_idx = 1 last_item_idx = 1
for d in self.get(parentfield): for d in self.get(parentfield):
if d.item_code and d.item_code in stock_items: if d.item_code and d.item_code in stock_and_asset_items:
stock_items_qty += flt(d.qty) stock_and_asset_items_qty += flt(d.qty)
stock_items_amount += flt(d.base_net_amount) stock_and_asset_items_amount += flt(d.base_net_amount)
last_stock_item_idx = d.idx last_item_idx = d.idx
total_valuation_amount = sum([flt(d.base_tax_amount_after_discount_amount) for d in self.get("taxes") total_valuation_amount = sum([flt(d.base_tax_amount_after_discount_amount) for d in self.get("taxes")
if d.category in ["Valuation", "Valuation and Total"]]) if d.category in ["Valuation", "Valuation and Total"]])
valuation_amount_adjustment = total_valuation_amount valuation_amount_adjustment = total_valuation_amount
for i, item in enumerate(self.get(parentfield)): for i, item in enumerate(self.get(parentfield)):
if item.item_code and item.qty and item.item_code in stock_items: if item.item_code and item.qty and item.item_code in stock_and_asset_items:
item_proportion = flt(item.base_net_amount) / stock_items_amount if stock_items_amount \ item_proportion = flt(item.base_net_amount) / stock_and_asset_items_amount if stock_and_asset_items_amount \
else flt(item.qty) / stock_items_qty else flt(item.qty) / stock_and_asset_items_qty
if i == (last_stock_item_idx - 1):
if i == (last_item_idx - 1):
item.item_tax_amount = flt(valuation_amount_adjustment, item.item_tax_amount = flt(valuation_amount_adjustment,
self.precision("item_tax_amount", item)) self.precision("item_tax_amount", item))
else: else:
@@ -572,43 +573,33 @@ class BuyingController(StockController):
asset_items = self.get_asset_items() asset_items = self.get_asset_items()
if asset_items: if asset_items:
self.make_serial_nos_for_asset(asset_items) self.auto_make_assets(asset_items)
def make_serial_nos_for_asset(self, asset_items): def auto_make_assets(self, asset_items):
items_data = get_asset_item_details(asset_items) items_data = get_asset_item_details(asset_items)
messages = []
for d in self.items: for d in self.items:
if d.is_fixed_asset: if d.is_fixed_asset:
item_data = items_data.get(d.item_code) item_data = items_data.get(d.item_code)
if not d.asset:
asset = self.make_asset(d)
d.db_set('asset', asset)
if item_data.get('has_serial_no'): if item_data.get('auto_create_assets'):
# If item has serial no # If asset has to be auto created
if item_data.get('serial_no_series') and not d.serial_no: # Check for asset naming series
serial_nos = get_auto_serial_nos(item_data.get('serial_no_series'), d.qty) if item_data.get('asset_naming_series'):
elif d.serial_no: for qty in range(cint(d.qty)):
serial_nos = d.serial_no self.make_asset(d)
elif not d.serial_no: is_plural = 's' if cint(d.qty) != 1 else ''
frappe.throw(_("Serial no is mandatory for the item {0}").format(d.item_code)) messages.append(_('{0} Asset{2} Created for <b>{1}</b>').format(cint(d.qty), d.item_code, is_plural))
else:
frappe.throw(_("Row {1}: Asset Naming Series is mandatory for the auto creation for item {0}")
.format(d.item_code, d.idx))
else:
messages.append(_("Assets not created for <b>{0}</b>. You will have to create asset manually.")
.format(d.item_code))
auto_make_serial_nos({ for message in messages:
'serial_no': serial_nos, frappe.msgprint(message, title="Success")
'item_code': d.item_code,
'via_stock_ledger': False,
'company': self.company,
'supplier': self.supplier,
'actual_qty': d.qty,
'purchase_document_type': self.doctype,
'purchase_document_no': self.name,
'asset': d.asset,
'location': d.asset_location
})
d.db_set('serial_no', serial_nos)
if d.asset:
self.make_asset_movement(d)
def make_asset(self, row): def make_asset(self, row):
if not row.asset_location: if not row.asset_location:
@@ -617,7 +608,7 @@ class BuyingController(StockController):
item_data = frappe.db.get_value('Item', item_data = frappe.db.get_value('Item',
row.item_code, ['asset_naming_series', 'asset_category'], as_dict=1) row.item_code, ['asset_naming_series', 'asset_category'], as_dict=1)
purchase_amount = flt(row.base_net_amount + row.item_tax_amount) purchase_amount = flt(row.base_rate + row.item_tax_amount)
asset = frappe.get_doc({ asset = frappe.get_doc({
'doctype': 'Asset', 'doctype': 'Asset',
'item_code': row.item_code, 'item_code': row.item_code,
@@ -640,57 +631,49 @@ class BuyingController(StockController):
asset.set_missing_values() asset.set_missing_values()
asset.insert() asset.insert()
asset_link = frappe.utils.get_link_to_form('Asset', asset.name)
frappe.msgprint(_("Asset {0} created").format(asset_link))
return asset.name
def make_asset_movement(self, row):
asset_movement = frappe.get_doc({
'doctype': 'Asset Movement',
'asset': row.asset,
'target_location': row.asset_location,
'purpose': 'Receipt',
'serial_no': row.serial_no,
'quantity': len(get_serial_nos(row.serial_no)),
'company': self.company,
'transaction_date': self.posting_date,
'reference_doctype': self.doctype,
'reference_name': self.name
}).insert()
return asset_movement.name
def update_fixed_asset(self, field, delete_asset = False): def update_fixed_asset(self, field, delete_asset = False):
for d in self.get("items"): for d in self.get("items"):
if d.is_fixed_asset and d.asset: if d.is_fixed_asset:
asset = frappe.get_doc("Asset", d.asset) is_auto_create_enabled = frappe.db.get_value('Item', d.item_code, 'auto_create_assets')
assets = frappe.db.get_all('Asset', filters={ field : self.name, 'item_code' : d.item_code })
if delete_asset and asset.docstatus == 0: for asset in assets:
frappe.delete_doc("Asset", asset.name) asset = frappe.get_doc('Asset', asset.name)
d.db_set('asset', None) if delete_asset and is_auto_create_enabled:
continue # need to delete movements to delete assets otherwise throws link exists error
movements = frappe.db.sql(
"""SELECT asm.name
FROM `tabAsset Movement` asm, `tabAsset Movement Item` asm_item
WHERE asm_item.parent=asm.name and asm_item.asset=%s""", asset.name, as_dict=1)
for movement in movements:
frappe.delete_doc('Asset Movement', movement.name, force=1)
frappe.delete_doc("Asset", asset.name, force=1)
continue
if self.docstatus in [0, 1] and not asset.get(field): if self.docstatus in [0, 1] and not asset.get(field):
asset.set(field, self.name) asset.set(field, self.name)
asset.purchase_date = self.posting_date asset.purchase_date = self.posting_date
asset.supplier = self.supplier asset.supplier = self.supplier
elif self.docstatus == 2: elif self.docstatus == 2:
asset.set(field, None) if asset.docstatus == 0:
asset.supplier = None asset.set(field, None)
asset.supplier = None
if asset.docstatus == 1 and delete_asset:
frappe.throw(_('Cannot cancel this document as it is linked with submitted asset {0}.\
Please cancel the it to continue.').format(asset.name))
asset.flags.ignore_validate_update_after_submit = True asset.flags.ignore_validate_update_after_submit = True
asset.flags.ignore_mandatory = True asset.flags.ignore_mandatory = True
if asset.docstatus == 0: if asset.docstatus == 0:
asset.flags.ignore_validate = True asset.flags.ignore_validate = True
asset.save() asset.save()
def delete_linked_asset(self): def delete_linked_asset(self):
if self.doctype == 'Purchase Invoice' and not self.get('update_stock'): if self.doctype == 'Purchase Invoice' and not self.get('update_stock'):
return return
frappe.db.sql("delete from `tabAsset Movement` where reference_name=%s", self.name) frappe.db.sql("delete from `tabAsset Movement` where reference_name=%s", self.name)
frappe.db.sql("delete from `tabSerial No` where purchase_document_no=%s", self.name)
def validate_schedule_date(self): def validate_schedule_date(self):
if not self.get("items"): if not self.get("items"):
@@ -764,7 +747,7 @@ def get_backflushed_subcontracted_raw_materials_from_se(purchase_orders, purchas
def get_asset_item_details(asset_items): def get_asset_item_details(asset_items):
asset_items_data = {} asset_items_data = {}
for d in frappe.get_all('Item', fields = ["name", "has_serial_no", "serial_no_series"], for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"],
filters = {'name': ('in', asset_items)}): filters = {'name': ('in', asset_items)}):
asset_items_data.setdefault(d.name, d) asset_items_data.setdefault(d.name, d)

View File

@@ -152,6 +152,24 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
conditions = [] conditions = []
#Get searchfields from meta and use in Item Link field query
meta = frappe.get_meta("Item", cached=True)
searchfields = meta.get_search_fields()
if "description" in searchfields:
searchfields.remove("description")
columns = ''
extra_searchfields = [field for field in searchfields
if not field in ["name", "item_group", "description"]]
if extra_searchfields:
columns = ", " + ", ".join(extra_searchfields)
searchfields = searchfields + [field for field in[searchfield or "name", "item_code", "item_group", "item_name"]
if not field in searchfields]
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
description_cond = '' description_cond = ''
if frappe.db.count('Item', cache=True) < 50000: if frappe.db.count('Item', cache=True) < 50000:
# scan description only if items are less than 50000 # scan description only if items are less than 50000
@@ -162,17 +180,14 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
concat(substr(tabItem.item_name, 1, 40), "..."), item_name) as item_name, concat(substr(tabItem.item_name, 1, 40), "..."), item_name) as item_name,
tabItem.item_group, tabItem.item_group,
if(length(tabItem.description) > 40, \ if(length(tabItem.description) > 40, \
concat(substr(tabItem.description, 1, 40), "..."), description) as decription concat(substr(tabItem.description, 1, 40), "..."), description) as description
{columns}
from tabItem from tabItem
where tabItem.docstatus < 2 where tabItem.docstatus < 2
and tabItem.has_variants=0 and tabItem.has_variants=0
and tabItem.disabled=0 and tabItem.disabled=0
and (tabItem.end_of_life > %(today)s or ifnull(tabItem.end_of_life, '0000-00-00')='0000-00-00') and (tabItem.end_of_life > %(today)s or ifnull(tabItem.end_of_life, '0000-00-00')='0000-00-00')
and (tabItem.`{key}` LIKE %(txt)s and ({scond} or tabItem.item_code IN (select parent from `tabItem Barcode` where barcode LIKE %(txt)s)
or tabItem.item_code LIKE %(txt)s
or tabItem.item_group LIKE %(txt)s
or tabItem.item_name LIKE %(txt)s
or tabItem.item_code IN (select parent from `tabItem Barcode` where barcode LIKE %(txt)s)
{description_cond}) {description_cond})
{fcond} {mcond} {fcond} {mcond}
order by order by
@@ -182,6 +197,8 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
name, item_name name, item_name
limit %(start)s, %(page_len)s """.format( limit %(start)s, %(page_len)s """.format(
key=searchfield, key=searchfield,
columns=columns,
scond=searchfields,
fcond=get_filters_cond(doctype, filters, conditions).replace('%', '%%'), fcond=get_filters_cond(doctype, filters, conditions).replace('%', '%%'),
mcond=get_match_cond(doctype).replace('%', '%%'), mcond=get_match_cond(doctype).replace('%', '%%'),
description_cond = description_cond), description_cond = description_cond),
@@ -463,3 +480,29 @@ def item_manufacturer_query(doctype, txt, searchfield, start, page_len, filters)
as_list=1 as_list=1
) )
return item_manufacturers return item_manufacturers
@frappe.whitelist()
def get_purchase_receipts(doctype, txt, searchfield, start, page_len, filters):
query = """
select pr.name
from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pritem
where pr.docstatus = 1 and pritem.parent = pr.name
and pr.name like {txt}""".format(txt = frappe.db.escape('%{0}%'.format(txt)))
if filters and filters.get('item_code'):
query += " and pritem.item_code = {item_code}".format(item_code = frappe.db.escape(filters.get('item_code')))
return frappe.db.sql(query, filters)
@frappe.whitelist()
def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters):
query = """
select pi.name
from `tabPurchase Invoice` pi, `tabPurchase Invoice Item` piitem
where pi.docstatus = 1 and piitem.parent = pi.name
and pi.name like {txt}""".format(txt = frappe.db.escape('%{0}%'.format(txt)))
if filters and filters.get('item_code'):
query += " and piitem.item_code = {item_code}".format(item_code = frappe.db.escape(filters.get('item_code')))
return frappe.db.sql(query, filters)

View File

@@ -72,7 +72,7 @@ def validate_returned_items(doc):
items_returned = False items_returned = False
for d in doc.get("items"): for d in doc.get("items"):
if d.item_code and (flt(d.qty) < 0 or d.get('received_qty') < 0): if d.item_code and (flt(d.qty) < 0 or flt(d.get('received_qty')) < 0):
if d.item_code not in valid_items: if d.item_code not in valid_items:
frappe.throw(_("Row # {0}: Returned Item {1} does not exists in {2} {3}") frappe.throw(_("Row # {0}: Returned Item {1} does not exists in {2} {3}")
.format(d.idx, d.item_code, doc.doctype, doc.return_against)) .format(d.idx, d.item_code, doc.doctype, doc.return_against))

View File

@@ -49,7 +49,8 @@ status_map = {
["Submitted", "eval:self.docstatus==1"], ["Submitted", "eval:self.docstatus==1"],
["Paid", "eval:self.outstanding_amount==0 and self.docstatus==1"], ["Paid", "eval:self.outstanding_amount==0 and self.docstatus==1"],
["Return", "eval:self.is_return==1 and self.docstatus==1"], ["Return", "eval:self.is_return==1 and self.docstatus==1"],
["Debit Note Issued", "eval:self.outstanding_amount < 0 and self.docstatus==1"], ["Debit Note Issued",
"eval:self.outstanding_amount <= 0 and self.docstatus==1 and self.is_return==0 and get_value('Purchase Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1})"],
["Unpaid", "eval:self.outstanding_amount > 0 and getdate(self.due_date) >= getdate(nowdate()) and self.docstatus==1"], ["Unpaid", "eval:self.outstanding_amount > 0 and getdate(self.due_date) >= getdate(nowdate()) and self.docstatus==1"],
["Overdue", "eval:self.outstanding_amount > 0 and getdate(self.due_date) < getdate(nowdate()) and self.docstatus==1"], ["Overdue", "eval:self.outstanding_amount > 0 and getdate(self.due_date) < getdate(nowdate()) and self.docstatus==1"],
["Cancelled", "eval:self.docstatus==2"], ["Cancelled", "eval:self.docstatus==2"],
@@ -118,7 +119,6 @@ class StatusUpdater(Document):
if self.doctype in status_map: if self.doctype in status_map:
_status = self.status _status = self.status
if status and update: if status and update:
self.db_set("status", status) self.db_set("status", status)

View File

@@ -207,41 +207,6 @@ class StockController(AccountsController):
reference_doctype=self.doctype, reference_doctype=self.doctype,
reference_name=self.name)).insert().name reference_name=self.name)).insert().name
def make_adjustment_entry(self, expected_gle, voucher_obj):
from erpnext.accounts.utils import get_stock_and_account_difference
account_list = [d.account for d in expected_gle]
acc_diff = get_stock_and_account_difference(account_list,
expected_gle[0].posting_date, self.company)
cost_center = self.get_company_default("cost_center")
stock_adjustment_account = self.get_company_default("stock_adjustment_account")
gl_entries = []
for account, diff in acc_diff.items():
if diff:
gl_entries.append([
# stock in hand account
voucher_obj.get_gl_dict({
"account": account,
"against": stock_adjustment_account,
"debit": diff,
"remarks": "Adjustment Accounting Entry for Stock",
}),
# account against stock in hand
voucher_obj.get_gl_dict({
"account": stock_adjustment_account,
"against": account,
"credit": diff,
"cost_center": cost_center or None,
"remarks": "Adjustment Accounting Entry for Stock",
}),
])
if gl_entries:
from erpnext.accounts.general_ledger import make_gl_entries
make_gl_entries(gl_entries)
def check_expense_account(self, item): def check_expense_account(self, item):
if not item.get("expense_account"): if not item.get("expense_account"):
frappe.throw(_("Expense or Difference account is mandatory for Item {0} as it impacts overall stock value").format(item.item_code)) frappe.throw(_("Expense or Difference account is mandatory for Item {0} as it impacts overall stock value").format(item.item_code))

View File

@@ -52,7 +52,8 @@
"fieldtype": "Select", "fieldtype": "Select",
"in_list_view": 1, "in_list_view": 1,
"label": "Email Campaign For ", "label": "Email Campaign For ",
"options": "\nLead\nContact" "options": "\nLead\nContact",
"reqd": 1
}, },
{ {
"fieldname": "recipient", "fieldname": "recipient",
@@ -69,7 +70,7 @@
"options": "User" "options": "User"
} }
], ],
"modified": "2019-07-12 13:47:37.261213", "modified": "2019-11-11 17:18:47.342839",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Email Campaign", "name": "Email Campaign",

View File

@@ -73,13 +73,13 @@ def send_mail(entry, email_campaign):
email_template = frappe.get_doc("Email Template", entry.get("email_template")) email_template = frappe.get_doc("Email Template", entry.get("email_template"))
sender = frappe.db.get_value("User", email_campaign.get("sender"), 'email') sender = frappe.db.get_value("User", email_campaign.get("sender"), 'email')
context = {"doc": frappe.get_doc(email_campaign.email_campaign_for, email_campaign.recipient)}
# send mail and link communication to document # send mail and link communication to document
comm = make( comm = make(
doctype = "Email Campaign", doctype = "Email Campaign",
name = email_campaign.name, name = email_campaign.name,
subject = email_template.get("subject"), subject = email_template.get("subject"),
content = email_template.get("response"), content = frappe.render_template(email_template.get("response"), context),
sender = sender, sender = sender,
recipients = recipient, recipients = recipient,
communication_medium = "Email", communication_medium = "Email",

View File

@@ -11,6 +11,7 @@
"validate_batch", "validate_batch",
"validate_course", "validate_course",
"academic_term_reqd", "academic_term_reqd",
"user_creation_skip",
"section_break_7", "section_break_7",
"instructor_created_by", "instructor_created_by",
"web_academy_settings_section", "web_academy_settings_section",
@@ -91,6 +92,13 @@
"fieldname": "enable_lms", "fieldname": "enable_lms",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Enable LMS" "label": "Enable LMS"
},
{
"default": "0",
"description": "By default, a new User is created for every new Student. If enabled, no new User will be created when a new Student is created.",
"fieldname": "user_creation_skip",
"fieldtype": "Check",
"label": "Skip User creation for new Student"
} }
], ],
"issingle": 1, "issingle": 1,
@@ -133,4 +141,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -27,3 +27,16 @@ frappe.ui.form.on('Student', {
} }
} }
}); });
frappe.ui.form.on('Student Guardian', {
guardians_add: function(frm){
frm.fields_dict['guardians'].grid.get_field('guardian').get_query = function(doc){
var guardian_list = [];
if(!doc.__islocal) guardian_list.push(doc.guardian);
$.each(doc.guardians, function(idx, val){
if (val.guardian) guardian_list.push(val.guardian);
});
return { filters: [['Guardian', 'name', 'not in', guardian_list]] };
};
}
});

View File

@@ -40,7 +40,8 @@ class Student(Document):
frappe.throw(_("Student {0} exist against student applicant {1}").format(student[0][0], self.student_applicant)) frappe.throw(_("Student {0} exist against student applicant {1}").format(student[0][0], self.student_applicant))
def after_insert(self): def after_insert(self):
self.create_student_user() if not frappe.get_single('Education Settings').user_creation_skip:
self.create_student_user()
def create_student_user(self): def create_student_user(self):
"""Create a website user for student creation if not already exists""" """Create a website user for student creation if not already exists"""

View File

@@ -1,10 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, base64, hashlib, hmac, json import frappe, base64, hashlib, hmac, json
import datetime
from frappe import _ from frappe import _
def verify_request(): def verify_request():
woocommerce_settings = frappe.get_doc("Woocommerce Settings") woocommerce_settings = frappe.get_doc("Woocommerce Settings")
sig = base64.b64encode( sig = base64.b64encode(
@@ -30,191 +28,149 @@ def order(*args, **kwargs):
frappe.log_error(error_message, "WooCommerce Error") frappe.log_error(error_message, "WooCommerce Error")
raise raise
def _order(*args, **kwargs): def _order(*args, **kwargs):
woocommerce_settings = frappe.get_doc("Woocommerce Settings") woocommerce_settings = frappe.get_doc("Woocommerce Settings")
if frappe.flags.woocomm_test_order_data: if frappe.flags.woocomm_test_order_data:
fd = frappe.flags.woocomm_test_order_data order = frappe.flags.woocomm_test_order_data
event = "created" event = "created"
elif frappe.request and frappe.request.data: elif frappe.request and frappe.request.data:
verify_request() verify_request()
fd = json.loads(frappe.request.data) try:
order = json.loads(frappe.request.data)
except ValueError:
#woocommerce returns 'webhook_id=value' for the first request which is not JSON
order = frappe.request.data
event = frappe.get_request_header("X-Wc-Webhook-Event") event = frappe.get_request_header("X-Wc-Webhook-Event")
else: else:
return "success" return "success"
if event == "created": if event == "created":
raw_billing_data = fd.get("billing") raw_billing_data = order.get("billing")
customer_woo_com_email = raw_billing_data.get("email")
if frappe.get_value("Customer",{"woocommerce_email": customer_woo_com_email}):
# Edit
link_customer_and_address(raw_billing_data,1)
else:
# Create
link_customer_and_address(raw_billing_data,0)
items_list = fd.get("line_items")
for item in items_list:
item_woo_com_id = item.get("product_id")
if frappe.get_value("Item",{"woocommerce_id": item_woo_com_id}):
#Edit
link_item(item,1)
else:
link_item(item,0)
customer_name = raw_billing_data.get("first_name") + " " + raw_billing_data.get("last_name") customer_name = raw_billing_data.get("first_name") + " " + raw_billing_data.get("last_name")
link_customer_and_address(raw_billing_data, customer_name)
link_items(order.get("line_items"), woocommerce_settings)
create_sales_order(order, woocommerce_settings, customer_name)
new_sales_order = frappe.new_doc("Sales Order") def link_customer_and_address(raw_billing_data, customer_name):
new_sales_order.customer = customer_name customer_woo_com_email = raw_billing_data.get("email")
customer_exists = frappe.get_value("Customer", {"woocommerce_email": customer_woo_com_email})
created_date = fd.get("date_created").split("T") if not customer_exists:
new_sales_order.transaction_date = created_date[0] # Create Customer
new_sales_order.po_no = fd.get("id")
new_sales_order.woocommerce_id = fd.get("id")
new_sales_order.naming_series = woocommerce_settings.sales_order_series or "SO-WOO-"
placed_order_date = created_date[0]
raw_date = datetime.datetime.strptime(placed_order_date, "%Y-%m-%d")
raw_delivery_date = frappe.utils.add_to_date(raw_date,days = 7)
order_delivery_date_str = raw_delivery_date.strftime('%Y-%m-%d')
order_delivery_date = str(order_delivery_date_str)
new_sales_order.delivery_date = order_delivery_date
default_set_company = frappe.get_doc("Global Defaults")
company = raw_billing_data.get("company") or default_set_company.default_company
found_company = frappe.get_doc("Company",{"name":company})
company_abbr = found_company.abbr
new_sales_order.company = company
for item in items_list:
woocomm_item_id = item.get("product_id")
found_item = frappe.get_doc("Item",{"woocommerce_id": woocomm_item_id})
ordered_items_tax = item.get("total_tax")
new_sales_order.append("items",{
"item_code": found_item.item_code,
"item_name": found_item.item_name,
"description": found_item.item_name,
"delivery_date":order_delivery_date,
"uom": woocommerce_settings.uom or _("Nos"),
"qty": item.get("quantity"),
"rate": item.get("price"),
"warehouse": woocommerce_settings.warehouse or "Stores" + " - " + company_abbr
})
add_tax_details(new_sales_order,ordered_items_tax,"Ordered Item tax",0)
# shipping_details = fd.get("shipping_lines") # used for detailed order
shipping_total = fd.get("shipping_total")
shipping_tax = fd.get("shipping_tax")
add_tax_details(new_sales_order,shipping_tax,"Shipping Tax",1)
add_tax_details(new_sales_order,shipping_total,"Shipping Total",1)
new_sales_order.submit()
frappe.db.commit()
def link_customer_and_address(raw_billing_data,customer_status):
if customer_status == 0:
# create
customer = frappe.new_doc("Customer") customer = frappe.new_doc("Customer")
address = frappe.new_doc("Address") else:
# Edit Customer
if customer_status == 1: customer = frappe.get_doc("Customer", {"woocommerce_email": customer_woo_com_email})
# Edit
customer_woo_com_email = raw_billing_data.get("email")
customer = frappe.get_doc("Customer",{"woocommerce_email": customer_woo_com_email})
old_name = customer.customer_name old_name = customer.customer_name
full_name = str(raw_billing_data.get("first_name"))+ " "+str(raw_billing_data.get("last_name")) customer.customer_name = customer_name
customer.customer_name = full_name customer.woocommerce_email = customer_woo_com_email
customer.woocommerce_email = str(raw_billing_data.get("email")) customer.flags.ignore_mandatory = True
customer.save() customer.save()
frappe.db.commit()
if customer_status == 1: if customer_exists:
frappe.rename_doc("Customer", old_name, full_name) frappe.rename_doc("Customer", old_name, customer_name)
address = frappe.get_doc("Address",{"woocommerce_email":customer_woo_com_email}) address = frappe.get_doc("Address", {"woocommerce_email": customer_woo_com_email})
customer = frappe.get_doc("Customer",{"woocommerce_email": customer_woo_com_email}) else:
address = frappe.new_doc("Address")
address.address_line1 = raw_billing_data.get("address_1", "Not Provided") address.address_line1 = raw_billing_data.get("address_1", "Not Provided")
address.address_line2 = raw_billing_data.get("address_2", "Not Provided") address.address_line2 = raw_billing_data.get("address_2", "Not Provided")
address.city = raw_billing_data.get("city", "Not Provided") address.city = raw_billing_data.get("city", "Not Provided")
address.woocommerce_email = str(raw_billing_data.get("email")) address.woocommerce_email = customer_woo_com_email
address.address_type = "Shipping" address.address_type = "Billing"
address.country = frappe.get_value("Country", filters={"code":raw_billing_data.get("country", "IN").lower()}) address.country = frappe.get_value("Country", {"code": raw_billing_data.get("country", "IN").lower()})
address.state = raw_billing_data.get("state") address.state = raw_billing_data.get("state")
address.pincode = str(raw_billing_data.get("postcode")) address.pincode = raw_billing_data.get("postcode")
address.phone = str(raw_billing_data.get("phone")) address.phone = raw_billing_data.get("phone")
address.email_id = str(raw_billing_data.get("email")) address.email_id = customer_woo_com_email
address.append("links", { address.append("links", {
"link_doctype": "Customer", "link_doctype": "Customer",
"link_name": customer.customer_name "link_name": customer.customer_name
}) })
address.flags.ignore_mandatory = True
address = address.save()
address.save() if customer_exists:
frappe.db.commit()
if customer_status == 1:
address = frappe.get_doc("Address",{"woocommerce_email":customer_woo_com_email})
old_address_title = address.name old_address_title = address.name
new_address_title = customer.customer_name+"-billing" new_address_title = customer.customer_name + "-billing"
address.address_title = customer.customer_name address.address_title = customer.customer_name
address.save() address.save()
frappe.rename_doc("Address",old_address_title,new_address_title) frappe.rename_doc("Address", old_address_title, new_address_title)
frappe.db.commit() def link_items(items_list, woocommerce_settings):
for item_data in items_list:
def link_item(item_data,item_status):
woocommerce_settings = frappe.get_doc("Woocommerce Settings")
if item_status == 0:
#Create Item
item = frappe.new_doc("Item")
if item_status == 1:
#Edit Item
item_woo_com_id = item_data.get("product_id") item_woo_com_id = item_data.get("product_id")
item = frappe.get_doc("Item",{"woocommerce_id": item_woo_com_id})
item.item_name = str(item_data.get("name")) if frappe.get_value("Item", {"woocommerce_id": item_woo_com_id}):
item.item_code = "woocommerce - " + str(item_data.get("product_id")) #Edit Item
item.woocommerce_id = str(item_data.get("product_id")) item = frappe.get_doc("Item", {"woocommerce_id": item_woo_com_id})
item.item_group = _("WooCommerce Products") else:
item.stock_uom = woocommerce_settings.uom or _("Nos") #Create Item
item.save() item = frappe.new_doc("Item")
item.item_name = item_data.get("name")
item.item_code = _("woocommerce - {0}").format(item_data.get("product_id"))
item.woocommerce_id = item_data.get("product_id")
item.item_group = _("WooCommerce Products")
item.stock_uom = woocommerce_settings.uom or _("Nos")
item.flags.ignore_mandatory = True
item.save()
def create_sales_order(order, woocommerce_settings, customer_name):
new_sales_order = frappe.new_doc("Sales Order")
new_sales_order.customer = customer_name
new_sales_order.po_no = new_sales_order.woocommerce_id = order.get("id")
new_sales_order.naming_series = woocommerce_settings.sales_order_series or "SO-WOO-"
created_date = order.get("date_created").split("T")
new_sales_order.transaction_date = created_date[0]
delivery_after = woocommerce_settings.delivery_after_days or 7
new_sales_order.delivery_date = frappe.utils.add_days(created_date[0], delivery_after)
new_sales_order.company = woocommerce_settings.company
set_items_in_sales_order(new_sales_order, woocommerce_settings, order)
new_sales_order.flags.ignore_mandatory = True
new_sales_order.insert()
new_sales_order.submit()
frappe.db.commit() frappe.db.commit()
def add_tax_details(sales_order,price,desc,status): def set_items_in_sales_order(new_sales_order, woocommerce_settings, order):
company_abbr = frappe.db.get_value('Company', woocommerce_settings.company, 'abbr')
woocommerce_settings = frappe.get_doc("Woocommerce Settings") for item in order.get("line_items"):
woocomm_item_id = item.get("product_id")
found_item = frappe.get_doc("Item", {"woocommerce_id": woocomm_item_id})
if status == 0: ordered_items_tax = item.get("total_tax")
# Product taxes
account_head_type = woocommerce_settings.tax_account
if status == 1: new_sales_order.append("items",{
# Shipping taxes "item_code": found_item.item_code,
account_head_type = woocommerce_settings.f_n_f_account "item_name": found_item.item_name,
"description": found_item.item_name,
"delivery_date": new_sales_order.delivery_date,
"uom": woocommerce_settings.uom or _("Nos"),
"qty": item.get("quantity"),
"rate": item.get("price"),
"warehouse": woocommerce_settings.warehouse or _("Stores - {0}").format(company_abbr)
})
sales_order.append("taxes",{ add_tax_details(new_sales_order, ordered_items_tax, "Ordered Item tax", woocommerce_settings.tax_account)
"charge_type":"Actual",
"account_head": account_head_type, # shipping_details = order.get("shipping_lines") # used for detailed order
"tax_amount": price,
"description": desc add_tax_details(new_sales_order, order.get("shipping_tax"), "Shipping Tax", woocommerce_settings.f_n_f_account)
}) add_tax_details(new_sales_order, order.get("shipping_total"), "Shipping Total", woocommerce_settings.f_n_f_account)
def add_tax_details(sales_order, price, desc, tax_account_head):
sales_order.append("taxes", {
"charge_type":"Actual",
"account_head": tax_account_head,
"tax_amount": price,
"description": desc
})

View File

@@ -39,7 +39,7 @@ def get_message(exception):
if hasattr(exception, 'message'): if hasattr(exception, 'message'):
message = exception.message message = exception.message
elif hasattr(exception, '__str__'): elif hasattr(exception, '__str__'):
message = e.__str__() message = exception.__str__()
else: else:
message = "Something went wrong while syncing" message = "Something went wrong while syncing"
return message return message

View File

@@ -43,14 +43,14 @@ class ShopifySettings(Document):
d.raise_for_status() d.raise_for_status()
self.update_webhook_table(method, d.json()) self.update_webhook_table(method, d.json())
except Exception as e: except Exception as e:
make_shopify_log(status="Warning", message=e, exception=False) make_shopify_log(status="Warning", exception=e, rollback=True)
def unregister_webhooks(self): def unregister_webhooks(self):
session = get_request_session() session = get_request_session()
deleted_webhooks = [] deleted_webhooks = []
for d in self.webhooks: for d in self.webhooks:
url = get_shopify_url('admin/api/2019-04/webhooks.json'.format(d.webhook_id), self) url = get_shopify_url('admin/api/2019-04/webhooks/{0}.json'.format(d.webhook_id), self)
try: try:
res = session.delete(url, headers=get_header(self)) res = session.delete(url, headers=get_header(self))
res.raise_for_status() res.raise_for_status()

View File

@@ -1,694 +1,175 @@
{ {
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2018-02-12 15:10:05.495713", "creation": "2018-02-12 15:10:05.495713",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"enable_sync",
"sb_00",
"woocommerce_server_url",
"secret",
"cb_00",
"api_consumer_key",
"api_consumer_secret",
"sb_accounting_details",
"tax_account",
"column_break_10",
"f_n_f_account",
"defaults_section",
"creation_user",
"warehouse",
"sales_order_series",
"column_break_14",
"company",
"delivery_after_days",
"uom",
"endpoints",
"endpoint"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "enable_sync", "fieldname": "enable_sync",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Enable Sync"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Enable Sync",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "sb_00", "fieldname": "sb_00",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "woocommerce_server_url", "fieldname": "woocommerce_server_url",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Woocommerce Server URL"
"label": "Woocommerce Server URL",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "secret", "fieldname": "secret",
"fieldtype": "Code", "fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Secret", "label": "Secret",
"length": 0, "read_only": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "cb_00", "fieldname": "cb_00",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "api_consumer_key", "fieldname": "api_consumer_key",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "API consumer key"
"label": "API consumer key",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "api_consumer_secret", "fieldname": "api_consumer_secret",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "API consumer secret"
"label": "API consumer secret",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "sb_accounting_details", "fieldname": "sb_accounting_details",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Accounting Details"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Accounting Details",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "tax_account", "fieldname": "tax_account",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Tax Account", "label": "Tax Account",
"length": 0,
"no_copy": 0,
"options": "Account", "options": "Account",
"permlevel": 0, "reqd": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_10", "fieldname": "column_break_10",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "f_n_f_account", "fieldname": "f_n_f_account",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Freight and Forwarding Account", "label": "Freight and Forwarding Account",
"length": 0,
"no_copy": 0,
"options": "Account", "options": "Account",
"permlevel": 0, "reqd": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "defaults_section", "fieldname": "defaults_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Defaults"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Defaults",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "The user that will be used to create Customers, Items and Sales Orders. This user should have the relevant permissions.", "description": "The user that will be used to create Customers, Items and Sales Orders. This user should have the relevant permissions.",
"fetch_if_empty": 0,
"fieldname": "creation_user", "fieldname": "creation_user",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Creation User", "label": "Creation User",
"length": 0,
"no_copy": 0,
"options": "User", "options": "User",
"permlevel": 0, "reqd": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "description": "This warehouse will be used to create Sales Orders. The fallback warehouse is \"Stores\".",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "This warehouse will be used to create Sale Orders. The fallback warehouse is \"Stores\".",
"fetch_if_empty": 0,
"fieldname": "warehouse", "fieldname": "warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Warehouse", "label": "Warehouse",
"length": 0, "options": "Warehouse"
"no_copy": 0,
"options": "Warehouse",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_14", "fieldname": "column_break_14",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "The fallback series is \"SO-WOO-\".", "description": "The fallback series is \"SO-WOO-\".",
"fetch_if_empty": 0,
"fieldname": "sales_order_series", "fieldname": "sales_order_series",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0, "label": "Sales Order Series"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Sales Order Series",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "This is the default UOM used for items and Sales orders. The fallback UOM is \"Nos\".", "description": "This is the default UOM used for items and Sales orders. The fallback UOM is \"Nos\".",
"fetch_if_empty": 0,
"fieldname": "uom", "fieldname": "uom",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "UOM", "label": "UOM",
"length": 0, "options": "UOM"
"no_copy": 0,
"options": "UOM",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "endpoints", "fieldname": "endpoints",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Endpoints"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Endpoints",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "endpoint", "fieldname": "endpoint",
"fieldtype": "Code", "fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Endpoint", "label": "Endpoint",
"length": 0, "read_only": 1
"no_copy": 0, },
"permlevel": 0, {
"precision": "", "description": "This company will be used to create Sales Orders.",
"print_hide": 0, "fieldname": "company",
"print_hide_if_no_value": 0, "fieldtype": "Link",
"read_only": 1, "label": "Company",
"remember_last_selected_value": 0, "options": "Company",
"report_hide": 0, "reqd": 1
"reqd": 0, },
"search_index": 0, {
"set_only_once": 0, "description": "This is the default offset (days) for the Delivery Date in Sales Orders. The fallback offset is 7 days from the order placement date.",
"translatable": 0, "fieldname": "delivery_after_days",
"unique": 0 "fieldtype": "Int",
"label": "Delivery After (Days)"
} }
], ],
"has_web_view": 0,
"hide_toolbar": 0,
"idx": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1, "issingle": 1,
"istable": 0, "modified": "2019-11-04 00:45:21.232096",
"max_attachments": 0,
"menu_index": 0,
"modified": "2019-04-08 17:04:16.720696",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "ERPNext Integrations", "module": "ERPNext Integrations",
"name": "Woocommerce Settings", "name": "Woocommerce Settings",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 0,
"email": 1, "email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 0,
"role": "System Manager", "role": "System Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1, "track_changes": 1
"track_seen": 0,
"track_views": 0
} }

View File

@@ -8,6 +8,7 @@ from frappe import _
from frappe.utils.nestedset import get_root_of from frappe.utils.nestedset import get_root_of
from frappe.model.document import Document from frappe.model.document import Document
from six.moves.urllib.parse import urlparse from six.moves.urllib.parse import urlparse
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
class WoocommerceSettings(Document): class WoocommerceSettings(Document):
def validate(self): def validate(self):
@@ -17,75 +18,21 @@ class WoocommerceSettings(Document):
def create_delete_custom_fields(self): def create_delete_custom_fields(self):
if self.enable_sync: if self.enable_sync:
custom_fields = {}
# create # create
create_custom_field_id_and_check_status = False for doctype in ["Customer", "Sales Order", "Item", "Address"]:
create_custom_field_email_check = False df = dict(fieldname='woocommerce_id', label='Woocommerce ID', fieldtype='Data', read_only=1, print_hide=1)
names = ["Customer-woocommerce_id","Sales Order-woocommerce_id","Item-woocommerce_id","Address-woocommerce_id"] create_custom_field(doctype, df)
names_check_box = ["Customer-woocommerce_check","Sales Order-woocommerce_check","Item-woocommerce_check","Address-woocommerce_check"]
email_names = ["Customer-woocommerce_email","Address-woocommerce_email"]
for i in zip(names,names_check_box): for doctype in ["Customer", "Address"]:
df = dict(fieldname='woocommerce_email', label='Woocommerce Email', fieldtype='Data', read_only=1, print_hide=1)
if not frappe.get_value("Custom Field",{"name":i[0]}) or not frappe.get_value("Custom Field",{"name":i[1]}): create_custom_field(doctype, df)
create_custom_field_id_and_check_status = True
break if not frappe.get_value("Item Group", {"name": _("WooCommerce Products")}):
if create_custom_field_id_and_check_status:
names = ["Customer","Sales Order","Item","Address"]
for name in names:
custom = frappe.new_doc("Custom Field")
custom.dt = name
custom.label = "woocommerce_id"
custom.read_only = 1
custom.save()
custom = frappe.new_doc("Custom Field")
custom.dt = name
custom.label = "woocommerce_check"
custom.fieldtype = "Check"
custom.read_only = 1
custom.save()
for i in email_names:
if not frappe.get_value("Custom Field",{"name":i}):
create_custom_field_email_check = True
break;
if create_custom_field_email_check:
names = ["Customer","Address"]
for name in names:
custom = frappe.new_doc("Custom Field")
custom.dt = name
custom.label = "woocommerce_email"
custom.read_only = 1
custom.save()
if not frappe.get_value("Item Group",{"name": _("WooCommerce Products")}):
item_group = frappe.new_doc("Item Group") item_group = frappe.new_doc("Item Group")
item_group.item_group_name = _("WooCommerce Products") item_group.item_group_name = _("WooCommerce Products")
item_group.parent_item_group = get_root_of("Item Group") item_group.parent_item_group = get_root_of("Item Group")
item_group.save() item_group.insert()
elif not self.enable_sync:
# delete
names = ["Customer-woocommerce_id","Sales Order-woocommerce_id","Item-woocommerce_id","Address-woocommerce_id"]
names_check_box = ["Customer-woocommerce_check","Sales Order-woocommerce_check","Item-woocommerce_check","Address-woocommerce_check"]
email_names = ["Customer-woocommerce_email","Address-woocommerce_email"]
for name in names:
frappe.delete_doc("Custom Field",name)
for name in names_check_box:
frappe.delete_doc("Custom Field",name)
for name in email_names:
frappe.delete_doc("Custom Field",name)
frappe.delete_doc("Item Group", _("WooCommerce Products"))
frappe.db.commit()
def validate_settings(self): def validate_settings(self):
if self.enable_sync: if self.enable_sync:

View File

@@ -235,17 +235,16 @@ doc_events = {
("Sales Taxes and Charges Template", 'Price List'): { ("Sales Taxes and Charges Template", 'Price List'): {
"on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings" "on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings"
}, },
"Website Settings": { "Website Settings": {
"validate": "erpnext.portal.doctype.products_settings.products_settings.home_page_is_products" "validate": "erpnext.portal.doctype.products_settings.products_settings.home_page_is_products"
}, },
"Sales Invoice": { "Sales Invoice": {
"on_submit": ["erpnext.regional.france.utils.create_transaction_log", "erpnext.regional.italy.utils.sales_invoice_on_submit"], "on_submit": ["erpnext.regional.create_transaction_log", "erpnext.regional.italy.utils.sales_invoice_on_submit"],
"on_cancel": "erpnext.regional.italy.utils.sales_invoice_on_cancel", "on_cancel": "erpnext.regional.italy.utils.sales_invoice_on_cancel",
"on_trash": "erpnext.regional.check_deletion_permission" "on_trash": "erpnext.regional.check_deletion_permission"
}, },
"Payment Entry": { "Payment Entry": {
"on_submit": ["erpnext.regional.france.utils.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.make_status_as_paid"], "on_submit": ["erpnext.regional.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.make_status_as_paid"],
"on_trash": "erpnext.regional.check_deletion_permission" "on_trash": "erpnext.regional.check_deletion_permission"
}, },
'Address': { 'Address': {
@@ -283,7 +282,6 @@ scheduler_events = {
], ],
"daily": [ "daily": [
"erpnext.stock.reorder_item.reorder_item", "erpnext.stock.reorder_item.reorder_item",
"erpnext.setup.doctype.email_digest.email_digest.send",
"erpnext.support.doctype.issue.issue.auto_close_tickets", "erpnext.support.doctype.issue.issue.auto_close_tickets",
"erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity", "erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity",
"erpnext.controllers.accounts_controller.update_invoice_status", "erpnext.controllers.accounts_controller.update_invoice_status",
@@ -306,6 +304,7 @@ scheduler_events = {
"erpnext.crm.doctype.email_campaign.email_campaign.set_email_campaign_status" "erpnext.crm.doctype.email_campaign.email_campaign.set_email_campaign_status"
], ],
"daily_long": [ "daily_long": [
"erpnext.setup.doctype.email_digest.email_digest.send",
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms", "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms",
"erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation", "erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation",
"erpnext.hr.utils.generate_leave_encashment" "erpnext.hr.utils.generate_leave_encashment"

View File

@@ -19,14 +19,19 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters):
approvers = [] approvers = []
department_details = {} department_details = {}
department_list = [] 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)
employee_department = filters.get("department") or employee.department
if employee_department: if employee_department:
department_details = frappe.db.get_value("Department", {"name": employee_department}, ["lft", "rgt"], as_dict=True) department_details = frappe.db.get_value("Department", {"name": employee_department}, ["lft", "rgt"], as_dict=True)
if department_details: if department_details:
department_list = frappe.db.sql("""select name from `tabDepartment` where lft <= %s department_list = frappe.db.sql("""select name from `tabDepartment` where lft <= %s
and rgt >= %s and rgt >= %s
and disabled=0 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" and employee.leave_approver:
approvers.append(frappe.db.get_value("User", employee.leave_approver, ['name', 'first_name', 'last_name']))
if filters.get("doctype") == "Leave Application": if filters.get("doctype") == "Leave Application":
parentfield = "leave_approvers" parentfield = "leave_approvers"
@@ -41,4 +46,4 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters):
and approver.parentfield = %s and approver.parentfield = %s
and approver.approver=user.name""",(d, "%" + txt + "%", parentfield), as_list=True) and approver.approver=user.name""",(d, "%" + txt + "%", parentfield), as_list=True)
return approvers return set(tuple(approver) for approver in approvers)

View File

@@ -167,10 +167,11 @@ class Employee(NestedSet):
def validate_status(self): def validate_status(self):
if self.status == 'Left': if self.status == 'Left':
reports_to = frappe.db.get_all('Employee', reports_to = frappe.db.get_all('Employee',
filters={'reports_to': self.name} filters={'reports_to': self.name, 'status': "Active"},
fields=['name','employee_name']
) )
if reports_to: if reports_to:
link_to_employees = [frappe.utils.get_link_to_form('Employee', employee.name) for employee in reports_to] link_to_employees = [frappe.utils.get_link_to_form('Employee', employee.name, label=employee.employee_name) for employee in reports_to]
throw(_("Employee status cannot be set to 'Left' as following employees are currently reporting to this employee:&nbsp;") throw(_("Employee status cannot be set to 'Left' as following employees are currently reporting to this employee:&nbsp;")
+ ', '.join(link_to_employees), EmployeeLeftValidationError) + ', '.join(link_to_employees), EmployeeLeftValidationError)
if not self.relieving_date: if not self.relieving_date:

View File

@@ -21,7 +21,7 @@ def get_data():
}, },
{ {
'label': _('Expense'), 'label': _('Expense'),
'items': ['Expense Claim', 'Travel Request'] 'items': ['Expense Claim', 'Travel Request', 'Employee Advance']
}, },
{ {
'label': _('Benefit'), 'label': _('Benefit'),

View File

@@ -45,7 +45,7 @@ class TestEmployee(unittest.TestCase):
employee1_doc.status = 'Left' employee1_doc.status = 'Left'
self.assertRaises(EmployeeLeftValidationError, employee1_doc.save) self.assertRaises(EmployeeLeftValidationError, employee1_doc.save)
def make_employee(user): def make_employee(user, company=None):
if not frappe.db.get_value("User", user): if not frappe.db.get_value("User", user):
frappe.get_doc({ frappe.get_doc({
"doctype": "User", "doctype": "User",
@@ -55,12 +55,12 @@ def make_employee(user):
"roles": [{"doctype": "Has Role", "role": "Employee"}] "roles": [{"doctype": "Has Role", "role": "Employee"}]
}).insert() }).insert()
if not frappe.db.get_value("Employee", {"user_id": user}): if not frappe.db.get_value("Employee", { "user_id": user, "company": company or erpnext.get_default_company() }):
employee = frappe.get_doc({ employee = frappe.get_doc({
"doctype": "Employee", "doctype": "Employee",
"naming_series": "EMP-", "naming_series": "EMP-",
"first_name": user, "first_name": user,
"company": erpnext.get_default_company(), "company": company or erpnext.get_default_company(),
"user_id": user, "user_id": user,
"date_of_birth": "1990-05-08", "date_of_birth": "1990-05-08",
"date_of_joining": "2013-01-01", "date_of_joining": "2013-01-01",

View File

@@ -1,435 +1,436 @@
{ {
"allow_import": 1, "allow_import": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2013-01-10 16:34:14", "creation": "2013-01-10 16:34:14",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"naming_series", "naming_series",
"employee", "employee",
"employee_name", "employee_name",
"department", "department",
"column_break_5", "column_break_5",
"expense_approver", "expense_approver",
"approval_status", "approval_status",
"is_paid", "is_paid",
"expense_details", "expense_details",
"expenses", "expenses",
"sb1", "sb1",
"taxes", "taxes",
"transactions_section", "transactions_section",
"total_sanctioned_amount", "total_sanctioned_amount",
"total_taxes_and_charges", "total_taxes_and_charges",
"total_advance_amount", "total_advance_amount",
"column_break_17", "column_break_17",
"grand_total", "grand_total",
"total_claimed_amount", "total_claimed_amount",
"total_amount_reimbursed", "total_amount_reimbursed",
"section_break_16", "section_break_16",
"posting_date", "posting_date",
"vehicle_log", "vehicle_log",
"task", "task",
"cb1", "cb1",
"remark", "remark",
"title", "title",
"email_id", "email_id",
"accounting_details", "accounting_details",
"company", "company",
"mode_of_payment", "mode_of_payment",
"clearance_date", "clearance_date",
"column_break_24", "column_break_24",
"payable_account", "payable_account",
"accounting_dimensions_section", "accounting_dimensions_section",
"project", "project",
"dimension_col_break", "dimension_col_break",
"cost_center", "cost_center",
"more_details", "more_details",
"status", "status",
"amended_from", "amended_from",
"advance_payments", "advance_payments",
"advances" "advances"
], ],
"fields": [ "fields": [
{ {
"fieldname": "naming_series", "fieldname": "naming_series",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Series", "label": "Series",
"no_copy": 1, "no_copy": 1,
"options": "HR-EXP-.YYYY.-", "options": "HR-EXP-.YYYY.-",
"print_hide": 1, "print_hide": 1,
"reqd": 1, "reqd": 1,
"set_only_once": 1 "set_only_once": 1
}, },
{ {
"fieldname": "employee", "fieldname": "employee",
"fieldtype": "Link", "fieldtype": "Link",
"in_global_search": 1, "in_global_search": 1,
"label": "From Employee", "label": "From Employee",
"oldfieldname": "employee", "oldfieldname": "employee",
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "Employee", "options": "Employee",
"reqd": 1, "reqd": 1,
"search_index": 1 "search_index": 1
}, },
{ {
"fetch_from": "employee.employee_name", "fetch_from": "employee.employee_name",
"fieldname": "employee_name", "fieldname": "employee_name",
"fieldtype": "Data", "fieldtype": "Data",
"in_global_search": 1, "in_global_search": 1,
"label": "Employee Name", "label": "Employee Name",
"oldfieldname": "employee_name", "oldfieldname": "employee_name",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"read_only": 1, "read_only": 1,
"width": "150px" "width": "150px"
}, },
{ {
"fetch_from": "employee.department", "fetch_from": "employee.department",
"fieldname": "department", "fieldname": "department",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Department", "label": "Department",
"options": "Department", "options": "Department",
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "column_break_5", "fieldname": "column_break_5",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"fieldname": "expense_approver", "fieldname": "expense_approver",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Expense Approver", "label": "Expense Approver",
"options": "User" "options": "User"
}, },
{ {
"default": "Draft", "default": "Draft",
"fieldname": "approval_status", "fieldname": "approval_status",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Approval Status", "label": "Approval Status",
"no_copy": 1, "no_copy": 1,
"options": "Draft\nApproved\nRejected", "options": "Draft\nApproved\nRejected",
"search_index": 1 "search_index": 1
}, },
{ {
"fieldname": "total_claimed_amount", "fieldname": "total_claimed_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1, "in_list_view": 1,
"label": "Total Claimed Amount", "label": "Total Claimed Amount",
"no_copy": 1, "no_copy": 1,
"oldfieldname": "total_claimed_amount", "oldfieldname": "total_claimed_amount",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1, "read_only": 1,
"width": "160px" "width": "160px"
}, },
{ {
"fieldname": "total_sanctioned_amount", "fieldname": "total_sanctioned_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Total Sanctioned Amount", "label": "Total Sanctioned Amount",
"no_copy": 1, "no_copy": 1,
"oldfieldname": "total_sanctioned_amount", "oldfieldname": "total_sanctioned_amount",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1, "read_only": 1,
"width": "160px" "width": "160px"
}, },
{ {
"default": "0", "default": "0",
"depends_on": "eval:(doc.docstatus==0 || doc.is_paid)", "depends_on": "eval:(doc.docstatus==0 || doc.is_paid)",
"fieldname": "is_paid", "fieldname": "is_paid",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Paid" "label": "Is Paid"
}, },
{ {
"fieldname": "expense_details", "fieldname": "expense_details",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"oldfieldtype": "Section Break" "oldfieldtype": "Section Break"
}, },
{ {
"fieldname": "expenses", "fieldname": "expenses",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Expenses", "label": "Expenses",
"oldfieldname": "expense_voucher_details", "oldfieldname": "expense_voucher_details",
"oldfieldtype": "Table", "oldfieldtype": "Table",
"options": "Expense Claim Detail", "options": "Expense Claim Detail",
"reqd": 1 "reqd": 1
}, },
{ {
"fieldname": "sb1", "fieldname": "sb1",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"options": "Simple" "options": "Simple"
}, },
{ {
"default": "Today", "default": "Today",
"fieldname": "posting_date", "fieldname": "posting_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Posting Date", "label": "Posting Date",
"oldfieldname": "posting_date", "oldfieldname": "posting_date",
"oldfieldtype": "Date", "oldfieldtype": "Date",
"reqd": 1 "reqd": 1
}, },
{ {
"fieldname": "vehicle_log", "fieldname": "vehicle_log",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Vehicle Log", "label": "Vehicle Log",
"options": "Vehicle Log", "options": "Vehicle Log",
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "project", "fieldname": "project",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Project", "label": "Project",
"options": "Project" "options": "Project"
}, },
{ {
"fieldname": "task", "fieldname": "task",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Task", "label": "Task",
"options": "Task", "options": "Task",
"remember_last_selected_value": 1 "remember_last_selected_value": 1
}, },
{ {
"fieldname": "cb1", "fieldname": "cb1",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"fieldname": "total_amount_reimbursed", "fieldname": "total_amount_reimbursed",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1, "in_list_view": 1,
"label": "Total Amount Reimbursed", "label": "Total Amount Reimbursed",
"no_copy": 1, "no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "remark", "fieldname": "remark",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Remark", "label": "Remark",
"no_copy": 1, "no_copy": 1,
"oldfieldname": "remark", "oldfieldname": "remark",
"oldfieldtype": "Small Text" "oldfieldtype": "Small Text"
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"default": "{employee_name}", "default": "{employee_name}",
"fieldname": "title", "fieldname": "title",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Title", "label": "Title",
"no_copy": 1 "no_copy": 1
}, },
{ {
"fieldname": "email_id", "fieldname": "email_id",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Employees Email Id", "label": "Employees Email Id",
"oldfieldname": "email_id", "oldfieldname": "email_id",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"print_hide": 1 "print_hide": 1
}, },
{ {
"fieldname": "accounting_details", "fieldname": "accounting_details",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Accounting Details" "label": "Accounting Details"
}, },
{ {
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Company", "label": "Company",
"oldfieldname": "company", "oldfieldname": "company",
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "Company", "options": "Company",
"remember_last_selected_value": 1, "remember_last_selected_value": 1,
"reqd": 1 "reqd": 1
}, },
{ {
"depends_on": "is_paid", "depends_on": "is_paid",
"fieldname": "mode_of_payment", "fieldname": "mode_of_payment",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Mode of Payment", "label": "Mode of Payment",
"options": "Mode of Payment" "options": "Mode of Payment"
}, },
{ {
"fieldname": "clearance_date", "fieldname": "clearance_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Clearance Date" "label": "Clearance Date"
}, },
{ {
"fieldname": "column_break_24", "fieldname": "column_break_24",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"fieldname": "payable_account", "fieldname": "payable_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Payable Account", "label": "Payable Account",
"options": "Account" "options": "Account",
}, "reqd": 1
{ },
"fieldname": "cost_center", {
"fieldtype": "Link", "fieldname": "cost_center",
"label": "Cost Center", "fieldtype": "Link",
"options": "Cost Center" "label": "Cost Center",
}, "options": "Cost Center"
{ },
"collapsible": 1, {
"fieldname": "more_details", "collapsible": 1,
"fieldtype": "Section Break", "fieldname": "more_details",
"label": "More Details" "fieldtype": "Section Break",
}, "label": "More Details"
{ },
"default": "Draft", {
"fieldname": "status", "default": "Draft",
"fieldtype": "Select", "fieldname": "status",
"in_list_view": 1, "fieldtype": "Select",
"label": "Status", "in_list_view": 1,
"no_copy": 1, "label": "Status",
"options": "Draft\nPaid\nUnpaid\nRejected\nSubmitted\nCancelled", "no_copy": 1,
"print_hide": 1, "options": "Draft\nPaid\nUnpaid\nRejected\nSubmitted\nCancelled",
"read_only": 1 "print_hide": 1,
}, "read_only": 1
{ },
"fieldname": "amended_from", {
"fieldtype": "Link", "fieldname": "amended_from",
"ignore_user_permissions": 1, "fieldtype": "Link",
"label": "Amended From", "ignore_user_permissions": 1,
"no_copy": 1, "label": "Amended From",
"oldfieldname": "amended_from", "no_copy": 1,
"oldfieldtype": "Data", "oldfieldname": "amended_from",
"options": "Expense Claim", "oldfieldtype": "Data",
"print_hide": 1, "options": "Expense Claim",
"read_only": 1, "print_hide": 1,
"report_hide": 1, "read_only": 1,
"width": "160px" "report_hide": 1,
}, "width": "160px"
{ },
"fieldname": "advance_payments", {
"fieldtype": "Section Break", "fieldname": "advance_payments",
"label": "Advance Payments" "fieldtype": "Section Break",
}, "label": "Advance Payments"
{ },
"fieldname": "advances", {
"fieldtype": "Table", "fieldname": "advances",
"label": "Advances", "fieldtype": "Table",
"options": "Expense Claim Advance" "label": "Advances",
}, "options": "Expense Claim Advance"
{ },
"fieldname": "total_advance_amount", {
"fieldtype": "Currency", "fieldname": "total_advance_amount",
"label": "Total Advance Amount", "fieldtype": "Currency",
"options": "Company:company:default_currency", "label": "Total Advance Amount",
"read_only": 1 "options": "Company:company:default_currency",
}, "read_only": 1
{ },
"fieldname": "accounting_dimensions_section", {
"fieldtype": "Section Break", "fieldname": "accounting_dimensions_section",
"label": "Accounting Dimensions" "fieldtype": "Section Break",
}, "label": "Accounting Dimensions"
{ },
"fieldname": "dimension_col_break", {
"fieldtype": "Column Break" "fieldname": "dimension_col_break",
}, "fieldtype": "Column Break"
{ },
"fieldname": "taxes", {
"fieldtype": "Table", "fieldname": "taxes",
"label": "Expense Taxes and Charges", "fieldtype": "Table",
"options": "Expense Taxes and Charges" "label": "Expense Taxes and Charges",
}, "options": "Expense Taxes and Charges"
{ },
"fieldname": "section_break_16", {
"fieldtype": "Section Break" "fieldname": "section_break_16",
}, "fieldtype": "Section Break"
{ },
"fieldname": "transactions_section", {
"fieldtype": "Section Break" "fieldname": "transactions_section",
}, "fieldtype": "Section Break"
{ },
"fieldname": "grand_total", {
"fieldtype": "Currency", "fieldname": "grand_total",
"in_list_view": 1, "fieldtype": "Currency",
"label": "Grand Total", "in_list_view": 1,
"options": "Company:company:default_currency", "label": "Grand Total",
"read_only": 1 "options": "Company:company:default_currency",
}, "read_only": 1
{ },
"fieldname": "column_break_17", {
"fieldtype": "Column Break" "fieldname": "column_break_17",
}, "fieldtype": "Column Break"
{ },
"fieldname": "total_taxes_and_charges", {
"fieldtype": "Currency", "fieldname": "total_taxes_and_charges",
"label": "Total Taxes and Charges", "fieldtype": "Currency",
"options": "Company:company:default_currency", "label": "Total Taxes and Charges",
"read_only": 1 "options": "Company:company:default_currency",
} "read_only": 1
], }
"icon": "fa fa-money", ],
"idx": 1, "icon": "fa fa-money",
"is_submittable": 1, "idx": 1,
"modified": "2019-06-26 18:05:52.530462", "is_submittable": 1,
"modified_by": "Administrator", "modified": "2019-11-08 14:13:08.964547",
"module": "HR", "modified_by": "Administrator",
"name": "Expense Claim", "module": "HR",
"name_case": "Title Case", "name": "Expense Claim",
"owner": "harshada@webnotestech.com", "name_case": "Title Case",
"permissions": [ "owner": "harshada@webnotestech.com",
{ "permissions": [
"amend": 1, {
"cancel": 1, "amend": 1,
"create": 1, "cancel": 1,
"delete": 1, "create": 1,
"email": 1, "delete": 1,
"export": 1, "email": 1,
"print": 1, "export": 1,
"read": 1, "print": 1,
"report": 1, "read": 1,
"role": "HR Manager", "report": 1,
"share": 1, "role": "HR Manager",
"submit": 1, "share": 1,
"write": 1 "submit": 1,
}, "write": 1
{ },
"create": 1, {
"email": 1, "create": 1,
"print": 1, "email": 1,
"read": 1, "print": 1,
"report": 1, "read": 1,
"role": "Employee", "report": 1,
"share": 1, "role": "Employee",
"write": 1 "share": 1,
}, "write": 1
{ },
"amend": 1, {
"cancel": 1, "amend": 1,
"create": 1, "cancel": 1,
"delete": 1, "create": 1,
"email": 1, "delete": 1,
"print": 1, "email": 1,
"read": 1, "print": 1,
"report": 1, "read": 1,
"role": "Expense Approver", "report": 1,
"share": 1, "role": "Expense Approver",
"submit": 1, "share": 1,
"write": 1 "submit": 1,
}, "write": 1
{ },
"amend": 1, {
"cancel": 1, "amend": 1,
"create": 1, "cancel": 1,
"delete": 1, "create": 1,
"email": 1, "delete": 1,
"print": 1, "email": 1,
"read": 1, "print": 1,
"report": 1, "read": 1,
"role": "HR User", "report": 1,
"share": 1, "role": "HR User",
"submit": 1, "share": 1,
"write": 1 "submit": 1,
} "write": 1
], }
"search_fields": "employee,employee_name", ],
"show_name_in_global_search": 1, "search_fields": "employee,employee_name",
"sort_field": "modified", "show_name_in_global_search": 1,
"sort_order": "DESC", "sort_field": "modified",
"timeline_field": "employee", "sort_order": "DESC",
"title_field": "title" "timeline_field": "employee",
} "title_field": "title"
}

View File

@@ -140,10 +140,11 @@ class ExpenseClaim(AccountsController):
"against": ",".join([d.default_account for d in self.expenses]), "against": ",".join([d.default_account for d in self.expenses]),
"party_type": "Employee", "party_type": "Employee",
"party": self.employee, "party": self.employee,
"against_voucher_type": self.doctype, "against_voucher_type": "Employee Advance",
"against_voucher": self.name "against_voucher": data.employee_advance
}) })
) )
self.add_tax_gl_entries(gl_entry) self.add_tax_gl_entries(gl_entry)
if self.is_paid and self.grand_total: if self.is_paid and self.grand_total:
@@ -192,9 +193,6 @@ class ExpenseClaim(AccountsController):
if not self.cost_center: if not self.cost_center:
frappe.throw(_("Cost center is required to book an expense claim")) frappe.throw(_("Cost center is required to book an expense claim"))
if not self.payable_account:
frappe.throw(_("Please set default payable account for the company {0}").format(getlink("Company",self.company)))
if self.is_paid: if self.is_paid:
if not self.mode_of_payment: if not self.mode_of_payment:
frappe.throw(_("Mode of payment is required to make a payment").format(self.employee)) frappe.throw(_("Mode of payment is required to make a payment").format(self.employee))

View File

@@ -2,11 +2,11 @@ frappe.listview_settings['Expense Claim'] = {
add_fields: ["total_claimed_amount", "docstatus"], add_fields: ["total_claimed_amount", "docstatus"],
get_indicator: function(doc) { get_indicator: function(doc) {
if(doc.status == "Paid") { if(doc.status == "Paid") {
return [__("Paid"), "green", "status,=,'Paid'"]; return [__("Paid"), "green", "status,=,Paid"];
}else if(doc.status == "Unpaid") { }else if(doc.status == "Unpaid") {
return [__("Unpaid"), "orange"]; return [__("Unpaid"), "orange", "status,=,Unpaid"];
} else if(doc.status == "Rejected") { } else if(doc.status == "Rejected") {
return [__("Rejected"), "grey"]; return [__("Rejected"), "grey", "status,=,Rejected"];
} }
} }
}; };

View File

@@ -55,11 +55,11 @@ class LeaveApplication(Document):
self.reload() self.reload()
def on_cancel(self): def on_cancel(self):
self.create_leave_ledger_entry(submit=False)
self.status = "Cancelled" self.status = "Cancelled"
# notify leave applier about cancellation # notify leave applier about cancellation
self.notify_employee() self.notify_employee()
self.cancel_attendance() self.cancel_attendance()
self.create_leave_ledger_entry(submit=False)
def validate_applicable_after(self): def validate_applicable_after(self):
if self.leave_type: if self.leave_type:
@@ -125,7 +125,7 @@ class LeaveApplication(Document):
status = "Half Day" if date == self.half_day_date else "On Leave" status = "Half Day" if date == self.half_day_date else "On Leave"
attendance_name = frappe.db.exists('Attendance', dict(employee = self.employee, attendance_name = frappe.db.exists('Attendance', dict(employee = self.employee,
attenance_date = date, docstatus = ('!=', 2))) attendance_date = date, docstatus = ('!=', 2)))
if attendance_name: if attendance_name:
# update existing attendance, change absent to on leave # update existing attendance, change absent to on leave
@@ -351,6 +351,9 @@ class LeaveApplication(Document):
pass pass
def create_leave_ledger_entry(self, submit=True): def create_leave_ledger_entry(self, submit=True):
if self.status != 'Approved':
return
expiry_date = get_allocation_expiry(self.employee, self.leave_type, expiry_date = get_allocation_expiry(self.employee, self.leave_type,
self.to_date, self.from_date) self.to_date, self.from_date)
@@ -503,14 +506,17 @@ def get_leave_allocation_records(employee, date, leave_type=None):
def get_pending_leaves_for_period(employee, leave_type, from_date, to_date): def get_pending_leaves_for_period(employee, leave_type, from_date, to_date):
''' Returns leaves that are pending approval ''' ''' Returns leaves that are pending approval '''
return frappe.db.get_value("Leave Application", leaves = frappe.get_all("Leave Application",
filters={ filters={
"employee": employee, "employee": employee,
"leave_type": leave_type, "leave_type": leave_type,
"from_date": ("<=", from_date),
"to_date": (">=", to_date),
"status": "Open" "status": "Open"
}, fieldname=['SUM(total_leave_days)']) or flt(0) },
or_filters={
"from_date": ["between", (from_date, to_date)],
"to_date": ["between", (from_date, to_date)]
}, fields=['SUM(total_leave_days) as leaves'])[0]
return leaves['leaves'] if leaves['leaves'] else 0.0
def get_remaining_leaves(allocation, leaves_taken, date, expiry): def get_remaining_leaves(allocation, leaves_taken, date, expiry):
''' Returns minimum leaves remaining after comparing with remaining days for allocation expiry ''' ''' Returns minimum leaves remaining after comparing with remaining days for allocation expiry '''

View File

@@ -5,6 +5,12 @@ from frappe import _
def get_data(): def get_data():
return { return {
'fieldname': 'leave_application',
'transactions': [
{
'items': ['Attendance']
}
],
'reports': [ 'reports': [
{ {
'label': _('Reports'), 'label': _('Reports'),

View File

@@ -72,7 +72,7 @@ class TestLeaveApplication(unittest.TestCase):
application.to_date = "2013-01-05" application.to_date = "2013-01-05"
return application return application
def test_attendance_creation(self): def test_overwrite_attendance(self):
'''check attendance is automatically created on leave approval''' '''check attendance is automatically created on leave approval'''
make_allocation_record() make_allocation_record()
application = self.get_application(_test_records[0]) application = self.get_application(_test_records[0])
@@ -82,7 +82,8 @@ class TestLeaveApplication(unittest.TestCase):
application.insert() application.insert()
application.submit() application.submit()
attendance = frappe.get_all('Attendance', ['name', 'status', 'attendance_date'], dict(leave_application = application.name)) attendance = frappe.get_all('Attendance', ['name', 'status', 'attendance_date'],
dict(attendance_date=('between', ['2018-01-01', '2018-01-03']), docstatus=("!=", 2)))
# attendance created for all 3 days # attendance created for all 3 days
self.assertEqual(len(attendance), 3) self.assertEqual(len(attendance), 3)
@@ -95,20 +96,6 @@ class TestLeaveApplication(unittest.TestCase):
for d in ('2018-01-01', '2018-01-02', '2018-01-03'): for d in ('2018-01-01', '2018-01-02', '2018-01-03'):
self.assertTrue(getdate(d) in dates) self.assertTrue(getdate(d) in dates)
def test_overwrite_attendance(self):
# employee marked as absent
doc = frappe.new_doc("Attendance")
doc.employee = '_T-Employee-00001'
doc.attendance_date = '2018-01-01'
doc.company = '_Test Company'
doc.status = 'Absent'
doc.flags.ignore_validate = True
doc.insert(ignore_permissions=True)
doc.submit()
# now check if the status has been updated
self.test_attendance_creation()
def test_block_list(self): def test_block_list(self):
self._clear_roles() self._clear_roles()

View File

@@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
import datetime, math import datetime, math
from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, getdate from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words
from frappe.model.naming import make_autoname from frappe.model.naming import make_autoname
from frappe import msgprint, _ from frappe import msgprint, _

View File

@@ -46,10 +46,12 @@ frappe.ui.form.on('Salary Structure', {
frm.trigger("toggle_fields"); frm.trigger("toggle_fields");
frm.fields_dict['earnings'].grid.set_column_disp("default_amount", false); frm.fields_dict['earnings'].grid.set_column_disp("default_amount", false);
frm.fields_dict['deductions'].grid.set_column_disp("default_amount", false); frm.fields_dict['deductions'].grid.set_column_disp("default_amount", false);
frm.add_custom_button(__("Preview Salary Slip"), function() { if(frm.doc.docstatus === 1) {
frm.trigger('preview_salary_slip'); frm.add_custom_button(__("Preview Salary Slip"), function() {
}); frm.trigger('preview_salary_slip');
});
}
if(frm.doc.docstatus==1) { if(frm.doc.docstatus==1) {
frm.add_custom_button(__("Assign Salary Structure"), function() { frm.add_custom_button(__("Assign Salary Structure"), function() {

View File

@@ -169,5 +169,10 @@ def make_salary_slip(source_name, target_doc = None, employee = None, as_print =
@frappe.whitelist() @frappe.whitelist()
def get_employees(salary_structure): def get_employees(salary_structure):
employees = frappe.get_list('Salary Structure Assignment', employees = frappe.get_list('Salary Structure Assignment',
filters={'salary_structure': salary_structure}, fields=['employee']) filters={'salary_structure': salary_structure, 'docstatus': 1}, fields=['employee'])
if not employees:
frappe.throw(_("There's no Employee with Salary Structure: {0}. \
Assign {1} to an Employee to preview Salary Slip").format(salary_structure, salary_structure))
return list(set([d.employee for d in employees])) return list(set([d.employee for d in employees]))

View File

@@ -7,6 +7,7 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe import _ from frappe import _
from frappe.utils import getdate, nowdate, cint, flt from frappe.utils import getdate, nowdate, cint, flt
from frappe.utils.nestedset import get_descendants_of
class SubsidiaryCompanyError(frappe.ValidationError): pass class SubsidiaryCompanyError(frappe.ValidationError): pass
class ParentCompanyError(frappe.ValidationError): pass class ParentCompanyError(frappe.ValidationError): pass
@@ -131,7 +132,8 @@ def get_designation_counts(designation, company):
return False return False
employee_counts = {} employee_counts = {}
company_set = get_company_set(company) company_set = get_descendants_of('Company', company)
company_set.append(company)
employee_counts["employee_count"] = frappe.db.get_value("Employee", employee_counts["employee_count"] = frappe.db.get_value("Employee",
filters={ filters={
@@ -167,14 +169,4 @@ def get_active_staffing_plan_details(company, designation, from_date=getdate(now
designation, from_date, to_date) designation, from_date, to_date)
# Only a single staffing plan can be active for a designation on given date # Only a single staffing plan can be active for a designation on given date
return staffing_plan if staffing_plan else None return staffing_plan if staffing_plan else None
def get_company_set(company):
return frappe.db.sql_list("""
SELECT
name
FROM `tabCompany`
WHERE
parent_company=%(company)s
OR name=%(company)s
""", (dict(company=company)))

View File

@@ -2,4 +2,14 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.query_reports["Department Analytics"] = { frappe.query_reports["Department Analytics"] = {
"filters": [
{
"fieldname":"company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"default": frappe.defaults.get_user_default("Company"),
"reqd": 1
},
]
}; };

View File

@@ -7,6 +7,10 @@ from frappe import _
def execute(filters=None): def execute(filters=None):
if not filters: filters = {} if not filters: filters = {}
if not filters["company"]:
frappe.throw(_('{0} is mandatory').format(_('Company')))
columns = get_columns() columns = get_columns()
employees = get_employees(filters) employees = get_employees(filters)
departments_result = get_department(filters) departments_result = get_department(filters)
@@ -28,6 +32,9 @@ def get_conditions(filters):
conditions = "" conditions = ""
if filters.get("department"): conditions += " and department = '%s'" % \ if filters.get("department"): conditions += " and department = '%s'" % \
filters["department"].replace("'", "\\'") filters["department"].replace("'", "\\'")
if filters.get("company"): conditions += " and company = '%s'" % \
filters["company"].replace("'", "\\'")
return conditions return conditions
def get_employees(filters): def get_employees(filters):
@@ -37,7 +44,7 @@ def get_employees(filters):
gender, company from `tabEmployee` where status = 'Active' %s""" % conditions, as_list=1) gender, company from `tabEmployee` where status = 'Active' %s""" % conditions, as_list=1)
def get_department(filters): def get_department(filters):
return frappe.db.sql("""select name from `tabDepartment`""" , as_list=1) return frappe.db.sql("""select name from `tabDepartment` where company = %s""", (filters["company"]), as_list=1)
def get_chart_data(departments,employees): def get_chart_data(departments,employees):
if not departments: if not departments:

View File

@@ -75,7 +75,7 @@ def get_data(filters):
leave_approvers = department_approver_map.get(employee.department_name, []).append(employee.leave_approver) leave_approvers = department_approver_map.get(employee.department_name, []).append(employee.leave_approver)
if (len(leave_approvers) and user in leave_approvers) or (user in ["Administrator", employee.user_id]) \ if (leave_approvers and len(leave_approvers) and user in leave_approvers) or (user in ["Administrator", employee.user_id]) \
or ("HR Manager" in frappe.get_roles(user)): or ("HR Manager" in frappe.get_roles(user)):
row = frappe._dict({ row = frappe._dict({
'employee': employee.name, 'employee': employee.name,
@@ -111,10 +111,10 @@ def get_conditions(filters):
def get_department_leave_approver_map(department=None): def get_department_leave_approver_map(department=None):
conditions='' conditions=''
if department: if department:
conditions='and department_name = %(department)s or parent_department = %(department)s'%{'department': department} conditions="and (department_name = '%(department)s' or parent_department = '%(department)s')"%{'department': department}
# get current department and all its child # get current department and all its child
department_list = frappe.db.sql_list(''' SELECT name FROM `tabDepartment` WHERE disabled=0 {0}'''.format(conditions)) #nosec department_list = frappe.db.sql_list(""" SELECT name FROM `tabDepartment` WHERE disabled=0 {0}""".format(conditions)) #nosec
# retrieve approvers list from current department and from its subsequent child departments # retrieve approvers list from current department and from its subsequent child departments
approver_list = frappe.get_all('Department Approver', filters={ approver_list = frappe.get_all('Department Approver', filters={

Some files were not shown because too many files have changed in this diff Show More