Merge branch 'develop' into supplier_items

This commit is contained in:
Deepesh Garg 2019-08-23 15:41:30 +05:30 committed by GitHub
commit 98b86ecbc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 1815 additions and 2916 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__ = '11.1.39' __version__ = '12.0.8'
def get_default_company(user=None): def get_default_company(user=None):
'''Get default company for user''' '''Get default company for user'''

View File

@ -40,9 +40,16 @@ frappe.ui.form.on('Accounting Dimension', {
}, },
document_type: function(frm) { document_type: function(frm) {
frm.set_value('label', frm.doc.document_type); frm.set_value('label', frm.doc.document_type);
frm.set_value('fieldname', frappe.model.scrub(frm.doc.document_type)); frm.set_value('fieldname', frappe.model.scrub(frm.doc.document_type));
if (frm.is_new()){
let row = frappe.model.add_child(frm.doc, "Accounting Dimension Detail", "dimension_defaults");
row.reference_document = frm.doc.document_type;
frm.refresh_fields("dimension_defaults");
}
frappe.db.get_value('Accounting Dimension', {'document_type': frm.doc.document_type}, 'document_type', (r) => { frappe.db.get_value('Accounting Dimension', {'document_type': frm.doc.document_type}, 'document_type', (r) => {
if (r && r.document_type) { if (r && r.document_type) {
frm.set_df_property('document_type', 'description', "Document type is already set as dimension"); frm.set_df_property('document_type', 'description', "Document type is already set as dimension");

View File

@ -45,12 +45,12 @@ class BankTransaction(StatusUpdater):
def clear_linked_payment_entries(self): def clear_linked_payment_entries(self):
for payment_entry in self.payment_entries: for payment_entry in self.payment_entries:
allocated_amount = get_total_allocated_amount(payment_entry) allocated_amount = get_total_allocated_amount(payment_entry)
paid_amount = get_paid_amount(payment_entry) paid_amount = get_paid_amount(payment_entry, self.currency)
if paid_amount and allocated_amount: if paid_amount and allocated_amount:
if flt(allocated_amount[0]["allocated_amount"]) > flt(paid_amount): if flt(allocated_amount[0]["allocated_amount"]) > flt(paid_amount):
frappe.throw(_("The total allocated amount ({0}) is greated than the paid amount ({1}).".format(flt(allocated_amount[0]["allocated_amount"]), flt(paid_amount)))) frappe.throw(_("The total allocated amount ({0}) is greated than the paid amount ({1}).".format(flt(allocated_amount[0]["allocated_amount"]), flt(paid_amount))))
elif flt(allocated_amount[0]["allocated_amount"]) == flt(paid_amount): else:
if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]: if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]:
self.clear_simple_entry(payment_entry) self.clear_simple_entry(payment_entry)
@ -80,9 +80,17 @@ def get_total_allocated_amount(payment_entry):
AND AND
bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True) bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True)
def get_paid_amount(payment_entry): def get_paid_amount(payment_entry, currency):
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]: if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "paid_amount")
paid_amount_field = "paid_amount"
if payment_entry.payment_document == 'Payment Entry':
doc = frappe.get_doc("Payment Entry", payment_entry.payment_entry)
paid_amount_field = ("base_paid_amount"
if doc.paid_to_account_currency == currency else "paid_amount")
return frappe.db.get_value(payment_entry.payment_document,
payment_entry.payment_entry, paid_amount_field)
elif payment_entry.payment_document == "Journal Entry": elif payment_entry.payment_document == "Journal Entry":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_credit") return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_credit")

View File

@ -8,7 +8,8 @@
"customer", "customer",
"column_break_3", "column_break_3",
"posting_date", "posting_date",
"outstanding_amount" "outstanding_amount",
"debit_to"
], ],
"fields": [ "fields": [
{ {
@ -48,10 +49,18 @@
{ {
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fetch_from": "sales_invoice.debit_to",
"fieldname": "debit_to",
"fieldtype": "Link",
"label": "Debit to",
"options": "Account",
"read_only": 1
} }
], ],
"istable": 1, "istable": 1,
"modified": "2019-05-30 19:27:29.436153", "modified": "2019-08-07 15:13:55.808349",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Discounted Invoice", "name": "Discounted Invoice",

View File

@ -13,41 +13,57 @@ frappe.ui.form.on('Invoice Discounting', {
}; };
}); });
frm.events.filter_accounts("bank_account", frm, {"account_type": "Bank"});
frm.events.filter_accounts("bank_charges_account", frm, {"root_type": "Expense"}); frm.events.filter_accounts("bank_account", frm, [["account_type", "=", "Bank"]]);
frm.events.filter_accounts("short_term_loan", frm, {"root_type": "Liability"}); frm.events.filter_accounts("bank_charges_account", frm, [["root_type", "=", "Expense"]]);
frm.events.filter_accounts("accounts_receivable_credit", frm, {"account_type": "Receivable"}); frm.events.filter_accounts("short_term_loan", frm, [["root_type", "=", "Liability"]]);
frm.events.filter_accounts("accounts_receivable_discounted", frm, {"account_type": "Receivable"}); frm.events.filter_accounts("accounts_receivable_discounted", frm, [["account_type", "=", "Receivable"]]);
frm.events.filter_accounts("accounts_receivable_unpaid", frm, {"account_type": "Receivable"}); frm.events.filter_accounts("accounts_receivable_credit", frm, [["account_type", "=", "Receivable"]]);
frm.events.filter_accounts("accounts_receivable_unpaid", frm, [["account_type", "=", "Receivable"]]);
}, },
filter_accounts: (fieldname, frm, addl_filters) => { filter_accounts: (fieldname, frm, addl_filters) => {
let filters = { let filters = [
"company": frm.doc.company, ["company", "=", frm.doc.company],
"is_group": 0 ["is_group", "=", 0]
}; ];
if(addl_filters) Object.assign(filters, addl_filters); if(addl_filters){
filters = $.merge(filters , addl_filters);
}
frm.set_query(fieldname, () => { return { "filters": filters }; }); frm.set_query(fieldname, () => { return { "filters": filters }; });
}, },
refresh_filters: (frm) =>{
let invoice_accounts = Object.keys(frm.doc.invoices).map(function(key) {
return frm.doc.invoices[key].debit_to;
});
let filters = [
["account_type", "=", "Receivable"],
["name", "not in", invoice_accounts]
];
frm.events.filter_accounts("accounts_receivable_credit", frm, filters);
frm.events.filter_accounts("accounts_receivable_discounted", frm, filters);
frm.events.filter_accounts("accounts_receivable_unpaid", frm, filters);
},
refresh: (frm) => { refresh: (frm) => {
frm.events.show_general_ledger(frm); frm.events.show_general_ledger(frm);
if(frm.doc.docstatus === 0) { if (frm.doc.docstatus === 0) {
frm.add_custom_button(__('Get Invoices'), function() { frm.add_custom_button(__('Get Invoices'), function() {
frm.events.get_invoices(frm); frm.events.get_invoices(frm);
}); });
} }
if(frm.doc.docstatus === 1 && frm.doc.status !== "Settled") { if (frm.doc.docstatus === 1 && frm.doc.status !== "Settled") {
if(frm.doc.status == "Sanctioned") { if (frm.doc.status == "Sanctioned") {
frm.add_custom_button(__('Disburse Loan'), function() { frm.add_custom_button(__('Disburse Loan'), function() {
frm.events.create_disbursement_entry(frm); frm.events.create_disbursement_entry(frm);
}).addClass("btn-primary"); }).addClass("btn-primary");
} }
if(frm.doc.status == "Disbursed") { if (frm.doc.status == "Disbursed") {
frm.add_custom_button(__('Close Loan'), function() { frm.add_custom_button(__('Close Loan'), function() {
frm.events.close_loan(frm); frm.events.close_loan(frm);
}).addClass("btn-primary"); }).addClass("btn-primary");
@ -64,7 +80,7 @@ frappe.ui.form.on('Invoice Discounting', {
}, },
set_end_date: (frm) => { set_end_date: (frm) => {
if(frm.doc.loan_start_date && frm.doc.loan_period) { if (frm.doc.loan_start_date && frm.doc.loan_period) {
let end_date = frappe.datetime.add_days(frm.doc.loan_start_date, frm.doc.loan_period); let end_date = frappe.datetime.add_days(frm.doc.loan_start_date, frm.doc.loan_period);
frm.set_value("loan_end_date", end_date); frm.set_value("loan_end_date", end_date);
} }
@ -132,6 +148,7 @@ frappe.ui.form.on('Invoice Discounting', {
frm.doc.invoices = frm.doc.invoices.filter(row => row.sales_invoice); frm.doc.invoices = frm.doc.invoices.filter(row => row.sales_invoice);
let row = frm.add_child("invoices"); let row = frm.add_child("invoices");
$.extend(row, v); $.extend(row, v);
frm.events.refresh_filters(frm);
}); });
refresh_field("invoices"); refresh_field("invoices");
} }
@ -190,8 +207,10 @@ frappe.ui.form.on('Invoice Discounting', {
frappe.ui.form.on('Discounted Invoice', { frappe.ui.form.on('Discounted Invoice', {
sales_invoice: (frm) => { sales_invoice: (frm) => {
frm.events.calculate_total_amount(frm); frm.events.calculate_total_amount(frm);
frm.events.refresh_filters(frm);
}, },
invoices_remove: (frm) => { invoices_remove: (frm) => {
frm.events.calculate_total_amount(frm); frm.events.calculate_total_amount(frm);
frm.events.refresh_filters(frm);
} }
}); });

View File

@ -12,6 +12,7 @@ from erpnext.accounts.general_ledger import make_gl_entries
class InvoiceDiscounting(AccountsController): class InvoiceDiscounting(AccountsController):
def validate(self): def validate(self):
self.validate_mandatory() self.validate_mandatory()
self.validate_invoices()
self.calculate_total_amount() self.calculate_total_amount()
self.set_status() self.set_status()
self.set_end_date() self.set_end_date()
@ -24,6 +25,15 @@ class InvoiceDiscounting(AccountsController):
if self.docstatus == 1 and not (self.loan_start_date and self.loan_period): if self.docstatus == 1 and not (self.loan_start_date and self.loan_period):
frappe.throw(_("Loan Start Date and Loan Period are mandatory to save the Invoice Discounting")) frappe.throw(_("Loan Start Date and Loan Period are mandatory to save the Invoice Discounting"))
def validate_invoices(self):
discounted_invoices = [record.sales_invoice for record in
frappe.get_all("Discounted Invoice",fields = ["sales_invoice"], filters= {"docstatus":1})]
for record in self.invoices:
if record.sales_invoice in discounted_invoices:
frappe.throw("Row({0}): {1} is already discounted in {2}"
.format(record.idx, frappe.bold(record.sales_invoice), frappe.bold(record.parent)))
def calculate_total_amount(self): def calculate_total_amount(self):
self.total_amount = sum([flt(d.outstanding_amount) for d in self.invoices]) self.total_amount = sum([flt(d.outstanding_amount) for d in self.invoices])
@ -212,7 +222,8 @@ def get_invoices(filters):
name as sales_invoice, name as sales_invoice,
customer, customer,
posting_date, posting_date,
outstanding_amount outstanding_amount,
debit_to
from `tabSales Invoice` si from `tabSales Invoice` si
where where
docstatus = 1 docstatus = 1

View File

@ -624,8 +624,8 @@ def get_outstanding_reference_documents(args):
data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed
if not data: if not data:
frappe.msgprint(_("No outstanding invoices found for the {0} <b>{1}</b> which qualify the filters you have specified.") frappe.msgprint(_("No outstanding invoices found for the {0} {1} which qualify the filters you have specified.")
.format(args.get("party_type").lower(), args.get("party"))) .format(args.get("party_type").lower(), frappe.bold(args.get("party"))))
return data return data

View File

@ -66,6 +66,7 @@ frappe.ui.form.on('Payment Order', {
get_query_filters: { get_query_filters: {
bank: frm.doc.bank, bank: frm.doc.bank,
docstatus: 1, docstatus: 1,
payment_type: ("!=", "Receive"),
bank_account: frm.doc.company_bank_account, bank_account: frm.doc.company_bank_account,
paid_from: frm.doc.account, paid_from: frm.doc.account,
payment_order_status: ["=", "Initiated"], payment_order_status: ["=", "Initiated"],

View File

@ -93,7 +93,7 @@ class PaymentReconciliation(Document):
and `tab{doc}`.is_return = 1 and `tabGL Entry`.against_voucher_type = %(voucher_type)s and `tab{doc}`.is_return = 1 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 `tabSales Invoice`.name GROUP BY `tab{doc}`.name
Having Having
amount > 0 amount > 0
""".format(doc=voucher_type, dr_or_cr=dr_or_cr, reconciled_dr_or_cr=reconciled_dr_or_cr), { """.format(doc=voucher_type, dr_or_cr=dr_or_cr, reconciled_dr_or_cr=reconciled_dr_or_cr), {
@ -257,11 +257,8 @@ def reconcile_dr_cr_note(dr_cr_notes):
voucher_type = ('Credit Note' voucher_type = ('Credit Note'
if d.voucher_type == 'Sales Invoice' else 'Debit Note') if d.voucher_type == 'Sales Invoice' else 'Debit Note')
dr_or_cr = ('credit_in_account_currency'
if d.reference_type == 'Sales Invoice' else 'debit_in_account_currency')
reconcile_dr_or_cr = ('debit_in_account_currency' reconcile_dr_or_cr = ('debit_in_account_currency'
if dr_or_cr == 'credit_in_account_currency' else 'credit_in_account_currency') if d.dr_or_cr == 'credit_in_account_currency' else 'credit_in_account_currency')
jv = frappe.get_doc({ jv = frappe.get_doc({
"doctype": "Journal Entry", "doctype": "Journal Entry",
@ -272,8 +269,7 @@ def reconcile_dr_cr_note(dr_cr_notes):
'account': d.account, 'account': d.account,
'party': d.party, 'party': d.party,
'party_type': d.party_type, 'party_type': d.party_type,
reconcile_dr_or_cr: (abs(d.allocated_amount) d.dr_or_cr: abs(d.allocated_amount),
if abs(d.unadjusted_amount) > abs(d.allocated_amount) else abs(d.unadjusted_amount)),
'reference_type': d.against_voucher_type, 'reference_type': d.against_voucher_type,
'reference_name': d.against_voucher 'reference_name': d.against_voucher
}, },
@ -281,7 +277,8 @@ def reconcile_dr_cr_note(dr_cr_notes):
'account': d.account, 'account': d.account,
'party': d.party, 'party': d.party,
'party_type': d.party_type, 'party_type': d.party_type,
dr_or_cr: abs(d.allocated_amount), reconcile_dr_or_cr: (abs(d.allocated_amount)
if abs(d.unadjusted_amount) > abs(d.allocated_amount) else abs(d.unadjusted_amount)),
'reference_type': d.voucher_type, 'reference_type': d.voucher_type,
'reference_name': d.voucher_no 'reference_name': d.voucher_no
} }

View File

@ -307,7 +307,7 @@ def get_item_tax_data():
# example: {'Consulting Services': {'Excise 12 - TS': '12.000'}} # example: {'Consulting Services': {'Excise 12 - TS': '12.000'}}
itemwise_tax = {} itemwise_tax = {}
taxes = frappe.db.sql(""" select parent, tax_type, tax_rate from `tabItem Tax`""", as_dict=1) taxes = frappe.db.sql(""" select parent, tax_type, tax_rate from `tabItem Tax Template Detail`""", as_dict=1)
for tax in taxes: for tax in taxes:
if tax.parent not in itemwise_tax: if tax.parent not in itemwise_tax:
@ -432,7 +432,6 @@ def get_customer_id(doc, customer=None):
return cust_id return cust_id
def make_customer_and_address(customers): def make_customer_and_address(customers):
customers_list = [] customers_list = []
for customer, data in iteritems(customers): for customer, data in iteritems(customers):
@ -449,7 +448,6 @@ def make_customer_and_address(customers):
frappe.db.commit() frappe.db.commit()
return customers_list return customers_list
def add_customer(data): def add_customer(data):
customer = data.get('full_name') or data.get('customer') customer = data.get('full_name') or data.get('customer')
if frappe.db.exists("Customer", customer.strip()): if frappe.db.exists("Customer", customer.strip()):
@ -466,21 +464,18 @@ def add_customer(data):
frappe.db.commit() frappe.db.commit()
return customer_doc.name return customer_doc.name
def get_territory(data): def get_territory(data):
if data.get('territory'): if data.get('territory'):
return data.get('territory') return data.get('territory')
return frappe.db.get_single_value('Selling Settings','territory') or _('All Territories') return frappe.db.get_single_value('Selling Settings','territory') or _('All Territories')
def get_customer_group(data): def get_customer_group(data):
if data.get('customer_group'): if data.get('customer_group'):
return data.get('customer_group') return data.get('customer_group')
return frappe.db.get_single_value('Selling Settings', 'customer_group') or frappe.db.get_value('Customer Group', {'is_group': 0}, 'name') return frappe.db.get_single_value('Selling Settings', 'customer_group') or frappe.db.get_value('Customer Group', {'is_group': 0}, 'name')
def make_contact(args, customer): def make_contact(args, customer):
if args.get('email_id') or args.get('phone'): if args.get('email_id') or args.get('phone'):
name = frappe.db.get_value('Dynamic Link', name = frappe.db.get_value('Dynamic Link',
@ -506,7 +501,6 @@ def make_contact(args, customer):
doc.flags.ignore_mandatory = True doc.flags.ignore_mandatory = True
doc.save(ignore_permissions=True) doc.save(ignore_permissions=True)
def make_address(args, customer): def make_address(args, customer):
if not args.get('address_line1'): if not args.get('address_line1'):
return return
@ -521,7 +515,10 @@ def make_address(args, customer):
address = frappe.get_doc('Address', name) address = frappe.get_doc('Address', name)
else: else:
address = frappe.new_doc('Address') address = frappe.new_doc('Address')
address.country = frappe.get_cached_value('Company', args.get('company'), 'country') if args.get('company'):
address.country = frappe.get_cached_value('Company',
args.get('company'), 'country')
address.append('links', { address.append('links', {
'link_doctype': 'Customer', 'link_doctype': 'Customer',
'link_name': customer 'link_name': customer
@ -533,7 +530,6 @@ def make_address(args, customer):
address.flags.ignore_mandatory = True address.flags.ignore_mandatory = True
address.save(ignore_permissions=True) address.save(ignore_permissions=True)
def make_email_queue(email_queue): def make_email_queue(email_queue):
name_list = [] name_list = []
for key, data in iteritems(email_queue): for key, data in iteritems(email_queue):
@ -550,7 +546,6 @@ def make_email_queue(email_queue):
return name_list return name_list
def validate_item(doc): def validate_item(doc):
for item in doc.get('items'): for item in doc.get('items'):
if not frappe.db.exists('Item', item.get('item_code')): if not frappe.db.exists('Item', item.get('item_code')):
@ -569,7 +564,6 @@ def validate_item(doc):
item_doc.save(ignore_permissions=True) item_doc.save(ignore_permissions=True)
frappe.db.commit() frappe.db.commit()
def submit_invoice(si_doc, name, doc, name_list): def submit_invoice(si_doc, name, doc, name_list):
try: try:
si_doc.insert() si_doc.insert()
@ -585,7 +579,6 @@ def submit_invoice(si_doc, name, doc, name_list):
return name_list return name_list
def save_invoice(doc, name, name_list): def save_invoice(doc, name, name_list):
try: try:
if not frappe.db.exists('Sales Invoice', {'offline_pos_name': name}): if not frappe.db.exists('Sales Invoice', {'offline_pos_name': name}):

View File

@ -78,6 +78,7 @@ class SalesInvoice(SellingController):
self.so_dn_required() self.so_dn_required()
self.validate_proj_cust() self.validate_proj_cust()
self.validate_pos_return()
self.validate_with_previous_doc() self.validate_with_previous_doc()
self.validate_uom_is_integer("stock_uom", "stock_qty") self.validate_uom_is_integer("stock_uom", "stock_qty")
self.validate_uom_is_integer("uom", "qty") self.validate_uom_is_integer("uom", "qty")
@ -199,6 +200,16 @@ class SalesInvoice(SellingController):
if "Healthcare" in active_domains: if "Healthcare" in active_domains:
manage_invoice_submit_cancel(self, "on_submit") manage_invoice_submit_cancel(self, "on_submit")
def validate_pos_return(self):
if self.is_pos and self.is_return:
total_amount_in_payments = 0
for payment in self.payments:
total_amount_in_payments += payment.amount
invoice_total = self.rounded_total or self.grand_total
if total_amount_in_payments < invoice_total:
frappe.throw(_("Total payments amount can't be greater than {}".format(-invoice_total)))
def validate_pos_paid_amount(self): def validate_pos_paid_amount(self):
if len(self.payments) == 0 and self.is_pos: if len(self.payments) == 0 and self.is_pos:
frappe.throw(_("At least one mode of payment is required for POS invoice.")) frappe.throw(_("At least one mode of payment is required for POS invoice."))
@ -1499,4 +1510,4 @@ def create_invoice_discounting(source_name, target_doc=None):
"outstanding_amount": invoice.outstanding_amount "outstanding_amount": invoice.outstanding_amount
}) })
return invoice_discounting return invoice_discounting

View File

@ -124,8 +124,6 @@ def check_matching_amount(bank_account, company, transaction):
'txt': '%%%s%%' % amount 'txt': '%%%s%%' % amount
}, as_dict=True) }, as_dict=True)
frappe.errprint(journal_entries)
if transaction.credit > 0: if transaction.credit > 0:
sales_invoices = frappe.db.sql(""" sales_invoices = frappe.db.sql("""
SELECT SELECT

View File

@ -1762,18 +1762,11 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({
this.si_docs = this.get_submitted_invoice() || []; this.si_docs = this.get_submitted_invoice() || [];
this.email_queue_list = this.get_email_queue() || {}; this.email_queue_list = this.get_email_queue() || {};
this.customers_list = this.get_customers_details() || {}; this.customers_list = this.get_customers_details() || {};
if(this.customer_doc) {
this.freeze = this.customer_doc.display
}
freeze_screen = this.freeze_screen || false;
if ((this.si_docs.length || this.email_queue_list || this.customers_list) && !this.freeze) {
this.freeze = true;
if (this.si_docs.length || this.email_queue_list || this.customers_list) {
frappe.call({ frappe.call({
method: "erpnext.accounts.doctype.sales_invoice.pos.make_invoice", method: "erpnext.accounts.doctype.sales_invoice.pos.make_invoice",
freeze: freeze_screen, freeze: true,
args: { args: {
doc_list: me.si_docs, doc_list: me.si_docs,
email_queue_list: me.email_queue_list, email_queue_list: me.email_queue_list,

View File

@ -197,8 +197,10 @@ class ReceivablePayableReport(object):
if self.filters.based_on_payment_terms and gl_entries_data: if self.filters.based_on_payment_terms and gl_entries_data:
self.payment_term_map = self.get_payment_term_detail(voucher_nos) self.payment_term_map = self.get_payment_term_detail(voucher_nos)
self.gle_inclusion_map = {}
for gle in gl_entries_data: for gle in gl_entries_data:
if self.is_receivable_or_payable(gle, self.dr_or_cr, future_vouchers, return_entries): if self.is_receivable_or_payable(gle, self.dr_or_cr, future_vouchers, return_entries):
self.gle_inclusion_map[gle.name] = True
outstanding_amount, credit_note_amount, payment_amount = self.get_outstanding_amount( outstanding_amount, credit_note_amount, payment_amount = self.get_outstanding_amount(
gle,self.filters.report_date, self.dr_or_cr, return_entries) gle,self.filters.report_date, self.dr_or_cr, return_entries)
temp_outstanding_amt = outstanding_amount temp_outstanding_amt = outstanding_amount
@ -409,7 +411,9 @@ class ReceivablePayableReport(object):
for e in self.get_gl_entries_for(gle.party, gle.party_type, gle.voucher_type, gle.voucher_no): for e in self.get_gl_entries_for(gle.party, gle.party_type, gle.voucher_type, gle.voucher_no):
if getdate(e.posting_date) <= report_date \ if getdate(e.posting_date) <= report_date \
and (e.name!=gle.name or (e.voucher_no in return_entries and not return_entries.get(e.voucher_no))): and (e.name!=gle.name or (e.voucher_no in return_entries and not return_entries.get(e.voucher_no))):
if e.name!=gle.name and self.gle_inclusion_map.get(e.name):
continue
self.gle_inclusion_map[e.name] = True
amount = flt(e.get(reverse_dr_or_cr), self.currency_precision) - flt(e.get(dr_or_cr), self.currency_precision) amount = flt(e.get(reverse_dr_or_cr), self.currency_precision) - flt(e.get(dr_or_cr), self.currency_precision)
if e.voucher_no not in return_entries: if e.voucher_no not in return_entries:
payment_amount += amount payment_amount += amount

View File

@ -119,19 +119,11 @@ def get_gl_entries(filters):
select_fields = """, debit, credit, debit_in_account_currency, select_fields = """, debit, credit, debit_in_account_currency,
credit_in_account_currency """ credit_in_account_currency """
group_by_statement = ''
order_by_statement = "order by posting_date, account" order_by_statement = "order by posting_date, account"
if filters.get("group_by") == _("Group by Voucher"): if filters.get("group_by") == _("Group by Voucher"):
order_by_statement = "order by posting_date, voucher_type, voucher_no" order_by_statement = "order by posting_date, voucher_type, voucher_no"
if filters.get("group_by") == _("Group by Voucher (Consolidated)"):
group_by_statement = "group by voucher_type, voucher_no, account, cost_center"
select_fields = """, sum(debit) as debit, sum(credit) as credit,
sum(debit_in_account_currency) as debit_in_account_currency,
sum(credit_in_account_currency) as credit_in_account_currency"""
if filters.get("include_default_book_entries"): if filters.get("include_default_book_entries"):
filters['company_fb'] = frappe.db.get_value("Company", filters['company_fb'] = frappe.db.get_value("Company",
filters.get("company"), 'default_finance_book') filters.get("company"), 'default_finance_book')
@ -144,11 +136,10 @@ def get_gl_entries(filters):
against_voucher_type, against_voucher, account_currency, against_voucher_type, against_voucher, account_currency,
remarks, against, is_opening {select_fields} remarks, against, is_opening {select_fields}
from `tabGL Entry` from `tabGL Entry`
where company=%(company)s {conditions} {group_by_statement} where company=%(company)s {conditions}
{order_by_statement} {order_by_statement}
""".format( """.format(
select_fields=select_fields, conditions=get_conditions(filters), select_fields=select_fields, conditions=get_conditions(filters),
group_by_statement=group_by_statement,
order_by_statement=order_by_statement order_by_statement=order_by_statement
), ),
filters, as_dict=1) filters, as_dict=1)
@ -185,7 +176,8 @@ def get_conditions(filters):
if not (filters.get("account") or filters.get("party") or if not (filters.get("account") or filters.get("party") or
filters.get("group_by") in ["Group by Account", "Group by Party"]): filters.get("group_by") in ["Group by Account", "Group by Party"]):
conditions.append("posting_date >=%(from_date)s") conditions.append("posting_date >=%(from_date)s")
conditions.append("posting_date <=%(to_date)s")
conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')")
if filters.get("project"): if filters.get("project"):
conditions.append("project in %(project)s") conditions.append("project in %(project)s")
@ -286,6 +278,7 @@ def initialize_gle_map(gl_entries, filters):
def get_accountwise_gle(filters, gl_entries, gle_map): def get_accountwise_gle(filters, gl_entries, gle_map):
totals = get_totals_dict() totals = get_totals_dict()
entries = [] entries = []
consolidated_gle = OrderedDict()
group_by = group_by_field(filters.get('group_by')) group_by = group_by_field(filters.get('group_by'))
def update_value_in_dict(data, key, gle): def update_value_in_dict(data, key, gle):
@ -310,12 +303,20 @@ def get_accountwise_gle(filters, gl_entries, gle_map):
update_value_in_dict(totals, 'total', gle) update_value_in_dict(totals, 'total', gle)
if filters.get("group_by") != _('Group by Voucher (Consolidated)'): if filters.get("group_by") != _('Group by Voucher (Consolidated)'):
gle_map[gle.get(group_by)].entries.append(gle) gle_map[gle.get(group_by)].entries.append(gle)
else: elif filters.get("group_by") == _('Group by Voucher (Consolidated)'):
entries.append(gle) key = (gle.get("voucher_type"), gle.get("voucher_no"),
gle.get("account"), gle.get("cost_center"))
if key not in consolidated_gle:
consolidated_gle.setdefault(key, gle)
else:
update_value_in_dict(consolidated_gle, key, gle)
update_value_in_dict(gle_map[gle.get(group_by)].totals, 'closing', gle) update_value_in_dict(gle_map[gle.get(group_by)].totals, 'closing', gle)
update_value_in_dict(totals, 'closing', gle) update_value_in_dict(totals, 'closing', gle)
for key, value in consolidated_gle.items():
entries.append(value)
return totals, entries return totals, entries
def get_result_as_list(data, filters): def get_result_as_list(data, filters):

View File

@ -303,14 +303,17 @@ frappe.ui.form.on('Asset', {
}, },
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) {
frappe.call({ frappe.call({
method: "get_depreciation_rate", method: "get_depreciation_rate",
doc: frm.doc, doc: frm.doc,
args: row, args: row,
callback: function(r) { callback: function(r) {
if (r.message) { if (r.message) {
frappe.model.set_value(row.doctype, row.name, "rate_of_depreciation", r.message); frappe.flags.dont_change_rate = true;
frappe.model.set_value(row.doctype, row.name,
"rate_of_depreciation", flt(r.message, precision("rate_of_depreciation", row)));
} }
} }
}); });
@ -338,6 +341,14 @@ frappe.ui.form.on('Asset Finance Book', {
total_number_of_depreciations: function(frm, cdt, cdn) { total_number_of_depreciations: function(frm, cdt, cdn) {
const row = locals[cdt][cdn]; const row = locals[cdt][cdn];
frm.events.set_depreciation_rate(frm, row); frm.events.set_depreciation_rate(frm, row);
},
rate_of_depreciation: function(frm, cdt, cdn) {
if(!frappe.flags.dont_change_rate) {
frappe.model.set_value(cdt, cdn, "expected_value_after_useful_life", 0);
}
frappe.flags.dont_change_rate = false;
} }
}); });

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 from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_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 \
@ -101,97 +101,88 @@ class Asset(AccountsController):
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 = self.get_depreciation_rate(d, on_validate=True) d.rate_of_depreciation = flt(self.get_depreciation_rate(d, on_validate=True),
d.precision("rate_of_depreciation"))
def make_depreciation_schedule(self): def make_depreciation_schedule(self):
depreciation_method = [d.depreciation_method for d in self.finance_books] if 'Manual' not in [d.depreciation_method for d in self.finance_books]:
if 'Manual' not in depreciation_method:
self.schedules = [] self.schedules = []
if not self.get("schedules") and self.available_for_use_date: if self.get("schedules") or not self.available_for_use_date:
total_depreciations = sum([d.total_number_of_depreciations for d in self.get('finance_books')]) return
for d in self.get('finance_books'): for d in self.get('finance_books'):
self.validate_asset_finance_books(d) self.validate_asset_finance_books(d)
value_after_depreciation = (flt(self.gross_purchase_amount) - value_after_depreciation = (flt(self.gross_purchase_amount) -
flt(self.opening_accumulated_depreciation)) flt(self.opening_accumulated_depreciation))
d.value_after_depreciation = value_after_depreciation d.value_after_depreciation = value_after_depreciation
no_of_depreciations = cint(d.total_number_of_depreciations - 1) - cint(self.number_of_depreciations_booked) number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \
end_date = add_months(d.depreciation_start_date, cint(self.number_of_depreciations_booked)
no_of_depreciations * cint(d.frequency_of_depreciation))
total_days = date_diff(end_date, self.available_for_use_date) has_pro_rata = self.check_is_pro_rata(d)
rate_per_day = (value_after_depreciation - d.get("expected_value_after_useful_life")) / total_days
number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \ if has_pro_rata:
cint(self.number_of_depreciations_booked) number_of_pending_depreciations += 1
from_date = self.available_for_use_date skip_row = False
if number_of_pending_depreciations: for n in range(number_of_pending_depreciations):
next_depr_date = getdate(add_months(self.available_for_use_date, # If depreciation is already completed (for double declining balance)
number_of_pending_depreciations * 12)) if skip_row: continue
if (cint(frappe.db.get_value("Asset Settings", None, "schedule_based_on_fiscal_year")) == 1
and getdate(d.depreciation_start_date) < next_depr_date):
number_of_pending_depreciations += 1 depreciation_amount = self.get_depreciation_amount(value_after_depreciation,
for n in range(number_of_pending_depreciations): d.total_number_of_depreciations, d)
if n == list(range(number_of_pending_depreciations))[-1]:
schedule_date = add_months(self.available_for_use_date, n * 12)
previous_scheduled_date = add_months(d.depreciation_start_date, (n-1) * 12)
depreciation_amount = \
self.get_depreciation_amount_prorata_temporis(value_after_depreciation,
d, previous_scheduled_date, schedule_date)
elif n == list(range(number_of_pending_depreciations))[0]: if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
schedule_date = d.depreciation_start_date schedule_date = add_months(d.depreciation_start_date,
depreciation_amount = \ n * cint(d.frequency_of_depreciation))
self.get_depreciation_amount_prorata_temporis(value_after_depreciation,
d, self.available_for_use_date, schedule_date)
else: # For first row
schedule_date = add_months(d.depreciation_start_date, n * 12) if has_pro_rata and n==0:
depreciation_amount = \ depreciation_amount, days = get_pro_rata_amt(d, depreciation_amount,
self.get_depreciation_amount_prorata_temporis(value_after_depreciation, d) self.available_for_use_date, d.depreciation_start_date)
# For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
to_date = add_months(self.available_for_use_date,
n * cint(d.frequency_of_depreciation))
if value_after_depreciation != 0: depreciation_amount, days = get_pro_rata_amt(d,
value_after_depreciation -= flt(depreciation_amount) depreciation_amount, schedule_date, to_date)
self.append("schedules", { schedule_date = add_days(schedule_date, days)
"schedule_date": schedule_date,
"depreciation_amount": depreciation_amount,
"depreciation_method": d.depreciation_method,
"finance_book": d.finance_book,
"finance_book_id": d.idx
})
else:
for n in range(number_of_pending_depreciations):
schedule_date = add_months(d.depreciation_start_date,
n * cint(d.frequency_of_depreciation))
if d.depreciation_method in ("Straight Line", "Manual"): if not depreciation_amount: continue
days = date_diff(schedule_date, from_date) value_after_depreciation -= flt(depreciation_amount,
if n == 0: days += 1 self.precision("gross_purchase_amount"))
depreciation_amount = days * rate_per_day # Adjust depreciation amount in the last period based on the expected value after useful life
from_date = schedule_date if d.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
else: and value_after_depreciation != d.expected_value_after_useful_life)
depreciation_amount = self.get_depreciation_amount(value_after_depreciation, or value_after_depreciation < d.expected_value_after_useful_life):
d.total_number_of_depreciations, d) depreciation_amount += (value_after_depreciation - d.expected_value_after_useful_life)
skip_row = True
if depreciation_amount: if depreciation_amount > 0:
value_after_depreciation -= flt(depreciation_amount) 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
})
self.append("schedules", { def check_is_pro_rata(self, row):
"schedule_date": schedule_date, has_pro_rata = False
"depreciation_amount": depreciation_amount,
"depreciation_method": d.depreciation_method, days = date_diff(row.depreciation_start_date, self.available_for_use_date) + 1
"finance_book": d.finance_book, total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
"finance_book_id": d.idx
}) if days < total_days:
has_pro_rata = True
return has_pro_rata
def validate_asset_finance_books(self, row): def validate_asset_finance_books(self, row):
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
@ -261,31 +252,20 @@ class Asset(AccountsController):
return flt(self.get('finance_books')[cint(idx)-1].value_after_depreciation) return flt(self.get('finance_books')[cint(idx)-1].value_after_depreciation)
def get_depreciation_amount(self, depreciable_value, total_number_of_depreciations, row): def get_depreciation_amount(self, depreciable_value, total_number_of_depreciations, row):
if row.depreciation_method in ["Straight Line", "Manual"]: precision = self.precision("gross_purchase_amount")
amt = (flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life) -
flt(self.opening_accumulated_depreciation))
depreciation_amount = amt * row.rate_of_depreciation
else:
depreciation_amount = flt(depreciable_value) * (flt(row.rate_of_depreciation) / 100)
value_after_depreciation = flt(depreciable_value) - depreciation_amount
if value_after_depreciation < flt(row.expected_value_after_useful_life):
depreciation_amount = flt(depreciable_value) - flt(row.expected_value_after_useful_life)
return depreciation_amount
def get_depreciation_amount_prorata_temporis(self, depreciable_value, row, start_date=None, end_date=None):
if start_date and end_date:
prorata_temporis = min(abs(flt(date_diff(str(end_date), str(start_date)))) / flt(frappe.db.get_value("Asset Settings", None, "number_of_days_in_fiscal_year")), 1)
else:
prorata_temporis = 1
if row.depreciation_method in ("Straight Line", "Manual"): if row.depreciation_method in ("Straight Line", "Manual"):
depreciation_left = (cint(row.total_number_of_depreciations) - cint(self.number_of_depreciations_booked))
if not depreciation_left:
frappe.msgprint(_("All the depreciations has been booked"))
depreciation_amount = flt(row.expected_value_after_useful_life)
return depreciation_amount
depreciation_amount = (flt(row.value_after_depreciation) - depreciation_amount = (flt(row.value_after_depreciation) -
flt(row.expected_value_after_useful_life)) / (cint(row.total_number_of_depreciations) - flt(row.expected_value_after_useful_life)) / depreciation_left
cint(self.number_of_depreciations_booked)) * prorata_temporis
else: else:
depreciation_amount = self.get_depreciation_amount(depreciable_value, row.total_number_of_depreciations, row) depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100), precision)
return depreciation_amount return depreciation_amount
@ -301,9 +281,12 @@ class Asset(AccountsController):
flt(accumulated_depreciation_after_full_schedule), flt(accumulated_depreciation_after_full_schedule),
self.precision('gross_purchase_amount')) self.precision('gross_purchase_amount'))
if row.expected_value_after_useful_life < asset_value_after_full_schedule: if (row.expected_value_after_useful_life and
row.expected_value_after_useful_life < asset_value_after_full_schedule):
frappe.throw(_("Depreciation Row {0}: Expected value after useful life must be greater than or equal to {1}") frappe.throw(_("Depreciation Row {0}: Expected value after useful life must be greater than or equal to {1}")
.format(row.idx, asset_value_after_full_schedule)) .format(row.idx, asset_value_after_full_schedule))
elif not row.expected_value_after_useful_life:
row.expected_value_after_useful_life = asset_value_after_full_schedule
def validate_cancellation(self): def validate_cancellation(self):
if self.status not in ("Submitted", "Partially Depreciated", "Fully Depreciated"): if self.status not in ("Submitted", "Partially Depreciated", "Fully Depreciated"):
@ -412,15 +395,7 @@ class Asset(AccountsController):
if isinstance(args, string_types): if isinstance(args, string_types):
args = json.loads(args) args = json.loads(args)
number_of_depreciations_booked = 0
if self.is_existing_asset:
number_of_depreciations_booked = self.number_of_depreciations_booked
float_precision = cint(frappe.db.get_default("float_precision")) or 2 float_precision = cint(frappe.db.get_default("float_precision")) or 2
tot_no_of_depreciation = flt(args.get("total_number_of_depreciations")) - flt(number_of_depreciations_booked)
if args.get("depreciation_method") in ["Straight Line", "Manual"]:
return 1.0 / tot_no_of_depreciation
if args.get("depreciation_method") == 'Double Declining Balance': if args.get("depreciation_method") == 'Double Declining Balance':
return 200.0 / args.get("total_number_of_depreciations") return 200.0 / args.get("total_number_of_depreciations")
@ -600,3 +575,15 @@ def make_journal_entry(asset_name):
def is_cwip_accounting_disabled(): def is_cwip_accounting_disabled():
return cint(frappe.db.get_single_value("Asset Settings", "disable_cwip_accounting")) return cint(frappe.db.get_single_value("Asset Settings", "disable_cwip_accounting"))
def get_pro_rata_amt(row, depreciation_amount, from_date, to_date):
days = date_diff(to_date, from_date)
total_days = get_total_days(to_date, row.frequency_of_depreciation)
return (depreciation_amount * flt(days)) / flt(total_days), days
def get_total_days(date, frequency):
period_start_date = add_months(date,
cint(frequency) * -1)
return date_diff(date, period_start_date)

View File

@ -88,23 +88,23 @@ class TestAsset(unittest.TestCase):
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') 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)
asset.calculate_depreciation = 1 asset.calculate_depreciation = 1
asset.available_for_use_date = '2020-06-06' asset.available_for_use_date = '2030-01-01'
asset.purchase_date = '2020-06-06' asset.purchase_date = '2030-01-01'
asset.append("finance_books", { asset.append("finance_books", {
"expected_value_after_useful_life": 10000, "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": 12,
"depreciation_start_date": "2020-06-06" "depreciation_start_date": "2030-12-31"
}) })
asset.save() asset.save()
self.assertEqual(asset.status, "Draft") self.assertEqual(asset.status, "Draft")
expected_schedules = [ expected_schedules = [
["2020-06-06", 147.54, 147.54], ["2030-12-31", 30000.00, 30000.00],
["2021-04-06", 44852.46, 45000.0], ["2031-12-31", 30000.00, 60000.00],
["2022-02-06", 45000.0, 90000.00] ["2032-12-31", 30000.00, 90000.00]
] ]
schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
@ -118,20 +118,21 @@ class TestAsset(unittest.TestCase):
asset.calculate_depreciation = 1 asset.calculate_depreciation = 1
asset.number_of_depreciations_booked = 1 asset.number_of_depreciations_booked = 1
asset.opening_accumulated_depreciation = 40000 asset.opening_accumulated_depreciation = 40000
asset.available_for_use_date = "2030-06-06"
asset.append("finance_books", { asset.append("finance_books", {
"expected_value_after_useful_life": 10000, "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": 12,
"depreciation_start_date": "2020-06-06" "depreciation_start_date": "2030-12-31"
}) })
asset.insert() asset.insert()
self.assertEqual(asset.status, "Draft") self.assertEqual(asset.status, "Draft")
asset.save() asset.save()
expected_schedules = [ expected_schedules = [
["2020-06-06", 164.47, 40164.47], ["2030-12-31", 14246.58, 54246.58],
["2021-04-06", 49835.53, 90000.00] ["2031-12-31", 25000.00, 79246.58],
["2032-06-06", 10753.42, 90000.00]
] ]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
for d in asset.get("schedules")] for d in asset.get("schedules")]
@ -145,24 +146,23 @@ class TestAsset(unittest.TestCase):
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') 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)
asset.calculate_depreciation = 1 asset.calculate_depreciation = 1
asset.available_for_use_date = '2020-06-06' asset.available_for_use_date = '2030-01-01'
asset.purchase_date = '2020-06-06' asset.purchase_date = '2030-01-01'
asset.append("finance_books", { asset.append("finance_books", {
"expected_value_after_useful_life": 10000, "expected_value_after_useful_life": 10000,
"next_depreciation_date": "2020-12-31",
"depreciation_method": "Double Declining Balance", "depreciation_method": "Double Declining Balance",
"total_number_of_depreciations": 3, "total_number_of_depreciations": 3,
"frequency_of_depreciation": 10, "frequency_of_depreciation": 12,
"depreciation_start_date": "2020-06-06" "depreciation_start_date": '2030-12-31'
}) })
asset.insert() asset.insert()
self.assertEqual(asset.status, "Draft") self.assertEqual(asset.status, "Draft")
asset.save() asset.save()
expected_schedules = [ expected_schedules = [
["2020-06-06", 66666.67, 66666.67], ['2030-12-31', 66667.00, 66667.00],
["2021-04-06", 22222.22, 88888.89], ['2031-12-31', 22222.11, 88889.11],
["2022-02-06", 1111.11, 90000.0] ['2032-12-31', 1110.89, 90000.0]
] ]
schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
@ -177,23 +177,21 @@ class TestAsset(unittest.TestCase):
asset.is_existing_asset = 1 asset.is_existing_asset = 1
asset.number_of_depreciations_booked = 1 asset.number_of_depreciations_booked = 1
asset.opening_accumulated_depreciation = 50000 asset.opening_accumulated_depreciation = 50000
asset.available_for_use_date = '2030-01-01'
asset.purchase_date = '2029-11-30'
asset.append("finance_books", { asset.append("finance_books", {
"expected_value_after_useful_life": 10000, "expected_value_after_useful_life": 10000,
"next_depreciation_date": "2020-12-31",
"depreciation_method": "Double Declining Balance", "depreciation_method": "Double Declining Balance",
"total_number_of_depreciations": 3, "total_number_of_depreciations": 3,
"frequency_of_depreciation": 10, "frequency_of_depreciation": 12,
"depreciation_start_date": "2020-06-06" "depreciation_start_date": "2030-12-31"
}) })
asset.insert() asset.insert()
self.assertEqual(asset.status, "Draft") self.assertEqual(asset.status, "Draft")
asset.save()
asset.save()
expected_schedules = [ expected_schedules = [
["2020-06-06", 33333.33, 83333.33], ["2030-12-31", 33333.50, 83333.50],
["2021-04-06", 6666.67, 90000.0] ["2031-12-31", 6666.50, 90000.0]
] ]
schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
@ -209,25 +207,25 @@ class TestAsset(unittest.TestCase):
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') 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)
asset.calculate_depreciation = 1 asset.calculate_depreciation = 1
asset.purchase_date = '2020-01-30' asset.purchase_date = '2030-01-30'
asset.is_existing_asset = 0 asset.is_existing_asset = 0
asset.available_for_use_date = "2020-01-30" asset.available_for_use_date = "2030-01-30"
asset.append("finance_books", { asset.append("finance_books", {
"expected_value_after_useful_life": 10000, "expected_value_after_useful_life": 10000,
"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": 12,
"depreciation_start_date": "2020-12-31" "depreciation_start_date": "2030-12-31"
}) })
asset.insert() asset.insert()
asset.save() asset.save()
expected_schedules = [ expected_schedules = [
["2020-12-31", 28000.0, 28000.0], ["2030-12-31", 27534.25, 27534.25],
["2021-12-31", 30000.0, 58000.0], ["2031-12-31", 30000.0, 57534.25],
["2022-12-31", 30000.0, 88000.0], ["2032-12-31", 30000.0, 87534.25],
["2023-01-30", 2000.0, 90000.0] ["2033-01-30", 2465.75, 90000.0]
] ]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
@ -266,8 +264,8 @@ class TestAsset(unittest.TestCase):
self.assertEqual(asset.get("schedules")[0].journal_entry[:4], "DEPR") self.assertEqual(asset.get("schedules")[0].journal_entry[:4], "DEPR")
expected_gle = ( expected_gle = (
("_Test Accumulated Depreciations - _TC", 0.0, 32129.24), ("_Test Accumulated Depreciations - _TC", 0.0, 30000.0),
("_Test Depreciations - _TC", 32129.24, 0.0) ("_Test Depreciations - _TC", 30000.0, 0.0)
) )
gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
@ -277,15 +275,15 @@ class TestAsset(unittest.TestCase):
self.assertEqual(gle, expected_gle) self.assertEqual(gle, expected_gle)
self.assertEqual(asset.get("value_after_depreciation"), 0) self.assertEqual(asset.get("value_after_depreciation"), 0)
def test_depreciation_entry_for_wdv(self): def test_depreciation_entry_for_wdv_without_pro_rata(self):
pr = make_purchase_receipt(item_code="Macbook Pro", pr = make_purchase_receipt(item_code="Macbook Pro",
qty=1, rate=8000.0, location="Test Location") qty=1, rate=8000.0, location="Test Location")
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') 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)
asset.calculate_depreciation = 1 asset.calculate_depreciation = 1
asset.available_for_use_date = '2030-06-06' asset.available_for_use_date = '2030-01-01'
asset.purchase_date = '2030-06-06' asset.purchase_date = '2030-01-01'
asset.append("finance_books", { asset.append("finance_books", {
"expected_value_after_useful_life": 1000, "expected_value_after_useful_life": 1000,
"depreciation_method": "Written Down Value", "depreciation_method": "Written Down Value",
@ -298,9 +296,41 @@ class TestAsset(unittest.TestCase):
self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
expected_schedules = [ expected_schedules = [
["2030-12-31", 4000.0, 4000.0], ["2030-12-31", 4000.00, 4000.00],
["2031-12-31", 2000.0, 6000.0], ["2031-12-31", 2000.00, 6000.00],
["2032-12-31", 1000.0, 7000.0], ["2032-12-31", 1000.00, 7000.0],
]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
for d in asset.get("schedules")]
self.assertEqual(schedules, expected_schedules)
def test_pro_rata_depreciation_entry_for_wdv(self):
pr = make_purchase_receipt(item_code="Macbook Pro",
qty=1, rate=8000.0, location="Test Location")
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
asset = frappe.get_doc('Asset', asset_name)
asset.calculate_depreciation = 1
asset.available_for_use_date = '2030-06-06'
asset.purchase_date = '2030-01-01'
asset.append("finance_books", {
"expected_value_after_useful_life": 1000,
"depreciation_method": "Written Down Value",
"total_number_of_depreciations": 3,
"frequency_of_depreciation": 12,
"depreciation_start_date": "2030-12-31"
})
asset.save(ignore_permissions=True)
self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
expected_schedules = [
["2030-12-31", 2279.45, 2279.45],
["2031-12-31", 2860.28, 5139.73],
["2032-12-31", 1430.14, 6569.87],
["2033-06-06", 430.13, 7000.0],
] ]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
@ -346,18 +376,19 @@ class TestAsset(unittest.TestCase):
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') 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)
asset.calculate_depreciation = 1 asset.calculate_depreciation = 1
asset.available_for_use_date = '2020-06-06' asset.available_for_use_date = nowdate()
asset.purchase_date = '2020-06-06' asset.purchase_date = nowdate()
asset.append("finance_books", { asset.append("finance_books", {
"expected_value_after_useful_life": 10000, "expected_value_after_useful_life": 10000,
"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": "2020-06-06" "depreciation_start_date": nowdate()
}) })
asset.insert() asset.insert()
asset.submit() asset.submit()
post_depreciation_entries(date="2021-01-01")
post_depreciation_entries(date=add_months(nowdate(), 10))
scrap_asset(asset.name) scrap_asset(asset.name)
@ -366,9 +397,9 @@ class TestAsset(unittest.TestCase):
self.assertTrue(asset.journal_entry_for_scrap) self.assertTrue(asset.journal_entry_for_scrap)
expected_gle = ( expected_gle = (
("_Test Accumulated Depreciations - _TC", 147.54, 0.0), ("_Test Accumulated Depreciations - _TC", 30000.0, 0.0),
("_Test Fixed Asset - _TC", 0.0, 100000.0), ("_Test Fixed Asset - _TC", 0.0, 100000.0),
("_Test Gain/Loss on Asset Disposal - _TC", 99852.46, 0.0) ("_Test Gain/Loss on Asset Disposal - _TC", 70000.0, 0.0)
) )
gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
@ -412,9 +443,9 @@ class TestAsset(unittest.TestCase):
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
expected_gle = ( expected_gle = (
("_Test Accumulated Depreciations - _TC", 23051.47, 0.0), ("_Test Accumulated Depreciations - _TC", 20392.16, 0.0),
("_Test Fixed Asset - _TC", 0.0, 100000.0), ("_Test Fixed Asset - _TC", 0.0, 100000.0),
("_Test Gain/Loss on Asset Disposal - _TC", 51948.53, 0.0), ("_Test Gain/Loss on Asset Disposal - _TC", 54607.84, 0.0),
("Debtors - _TC", 25000.0, 0.0) ("Debtors - _TC", 25000.0, 0.0)
) )

View File

@ -46,75 +46,6 @@
"translatable": 0, "translatable": 0,
"unique": 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": "schedule_based_on_fiscal_year",
"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": "Calculate Prorated Depreciation Schedule Based on Fiscal Year",
"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,
"default": "360",
"depends_on": "eval:doc.schedule_based_on_fiscal_year",
"description": "This value is used for pro-rata temporis calculation",
"fetch_if_empty": 0,
"fieldname": "number_of_days_in_fiscal_year",
"fieldtype": "Data",
"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": "Number of Days in Fiscal Year",
"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_bulk_edit": 0,
"allow_in_quick_entry": 0, "allow_in_quick_entry": 0,
@ -159,7 +90,7 @@
"issingle": 1, "issingle": 1,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2019-03-08 10:44:41.924547", "modified": "2019-05-26 18:31:19.930563",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Settings", "name": "Asset Settings",

View File

@ -484,7 +484,7 @@ def make_rm_stock_entry(purchase_order, rm_items):
'from_warehouse': rm_item_data["warehouse"], 'from_warehouse': rm_item_data["warehouse"],
'stock_uom': rm_item_data["stock_uom"], 'stock_uom': rm_item_data["stock_uom"],
'main_item_code': rm_item_data["item_code"], 'main_item_code': rm_item_data["item_code"],
'allow_alternative_item': item_wh[rm_item_code].get('allow_alternative_item') 'allow_alternative_item': item_wh.get(rm_item_code, {}).get('allow_alternative_item')
} }
} }
stock_entry.add_to_stock_entry_detail(items_dict) stock_entry.add_to_stock_entry_detail(items_dict)

View File

@ -0,0 +1,41 @@
# Version 12 Release Notes
### Accounting
1. [Accounting Dimensions](https://erpnext.com/docs/user/manual/en/accounts/accounting-dimensions)
1. [Chart of Accounts Importer](https://erpnext.com/docs/user/manual/en/setting-up/chart-of-accounts-importer)
1. [Invoice Discounting](https://erpnext.com/docs/user/manual/en/accounts/invoice_discounting)
1. [Tally Migrator](https://github.com/frappe/erpnext/pull/17405)
### Stock
1. [Serialized & Batched Item Reconciliation](https://erpnext.com/docs/user/manual/en/setting-up/stock-reconciliation#12-for-serialized-items)
1. [Auto Fetch Serialized Items](https://erpnext.com/version-12/release-notes/features#new-upload-dialog)
1. [Item Tax Templates](https://erpnext.com/docs/user/manual/en/accounts/item-tax-template)
### HR
1. [Auto Attendance](https://erpnext.com/docs/user/manual/en/human-resources/auto-attendance)
1. [Employee Skill Map](https://erpnext.com/docs/user/manual/en/human-resources/employee_skill_map)
1. [Encrypted Salary Slips](https://erpnext.com/docs/user/manual/en/human-resources/hr-settings#24-encrypt-salary-slips-in-emails)
1. [Leave Ledger](https://erpnext.com/docs/user/manual/en/human-resources/leave-ledger-entry)
1. [Staffing Plan](https://erpnext.com/docs/user/manual/en/human-resources/staffing-plan)
### CRM
1. [Promotional Scheme](https://erpnext.com/docs/user/manual/en/accounts/promotional-schemes)
1. [SLA](https://erpnext.com/docs/user/manual/en/support/service-level-agreement)
1. [Exotel Call Integration](https://erpnext.com/docs/user/manual/en/erpnext_integration/exotel_integration)
1. [Email Campaign](https://erpnext.com/docs/user/manual/en/CRM/email-campaign)
### Domain Specific Features
1. [Learning Management System](https://erpnext.com/docs/user/manual/en/education/setting-up-lms)
1. [Quality Management System](https://erpnext.com/docs/user/manual/en/quality-management)
1. [Production Planning Enhancements](https://erpnext.com/docs/user/manual/en/manufacturing/production-plan/planning-for-material-requests)
1. [Project Template](https://erpnext.com/docs/user/manual/en/projects/project-template)
### New Reports
1. [Bank Remittance](https://erpnext.com/docs/user/manual/en/human-resources/human-resources-reports#bank-remittance-report)
1. [BOM Explorer](https://erpnext.com/docs/user/manual/en/stock/articles/bom_explorer)
1. [Billing Summary Report](https://erpnext.com/docs/user/manual/en/projects/reports/billing_summary_reports)
1. [Procurement Tracker Report](docs/user/manual/en/buying/articles/procurement-tracker-report)
1. [Loan Repayment](https://erpnext.com/docs/user/manual/en/human-resources/human-resources-reports#loan-repayment-report)
1. [GSTR-3B](https://erpnext.com/docs/user/manual/en/regional/india/gst-3b-report)
1. [Sales Partner](https://erpnext.com/docs/user/manual/en/selling/sales-partner#sales-partner-reports)
1. [Sales Partner Target Variance based on Item Group](https://erpnext.com/docs/user/manual/en/selling/sales-partner#sales-partner-target-variance-based-on-item-group)

View File

@ -6,15 +6,13 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.crm.doctype.utils import get_scheduled_employees_for_popup from erpnext.crm.doctype.utils import get_scheduled_employees_for_popup, strip_number
from frappe.contacts.doctype.contact.contact import get_contact_with_phone_number from frappe.contacts.doctype.contact.contact import get_contact_with_phone_number
from erpnext.crm.doctype.lead.lead import get_lead_with_phone_number from erpnext.crm.doctype.lead.lead import get_lead_with_phone_number
class CallLog(Document): class CallLog(Document):
def before_insert(self): def before_insert(self):
# strip 0 from the start of the number for proper number comparisions number = strip_number(self.get('from'))
# eg. 07888383332 should match with 7888383332
number = self.get('from').lstrip('0')
self.contact = get_contact_with_phone_number(number) self.contact = get_contact_with_phone_number(number)
self.lead = get_lead_with_phone_number(number) self.lead = get_lead_with_phone_number(number)
@ -48,13 +46,14 @@ def add_call_summary(call_log, summary):
doc.add_comment('Comment', frappe.bold(_('Call Summary')) + '<br><br>' + summary) doc.add_comment('Comment', frappe.bold(_('Call Summary')) + '<br><br>' + summary)
def get_employees_with_number(number): def get_employees_with_number(number):
number = strip_number(number)
if not number: return [] if not number: return []
employee_emails = frappe.cache().hget('employees_with_number', number) employee_emails = frappe.cache().hget('employees_with_number', number)
if employee_emails: return employee_emails if employee_emails: return employee_emails
employees = frappe.get_all('Employee', filters={ employees = frappe.get_all('Employee', filters={
'cell_number': ['like', '%{}'.format(number.lstrip('0'))], 'cell_number': ['like', '%{}%'.format(number)],
'user_id': ['!=', ''] 'user_id': ['!=', '']
}, fields=['user_id']) }, fields=['user_id'])
@ -64,23 +63,29 @@ def get_employees_with_number(number):
return employee return employee
def set_caller_information(doc, state): def set_caller_information(doc, state):
'''Called from hoooks on creation of Lead or Contact''' '''Called from hooks on creation of Lead or Contact'''
if doc.doctype not in ['Lead', 'Contact']: return if doc.doctype not in ['Lead', 'Contact']: return
numbers = [doc.get('phone'), doc.get('mobile_no')] numbers = [doc.get('phone'), doc.get('mobile_no')]
for_doc = doc.doctype.lower() # contact for Contact and lead for Lead
fieldname = doc.doctype.lower()
# contact_name or lead_name
display_name_field = '{}_name'.format(fieldname)
for number in numbers: for number in numbers:
number = strip_number(number)
if not number: continue if not number: continue
print(number)
filters = frappe._dict({ filters = frappe._dict({
'from': ['like', '%{}'.format(number.lstrip('0'))], 'from': ['like', '%{}'.format(number)],
for_doc: '' fieldname: ''
}) })
logs = frappe.get_all('Call Log', filters=filters) logs = frappe.get_all('Call Log', filters=filters)
for log in logs: for log in logs:
call_log = frappe.get_doc('Call Log', log.name) frappe.db.set_value('Call Log', log.name, {
call_log.set(for_doc, doc.name) fieldname: doc.name,
call_log.save(ignore_permissions=True) display_name_field: doc.get_title()
}, update_modified=False)

View File

@ -41,6 +41,11 @@ def get_data():
"name": "Lead Source", "name": "Lead Source",
"description": _("Track Leads by Lead Source.") "description": _("Track Leads by Lead Source.")
}, },
{
"type": "doctype",
"name": "Contract",
"description": _("Helps you keep tracks of Contracts based on Supplier, Customer and Employee"),
},
] ]
}, },
{ {

View File

@ -166,6 +166,10 @@ def get_data():
"name": "Salary Slip", "name": "Salary Slip",
"onboard": 1, "onboard": 1,
}, },
{
"type": "doctype",
"name": "Payroll Period",
},
{ {
"type": "doctype", "type": "doctype",
"name": "Salary Component", "name": "Salary Component",

View File

@ -94,6 +94,13 @@ def get_data():
"name": "BOM Update Tool", "name": "BOM Update Tool",
"description": _("Replace BOM and update latest price in all BOMs"), "description": _("Replace BOM and update latest price in all BOMs"),
}, },
{
"type": "page",
"label": _("BOM Comparison Tool"),
"name": "bom-comparison-tool",
"description": _("Compare BOMs for changes in Raw Materials and Operations"),
"data_doctype": "BOM"
},
] ]
}, },
{ {

View File

@ -727,7 +727,7 @@ def get_items_from_bom(item_code, bom, exploded_item=1):
where where
t2.parent = t1.name and t1.item = %s t2.parent = t1.name and t1.item = %s
and t1.docstatus = 1 and t1.is_active = 1 and t1.name = %s and t1.docstatus = 1 and t1.is_active = 1 and t1.name = %s
and t2.item_code = t3.name and t3.is_stock_item = 1""".format(doctype), and t2.item_code = t3.name""".format(doctype),
(item_code, bom), as_dict=1) (item_code, bom), as_dict=1)
if not bom_items: if not bom_items:

View File

@ -371,7 +371,7 @@ def get_expense_account(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql("""select tabAccount.name from `tabAccount` return frappe.db.sql("""select tabAccount.name from `tabAccount`
where (tabAccount.report_type = "Profit and Loss" where (tabAccount.report_type = "Profit and Loss"
or tabAccount.account_type in ("Expense Account", "Fixed Asset", "Temporary", "Asset Received But Not Billed")) or tabAccount.account_type in ("Expense Account", "Fixed Asset", "Temporary", "Asset Received But Not Billed", "Capital Work in Progress"))
and tabAccount.is_group=0 and tabAccount.is_group=0
and tabAccount.docstatus!=2 and tabAccount.docstatus!=2
and tabAccount.{key} LIKE %(txt)s and tabAccount.{key} LIKE %(txt)s

View File

@ -24,7 +24,9 @@ def validate_return_against(doc):
else: else:
ref_doc = frappe.get_doc(doc.doctype, doc.return_against) ref_doc = frappe.get_doc(doc.doctype, doc.return_against)
if ref_doc.company == doc.company and ref_doc.customer == doc.customer and ref_doc.docstatus == 1: party_type = "customer" if doc.doctype in ("Sales Invoice", "Delivery Note") else "supplier"
if ref_doc.company == doc.company and ref_doc.get(party_type) == doc.get(party_type) and ref_doc.docstatus == 1:
# validate posting date time # validate posting date time
return_posting_datetime = "%s %s" % (doc.posting_date, doc.get("posting_time") or "00:00:00") return_posting_datetime = "%s %s" % (doc.posting_date, doc.get("posting_time") or "00:00:00")
ref_posting_datetime = "%s %s" % (ref_doc.posting_date, ref_doc.get("posting_time") or "00:00:00") ref_posting_datetime = "%s %s" % (ref_doc.posting_date, ref_doc.get("posting_time") or "00:00:00")

View File

@ -45,6 +45,7 @@ class SellingController(StockController):
self.set_gross_profit() self.set_gross_profit()
set_default_income_account_for_item(self) set_default_income_account_for_item(self)
self.set_customer_address() self.set_customer_address()
self.validate_for_duplicate_items()
def set_missing_values(self, for_validate=False): def set_missing_values(self, for_validate=False):
@ -381,6 +382,34 @@ class SellingController(StockController):
if self.get(address_field): if self.get(address_field):
self.set(address_display_field, get_address_display(self.get(address_field))) self.set(address_display_field, get_address_display(self.get(address_field)))
def validate_for_duplicate_items(self):
check_list, chk_dupl_itm = [], []
if cint(frappe.db.get_single_value("Selling Settings", "allow_multiple_items")):
return
for d in self.get('items'):
if self.doctype == "Sales Invoice":
e = [d.item_code, d.description, d.warehouse, d.sales_order or d.delivery_note, d.batch_no or '']
f = [d.item_code, d.description, d.sales_order or d.delivery_note]
elif self.doctype == "Delivery Note":
e = [d.item_code, d.description, d.warehouse, d.against_sales_order or d.against_sales_invoice, d.batch_no or '']
f = [d.item_code, d.description, d.against_sales_order or d.against_sales_invoice]
elif self.doctype == "Sales Order":
e = [d.item_code, d.description, d.warehouse, d.batch_no or '']
f = [d.item_code, d.description]
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1:
if e in check_list:
frappe.throw(_("Note: Item {0} entered multiple times").format(d.item_code))
else:
check_list.append(e)
else:
if f in chk_dupl_itm:
frappe.throw(_("Note: Item {0} entered multiple times").format(d.item_code))
else:
chk_dupl_itm.append(f)
def validate_items(self): def validate_items(self):
# validate items to see if they have is_sales_item enabled # validate items to see if they have is_sales_item enabled
from erpnext.controllers.buying_controller import validate_item_type from erpnext.controllers.buying_controller import validate_item_type

View File

@ -21,42 +21,45 @@ def get_list_context(context=None):
def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified"): def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified"):
user = frappe.session.user user = frappe.session.user
key = None ignore_permissions = False
if not filters: filters = [] if not filters: filters = []
if doctype == 'Supplier Quotation': if doctype == 'Supplier Quotation':
filters.append((doctype, "docstatus", "<", 2)) filters.append((doctype, 'docstatus', '<', 2))
else: else:
filters.append((doctype, "docstatus", "=", 1)) filters.append((doctype, 'docstatus', '=', 1))
if (user != "Guest" and is_website_user()) or doctype == 'Request for Quotation': if (user != 'Guest' and is_website_user()) or doctype == 'Request for Quotation':
parties_doctype = 'Request for Quotation Supplier' if doctype == 'Request for Quotation' else doctype parties_doctype = 'Request for Quotation Supplier' if doctype == 'Request for Quotation' else doctype
# find party for this contact # find party for this contact
customers, suppliers = get_customers_suppliers(parties_doctype, user) customers, suppliers = get_customers_suppliers(parties_doctype, user)
if not customers and not suppliers: return [] if customers:
if doctype == 'Quotation':
key, parties = get_party_details(customers, suppliers) filters.append(('quotation_to', '=', 'Customer'))
filters.append(('party_name', 'in', customers))
if doctype == 'Request for Quotation': else:
return rfq_transaction_list(parties_doctype, doctype, parties, limit_start, limit_page_length) filters.append(('customer', 'in', customers))
elif suppliers:
filters.append((doctype, key, "in", parties)) filters.append(('supplier', 'in', suppliers))
if key:
return post_process(doctype, get_list_for_transactions(doctype, txt,
filters=filters, fields="name",limit_start=limit_start,
limit_page_length=limit_page_length,ignore_permissions=True,
order_by="modified desc"))
else: else:
return [] return []
return post_process(doctype, get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length, if doctype == 'Request for Quotation':
fields="name", order_by="modified desc")) parties = customers or suppliers
return rfq_transaction_list(parties_doctype, doctype, parties, limit_start, limit_page_length)
# Since customers and supplier do not have direct access to internal doctypes
ignore_permissions = True
transactions = get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length,
fields='name', ignore_permissions=ignore_permissions, order_by='modified desc')
return post_process(doctype, transactions)
def get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length=20, def get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length=20,
ignore_permissions=False,fields=None, order_by=None): ignore_permissions=False, fields=None, order_by=None):
""" Get List of transactions like Invoices, Orders """ """ Get List of transactions like Invoices, Orders """
from frappe.www.list import get_list from frappe.www.list import get_list
meta = frappe.get_meta(doctype) meta = frappe.get_meta(doctype)
@ -77,22 +80,12 @@ def get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_len
if or_filters: if or_filters:
for r in frappe.get_list(doctype, fields=fields,filters=filters, or_filters=or_filters, for r in frappe.get_list(doctype, fields=fields,filters=filters, or_filters=or_filters,
limit_start=limit_start, limit_page_length=limit_page_length, limit_start=limit_start, limit_page_length=limit_page_length,
ignore_permissions=ignore_permissions, order_by=order_by): ignore_permissions=ignore_permissions, order_by=order_by):
data.append(r) data.append(r)
return data return data
def get_party_details(customers, suppliers):
if customers:
key, parties = "customer", customers
elif suppliers:
key, parties = "supplier", suppliers
else:
key, parties = "customer", []
return key, parties
def rfq_transaction_list(parties_doctype, doctype, parties, limit_start, limit_page_length): def rfq_transaction_list(parties_doctype, doctype, parties, limit_start, limit_page_length):
data = frappe.db.sql("""select distinct parent as name, supplier from `tab{doctype}` data = frappe.db.sql("""select distinct parent as name, supplier from `tab{doctype}`
where supplier = '{supplier}' and docstatus=1 order by modified desc limit {start}, {len}""". where supplier = '{supplier}' and docstatus=1 order by modified desc limit {start}, {len}""".
@ -130,38 +123,56 @@ def get_customers_suppliers(doctype, user):
suppliers = [] suppliers = []
meta = frappe.get_meta(doctype) meta = frappe.get_meta(doctype)
customer_field_name = get_customer_field_name(doctype)
has_customer_field = meta.has_field(customer_field_name)
has_supplier_field = meta.has_field('supplier')
if has_common(["Supplier", "Customer"], frappe.get_roles(user)): if has_common(["Supplier", "Customer"], frappe.get_roles(user)):
contacts = frappe.db.sql(""" contacts = frappe.db.sql("""
select select
`tabContact`.email_id, `tabContact`.email_id,
`tabDynamic Link`.link_doctype, `tabDynamic Link`.link_doctype,
`tabDynamic Link`.link_name `tabDynamic Link`.link_name
from from
`tabContact`, `tabDynamic Link` `tabContact`, `tabDynamic Link`
where where
`tabContact`.name=`tabDynamic Link`.parent and `tabContact`.email_id =%s `tabContact`.name=`tabDynamic Link`.parent and `tabContact`.email_id =%s
""", user, as_dict=1) """, user, as_dict=1)
customers = [c.link_name for c in contacts if c.link_doctype == 'Customer'] \ customers = [c.link_name for c in contacts if c.link_doctype == 'Customer']
if meta.get_field("customer") else None suppliers = [c.link_name for c in contacts if c.link_doctype == 'Supplier']
suppliers = [c.link_name for c in contacts if c.link_doctype == 'Supplier'] \
if meta.get_field("supplier") else None
elif frappe.has_permission(doctype, 'read', user=user): elif frappe.has_permission(doctype, 'read', user=user):
customers = [customer.name for customer in frappe.get_list("Customer")] \ customer_list = frappe.get_list("Customer")
if meta.get_field("customer") else None customers = suppliers = [customer.name for customer in customer_list]
suppliers = [supplier.name for supplier in frappe.get_list("Customer")] \
if meta.get_field("supplier") else None
return customers, suppliers return customers if has_customer_field else None, \
suppliers if has_supplier_field else None
def has_website_permission(doc, ptype, user, verbose=False): def has_website_permission(doc, ptype, user, verbose=False):
doctype = doc.doctype doctype = doc.doctype
customers, suppliers = get_customers_suppliers(doctype, user) customers, suppliers = get_customers_suppliers(doctype, user)
if customers: if customers:
return frappe.get_all(doctype, filters=[(doctype, "customer", "in", customers), return frappe.db.exists(doctype, get_customer_filter(doc, customers))
(doctype, "name", "=", doc.name)]) and True or False
elif suppliers: elif suppliers:
fieldname = 'suppliers' if doctype == 'Request for Quotation' else 'supplier' fieldname = 'suppliers' if doctype == 'Request for Quotation' else 'supplier'
return frappe.get_all(doctype, filters=[(doctype, fieldname, "in", suppliers), return frappe.db.exists(doctype, filters={
(doctype, "name", "=", doc.name)]) and True or False 'name': doc.name,
fieldname: ["in", suppliers]
})
else: else:
return False return False
def get_customer_filter(doc, customers):
doctype = doc.doctype
filters = frappe._dict()
filters.name = doc.name
filters[get_customer_field_name(doctype)] = ['in', customers]
if doctype == 'Quotation':
filters.quotation_to = 'Customer'
return filters
def get_customer_field_name(doctype):
if doctype == 'Quotation':
return 'party_name'
else:
return 'customer'

View File

@ -54,6 +54,8 @@ def get_last_issue_from_customer(customer_name):
def get_scheduled_employees_for_popup(communication_medium): def get_scheduled_employees_for_popup(communication_medium):
if not communication_medium: return []
now_time = frappe.utils.nowtime() now_time = frappe.utils.nowtime()
weekday = frappe.utils.get_weekday() weekday = frappe.utils.get_weekday()
@ -73,3 +75,10 @@ def get_scheduled_employees_for_popup(communication_medium):
employee_emails = set([employee.user_id for employee in employees]) employee_emails = set([employee.user_id for employee in employees])
return employee_emails return employee_emails
def strip_number(number):
if not number: return
# strip 0 from the start of the number for proper number comparisions
# eg. 07888383332 should match with 7888383332
number = number.lstrip('0')
return number

View File

@ -5,6 +5,8 @@ frappe.listview_settings['Leave Application'] = {
return [__("Approved"), "green", "status,=,Approved"]; return [__("Approved"), "green", "status,=,Approved"];
} else if (doc.status === "Rejected") { } else if (doc.status === "Rejected") {
return [__("Rejected"), "red", "status,=,Rejected"]; return [__("Rejected"), "red", "status,=,Rejected"];
} else {
return [__("Open"), "red", "status,=,Open"];
} }
} }
}; };

View File

@ -27,6 +27,7 @@
"options": "Employee" "options": "Employee"
}, },
{ {
"fetch_from": "employee.employee_name",
"fieldname": "employee_name", "fieldname": "employee_name",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Employee Name" "label": "Employee Name"
@ -101,7 +102,7 @@
], ],
"in_create": 1, "in_create": 1,
"is_submittable": 1, "is_submittable": 1,
"modified": "2019-06-21 00:37:07.782810", "modified": "2019-08-20 14:40:04.130799",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Leave Ledger Entry", "name": "Leave Ledger Entry",

View File

@ -14,12 +14,12 @@ from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
class PayrollEntry(Document): class PayrollEntry(Document):
def onload(self): def onload(self):
if not self.docstatus==1 or self.salary_slips_submitted: if not self.docstatus==1 or self.salary_slips_submitted:
return return
# check if salary slips were manually submitted # check if salary slips were manually submitted
entries = frappe.db.count("Salary Slip", {'payroll_entry': self.name, 'docstatus': 1}, ['name']) entries = frappe.db.count("Salary Slip", {'payroll_entry': self.name, 'docstatus': 1}, ['name'])
if cint(entries) == len(self.employees): if cint(entries) == len(self.employees):
self.set_onload("submitted_ss", True) self.set_onload("submitted_ss", True)
def on_submit(self): def on_submit(self):
self.create_salary_slips() self.create_salary_slips()

View File

@ -97,16 +97,15 @@ def get_approvers(department):
def get_total_allocated_leaves(employee, leave_type, from_date, to_date): def get_total_allocated_leaves(employee, leave_type, from_date, to_date):
''' Returns leave allocation between from date and to date ''' ''' Returns leave allocation between from date and to date '''
filters= { leave_allocation_records = frappe.db.get_all('Leave Ledger Entry', filters={
'from_date': ['between', (from_date, to_date)], 'docstatus': 1,
'to_date': ['between', (from_date, to_date)], 'is_expired': 0,
'docstatus': 1, 'leave_type': leave_type,
'is_expired': 0, 'employee': employee,
'leave_type': leave_type, 'transaction_type': 'Leave Allocation'
'employee': employee, }, or_filters={
'transaction_type': 'Leave Allocation' 'from_date': ['between', (from_date, to_date)],
} 'to_date': ['between', (from_date, to_date)]
}, fields=['SUM(leaves) as leaves'])
leave_allocation_records = frappe.db.get_all('Leave Ledger Entry', filters=filters, fields=['SUM(leaves) as leaves'])
return flt(leave_allocation_records[0].get('leaves')) if leave_allocation_records else flt(0) return flt(leave_allocation_records[0].get('leaves')) if leave_allocation_records else flt(0)

View File

@ -23,7 +23,8 @@ def get_columns():
_("Model") + ":data:50", _("Location") + ":data:100", _("Model") + ":data:50", _("Location") + ":data:100",
_("Log") + ":Link/Vehicle Log:100", _("Odometer") + ":Int:80", _("Log") + ":Link/Vehicle Log:100", _("Odometer") + ":Int:80",
_("Date") + ":Date:100", _("Fuel Qty") + ":Float:80", _("Date") + ":Date:100", _("Fuel Qty") + ":Float:80",
_("Fuel Price") + ":Float:100",_("Service Expense") + ":Float:100" _("Fuel Price") + ":Float:100",_("Fuel Expense") + ":Float:100",
_("Service Expense") + ":Float:100"
] ]
return columns return columns
@ -32,7 +33,8 @@ def get_log_data(filters):
data = frappe.db.sql("""select data = frappe.db.sql("""select
vhcl.license_plate as "License", vhcl.make as "Make", vhcl.model as "Model", vhcl.license_plate as "License", vhcl.make as "Make", vhcl.model as "Model",
vhcl.location as "Location", log.name as "Log", log.odometer as "Odometer", vhcl.location as "Location", log.name as "Log", log.odometer as "Odometer",
log.date as "Date", log.fuel_qty as "Fuel Qty", log.price as "Fuel Price" log.date as "Date", log.fuel_qty as "Fuel Qty", log.price as "Fuel Price",
log.fuel_qty * log.price as "Fuel Expense"
from from
`tabVehicle` vhcl,`tabVehicle Log` log `tabVehicle` vhcl,`tabVehicle Log` log
where where
@ -58,7 +60,7 @@ def get_chart_data(data,period_list):
total_ser_exp=0 total_ser_exp=0
for row in data: for row in data:
if row["Date"] <= period.to_date and row["Date"] >= period.from_date: if row["Date"] <= period.to_date and row["Date"] >= period.from_date:
total_fuel_exp+=flt(row["Fuel Price"]) total_fuel_exp+=flt(row["Fuel Expense"])
total_ser_exp+=flt(row["Service Expense"]) total_ser_exp+=flt(row["Service Expense"])
fueldata.append([period.key,total_fuel_exp]) fueldata.append([period.key,total_fuel_exp])
servicedata.append([period.key,total_ser_exp]) servicedata.append([period.key,total_ser_exp])
@ -84,4 +86,4 @@ def get_chart_data(data,period_list):
} }
} }
chart["type"] = "line" chart["type"] = "line"
return chart return chart

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@ from erpnext.setup.utils import get_exchange_rate
from frappe.website.website_generator import WebsiteGenerator from frappe.website.website_generator import WebsiteGenerator
from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.stock.get_item_details import get_price_list_rate from erpnext.stock.get_item_details import get_price_list_rate
from frappe.core.doctype.version.version import get_diff
import functools import functools
@ -763,3 +764,52 @@ def add_additional_cost(stock_entry, work_order):
'description': name[0], 'description': name[0],
'amount': items.get(name[0]) 'amount': items.get(name[0])
}) })
@frappe.whitelist()
def get_bom_diff(bom1, bom2):
from frappe.model import table_fields
doc1 = frappe.get_doc('BOM', bom1)
doc2 = frappe.get_doc('BOM', bom2)
out = get_diff(doc1, doc2)
out.row_changed = []
out.added = []
out.removed = []
meta = doc1.meta
identifiers = {
'operations': 'operation',
'items': 'item_code',
'scrap_items': 'item_code',
'exploded_items': 'item_code'
}
for df in meta.fields:
old_value, new_value = doc1.get(df.fieldname), doc2.get(df.fieldname)
if df.fieldtype in table_fields:
identifier = identifiers[df.fieldname]
# make maps
old_row_by_identifier, new_row_by_identifier = {}, {}
for d in old_value:
old_row_by_identifier[d.get(identifier)] = d
for d in new_value:
new_row_by_identifier[d.get(identifier)] = d
# check rows for additions, changes
for i, d in enumerate(new_value):
if d.get(identifier) in old_row_by_identifier:
diff = get_diff(old_row_by_identifier[d.get(identifier)], d, for_child=True)
if diff and diff.changed:
out.row_changed.append((df.fieldname, i, d.get(identifier), diff.changed))
else:
out.added.append([df.fieldname, d.as_dict()])
# check for deletions
for d in old_value:
if not d.get(identifier) in new_row_by_identifier:
out.removed.append([df.fieldname, d.as_dict()])
return out

View File

@ -320,7 +320,8 @@ class ProductionPlan(Document):
'qty': data.get("stock_qty") * item.get("qty"), 'qty': data.get("stock_qty") * item.get("qty"),
'production_plan': self.name, 'production_plan': self.name,
'company': self.company, 'company': self.company,
'fg_warehouse': item.get("fg_warehouse") 'fg_warehouse': item.get("fg_warehouse"),
'update_consumed_material_cost_in_project': 0
}) })
work_order = self.create_work_order(data) work_order = self.create_work_order(data)

View File

@ -1,484 +1,504 @@
{ {
"allow_import": 1, "allow_import": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2013-01-10 16:34:16", "creation": "2013-01-10 16:34:16",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"field_order": [ "engine": "InnoDB",
"item", "field_order": [
"naming_series", "item",
"status", "naming_series",
"production_item", "status",
"item_name", "production_item",
"image", "item_name",
"bom_no", "image",
"allow_alternative_item", "bom_no",
"use_multi_level_bom", "column_break1",
"skip_transfer", "company",
"column_break1", "qty",
"company", "material_transferred_for_manufacturing",
"qty", "produced_qty",
"material_transferred_for_manufacturing", "sales_order",
"produced_qty", "project",
"sales_order", "settings_section",
"project", "allow_alternative_item",
"from_wip_warehouse", "use_multi_level_bom",
"warehouses", "column_break_18",
"wip_warehouse", "skip_transfer",
"fg_warehouse", "from_wip_warehouse",
"column_break_12", "update_consumed_material_cost_in_project",
"scrap_warehouse", "warehouses",
"required_items_section", "wip_warehouse",
"required_items", "fg_warehouse",
"time", "column_break_12",
"planned_start_date", "scrap_warehouse",
"actual_start_date", "required_items_section",
"column_break_13", "required_items",
"planned_end_date", "time",
"actual_end_date", "planned_start_date",
"expected_delivery_date", "actual_start_date",
"operations_section", "column_break_13",
"transfer_material_against", "planned_end_date",
"operations", "actual_end_date",
"section_break_22", "expected_delivery_date",
"planned_operating_cost", "operations_section",
"actual_operating_cost", "transfer_material_against",
"additional_operating_cost", "operations",
"column_break_24", "section_break_22",
"total_operating_cost", "planned_operating_cost",
"more_info", "actual_operating_cost",
"description", "additional_operating_cost",
"stock_uom", "column_break_24",
"column_break2", "total_operating_cost",
"material_request", "more_info",
"material_request_item", "description",
"sales_order_item", "stock_uom",
"production_plan", "column_break2",
"production_plan_item", "material_request",
"product_bundle_item", "material_request_item",
"amended_from" "sales_order_item",
], "production_plan",
"fields": [ "production_plan_item",
{ "product_bundle_item",
"fieldname": "item", "amended_from"
"fieldtype": "Section Break", ],
"options": "fa fa-gift" "fields": [
}, {
{ "fieldname": "item",
"fieldname": "naming_series", "fieldtype": "Section Break",
"fieldtype": "Select", "options": "fa fa-gift"
"label": "Series", },
"options": "MFG-WO-.YYYY.-", {
"print_hide": 1, "fieldname": "naming_series",
"reqd": 1, "fieldtype": "Select",
"set_only_once": 1 "label": "Series",
}, "options": "MFG-WO-.YYYY.-",
{ "print_hide": 1,
"default": "Draft", "reqd": 1,
"depends_on": "eval:!doc.__islocal", "set_only_once": 1
"fieldname": "status", },
"fieldtype": "Select", {
"label": "Status", "default": "Draft",
"no_copy": 1, "depends_on": "eval:!doc.__islocal",
"oldfieldname": "status", "fieldname": "status",
"oldfieldtype": "Select", "fieldtype": "Select",
"options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nStopped\nCancelled", "label": "Status",
"read_only": 1, "no_copy": 1,
"reqd": 1, "oldfieldname": "status",
"search_index": 1 "oldfieldtype": "Select",
}, "options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nStopped\nCancelled",
{ "read_only": 1,
"fieldname": "production_item", "reqd": 1,
"fieldtype": "Link", "search_index": 1
"in_global_search": 1, },
"in_list_view": 1, {
"in_standard_filter": 1, "fieldname": "production_item",
"label": "Item To Manufacture", "fieldtype": "Link",
"oldfieldname": "production_item", "in_global_search": 1,
"oldfieldtype": "Link", "in_list_view": 1,
"options": "Item", "in_standard_filter": 1,
"reqd": 1 "label": "Item To Manufacture",
}, "oldfieldname": "production_item",
{ "oldfieldtype": "Link",
"depends_on": "eval:doc.production_item", "options": "Item",
"fieldname": "item_name", "reqd": 1
"fieldtype": "Data", },
"label": "Item Name", {
"read_only": 1 "depends_on": "eval:doc.production_item",
}, "fieldname": "item_name",
{ "fieldtype": "Data",
"fetch_from": "production_item.image", "label": "Item Name",
"fieldname": "image", "read_only": 1
"fieldtype": "Attach Image", },
"hidden": 1, {
"label": "Image", "fetch_from": "production_item.image",
"options": "image", "fieldname": "image",
"print_hide": 1, "fieldtype": "Attach Image",
"read_only": 1 "hidden": 1,
}, "label": "Image",
{ "options": "image",
"fieldname": "bom_no", "print_hide": 1,
"fieldtype": "Link", "read_only": 1
"label": "BOM No", },
"oldfieldname": "bom_no", {
"oldfieldtype": "Link", "fieldname": "bom_no",
"options": "BOM", "fieldtype": "Link",
"reqd": 1 "label": "BOM No",
}, "oldfieldname": "bom_no",
{ "oldfieldtype": "Link",
"default": "0", "options": "BOM",
"fieldname": "allow_alternative_item", "reqd": 1
"fieldtype": "Check", },
"label": "Allow Alternative Item" {
}, "default": "0",
{ "fieldname": "allow_alternative_item",
"default": "1", "fieldtype": "Check",
"description": "Plan material for sub-assemblies", "label": "Allow Alternative Item"
"fieldname": "use_multi_level_bom", },
"fieldtype": "Check", {
"label": "Use Multi-Level BOM", "default": "1",
"print_hide": 1 "description": "Plan material for sub-assemblies",
}, "fieldname": "use_multi_level_bom",
{ "fieldtype": "Check",
"default": "0", "label": "Use Multi-Level BOM",
"description": "Check if material transfer entry is not required", "print_hide": 1
"fieldname": "skip_transfer", },
"fieldtype": "Check", {
"label": "Skip Material Transfer to WIP Warehouse" "default": "0",
}, "description": "Check if material transfer entry is not required",
{ "fieldname": "skip_transfer",
"fieldname": "column_break1", "fieldtype": "Check",
"fieldtype": "Column Break", "label": "Skip Material Transfer to WIP Warehouse"
"oldfieldtype": "Column Break", },
"width": "50%" {
}, "fieldname": "column_break1",
{ "fieldtype": "Column Break",
"fieldname": "company", "oldfieldtype": "Column Break",
"fieldtype": "Link", "width": "50%"
"label": "Company", },
"oldfieldname": "company", {
"oldfieldtype": "Link", "fieldname": "company",
"options": "Company", "fieldtype": "Link",
"remember_last_selected_value": 1, "label": "Company",
"reqd": 1 "oldfieldname": "company",
}, "oldfieldtype": "Link",
{ "options": "Company",
"fieldname": "qty", "remember_last_selected_value": 1,
"fieldtype": "Float", "reqd": 1
"label": "Qty To Manufacture", },
"oldfieldname": "qty", {
"oldfieldtype": "Currency", "fieldname": "qty",
"reqd": 1 "fieldtype": "Float",
}, "label": "Qty To Manufacture",
{ "oldfieldname": "qty",
"default": "0", "oldfieldtype": "Currency",
"depends_on": "eval:doc.docstatus==1 && doc.skip_transfer==0", "reqd": 1
"fieldname": "material_transferred_for_manufacturing", },
"fieldtype": "Float", {
"label": "Material Transferred for Manufacturing", "default": "0",
"no_copy": 1, "depends_on": "eval:doc.docstatus==1 && doc.skip_transfer==0",
"read_only": 1 "fieldname": "material_transferred_for_manufacturing",
}, "fieldtype": "Float",
{ "label": "Material Transferred for Manufacturing",
"default": "0", "no_copy": 1,
"depends_on": "eval:doc.docstatus==1", "read_only": 1
"fieldname": "produced_qty", },
"fieldtype": "Float", {
"label": "Manufactured Qty", "default": "0",
"no_copy": 1, "depends_on": "eval:doc.docstatus==1",
"oldfieldname": "produced_qty", "fieldname": "produced_qty",
"oldfieldtype": "Currency", "fieldtype": "Float",
"read_only": 1 "label": "Manufactured Qty",
}, "no_copy": 1,
{ "oldfieldname": "produced_qty",
"allow_on_submit": 1, "oldfieldtype": "Currency",
"fieldname": "sales_order", "read_only": 1
"fieldtype": "Link", },
"in_global_search": 1, {
"label": "Sales Order", "allow_on_submit": 1,
"options": "Sales Order" "fieldname": "sales_order",
}, "fieldtype": "Link",
{ "in_global_search": 1,
"fieldname": "project", "label": "Sales Order",
"fieldtype": "Link", "options": "Sales Order"
"label": "Project", },
"oldfieldname": "project", {
"oldfieldtype": "Link", "fieldname": "project",
"options": "Project" "fieldtype": "Link",
}, "label": "Project",
{ "oldfieldname": "project",
"default": "0", "oldfieldtype": "Link",
"depends_on": "skip_transfer", "options": "Project"
"fieldname": "from_wip_warehouse", },
"fieldtype": "Check", {
"label": "Backflush Raw Materials From Work-in-Progress Warehouse" "default": "0",
}, "depends_on": "skip_transfer",
{ "fieldname": "from_wip_warehouse",
"fieldname": "warehouses", "fieldtype": "Check",
"fieldtype": "Section Break", "label": "Backflush Raw Materials From Work-in-Progress Warehouse"
"label": "Warehouses", },
"options": "fa fa-building" {
}, "fieldname": "warehouses",
{ "fieldtype": "Section Break",
"fieldname": "wip_warehouse", "label": "Warehouses",
"fieldtype": "Link", "options": "fa fa-building"
"label": "Work-in-Progress Warehouse", },
"options": "Warehouse" {
}, "fieldname": "wip_warehouse",
{ "fieldtype": "Link",
"fieldname": "fg_warehouse", "label": "Work-in-Progress Warehouse",
"fieldtype": "Link", "options": "Warehouse"
"label": "Target Warehouse", },
"options": "Warehouse" {
}, "fieldname": "fg_warehouse",
{ "fieldtype": "Link",
"fieldname": "column_break_12", "label": "Target Warehouse",
"fieldtype": "Column Break" "options": "Warehouse"
}, },
{ {
"fieldname": "scrap_warehouse", "fieldname": "column_break_12",
"fieldtype": "Link", "fieldtype": "Column Break"
"label": "Scrap Warehouse", },
"options": "Warehouse" {
}, "fieldname": "scrap_warehouse",
{ "fieldtype": "Link",
"fieldname": "required_items_section", "label": "Scrap Warehouse",
"fieldtype": "Section Break", "options": "Warehouse"
"label": "Required Items" },
}, {
{ "fieldname": "required_items_section",
"fieldname": "required_items", "fieldtype": "Section Break",
"fieldtype": "Table", "label": "Required Items"
"label": "Required Items", },
"no_copy": 1, {
"options": "Work Order Item", "fieldname": "required_items",
"print_hide": 1 "fieldtype": "Table",
}, "label": "Required Items",
{ "no_copy": 1,
"fieldname": "time", "options": "Work Order Item",
"fieldtype": "Section Break", "print_hide": 1
"label": "Time", },
"options": "fa fa-time" {
}, "fieldname": "time",
{ "fieldtype": "Section Break",
"allow_on_submit": 1, "label": "Time",
"default": "now", "options": "fa fa-time"
"fieldname": "planned_start_date", },
"fieldtype": "Datetime", {
"label": "Planned Start Date", "allow_on_submit": 1,
"reqd": 1 "default": "now",
}, "fieldname": "planned_start_date",
{ "fieldtype": "Datetime",
"fieldname": "actual_start_date", "label": "Planned Start Date",
"fieldtype": "Datetime", "reqd": 1
"label": "Actual Start Date", },
"read_only": 1 {
}, "fieldname": "actual_start_date",
{ "fieldtype": "Datetime",
"fieldname": "column_break_13", "label": "Actual Start Date",
"fieldtype": "Column Break" "read_only": 1
}, },
{ {
"fieldname": "planned_end_date", "fieldname": "column_break_13",
"fieldtype": "Datetime", "fieldtype": "Column Break"
"label": "Planned End Date", },
"no_copy": 1, {
"read_only": 1 "fieldname": "planned_end_date",
}, "fieldtype": "Datetime",
{ "label": "Planned End Date",
"fieldname": "actual_end_date", "no_copy": 1,
"fieldtype": "Datetime", "read_only": 1
"label": "Actual End Date", },
"read_only": 1 {
}, "fieldname": "actual_end_date",
{ "fieldtype": "Datetime",
"allow_on_submit": 1, "label": "Actual End Date",
"fieldname": "expected_delivery_date", "read_only": 1
"fieldtype": "Date", },
"label": "Expected Delivery Date" {
}, "allow_on_submit": 1,
{ "fieldname": "expected_delivery_date",
"fieldname": "operations_section", "fieldtype": "Date",
"fieldtype": "Section Break", "label": "Expected Delivery Date"
"label": "Operations", },
"options": "fa fa-wrench" {
}, "fieldname": "operations_section",
{ "fieldtype": "Section Break",
"default": "Work Order", "label": "Operations",
"depends_on": "operations", "options": "fa fa-wrench"
"fieldname": "transfer_material_against", },
"fieldtype": "Select", {
"label": "Transfer Material Against", "default": "Work Order",
"options": "\nWork Order\nJob Card" "depends_on": "operations",
}, "fieldname": "transfer_material_against",
{ "fieldtype": "Select",
"fieldname": "operations", "label": "Transfer Material Against",
"fieldtype": "Table", "options": "\nWork Order\nJob Card"
"label": "Operations", },
"options": "Work Order Operation", {
"read_only": 1 "fieldname": "operations",
}, "fieldtype": "Table",
{ "label": "Operations",
"depends_on": "operations", "options": "Work Order Operation",
"fieldname": "section_break_22", "read_only": 1
"fieldtype": "Section Break", },
"label": "Operation Cost" {
}, "depends_on": "operations",
{ "fieldname": "section_break_22",
"fieldname": "planned_operating_cost", "fieldtype": "Section Break",
"fieldtype": "Currency", "label": "Operation Cost"
"label": "Planned Operating Cost", },
"options": "Company:company:default_currency", {
"read_only": 1 "fieldname": "planned_operating_cost",
}, "fieldtype": "Currency",
{ "label": "Planned Operating Cost",
"fieldname": "actual_operating_cost", "options": "Company:company:default_currency",
"fieldtype": "Currency", "read_only": 1
"label": "Actual Operating Cost", },
"no_copy": 1, {
"options": "Company:company:default_currency", "fieldname": "actual_operating_cost",
"read_only": 1 "fieldtype": "Currency",
}, "label": "Actual Operating Cost",
{ "no_copy": 1,
"fieldname": "additional_operating_cost", "options": "Company:company:default_currency",
"fieldtype": "Currency", "read_only": 1
"label": "Additional Operating Cost", },
"no_copy": 1, {
"options": "Company:company:default_currency" "fieldname": "additional_operating_cost",
}, "fieldtype": "Currency",
{ "label": "Additional Operating Cost",
"fieldname": "column_break_24", "no_copy": 1,
"fieldtype": "Column Break" "options": "Company:company:default_currency"
}, },
{ {
"fieldname": "total_operating_cost", "fieldname": "column_break_24",
"fieldtype": "Currency", "fieldtype": "Column Break"
"label": "Total Operating Cost", },
"no_copy": 1, {
"options": "Company:company:default_currency", "fieldname": "total_operating_cost",
"read_only": 1 "fieldtype": "Currency",
}, "label": "Total Operating Cost",
{ "no_copy": 1,
"collapsible": 1, "options": "Company:company:default_currency",
"fieldname": "more_info", "read_only": 1
"fieldtype": "Section Break", },
"label": "More Information", {
"options": "fa fa-file-text" "collapsible": 1,
}, "fieldname": "more_info",
{ "fieldtype": "Section Break",
"fieldname": "description", "label": "More Information",
"fieldtype": "Small Text", "options": "fa fa-file-text"
"label": "Item Description", },
"read_only": 1 {
}, "fieldname": "description",
{ "fieldtype": "Small Text",
"fieldname": "stock_uom", "label": "Item Description",
"fieldtype": "Link", "read_only": 1
"label": "Stock UOM", },
"oldfieldname": "stock_uom", {
"oldfieldtype": "Data", "fieldname": "stock_uom",
"options": "UOM", "fieldtype": "Link",
"read_only": 1 "label": "Stock UOM",
}, "oldfieldname": "stock_uom",
{ "oldfieldtype": "Data",
"fieldname": "column_break2", "options": "UOM",
"fieldtype": "Column Break", "read_only": 1
"width": "50%" },
}, {
{ "fieldname": "column_break2",
"description": "Manufacture against Material Request", "fieldtype": "Column Break",
"fieldname": "material_request", "width": "50%"
"fieldtype": "Link", },
"label": "Material Request", {
"options": "Material Request" "description": "Manufacture against Material Request",
}, "fieldname": "material_request",
{ "fieldtype": "Link",
"fieldname": "material_request_item", "label": "Material Request",
"fieldtype": "Data", "options": "Material Request"
"hidden": 1, },
"label": "Material Request Item", {
"read_only": 1 "fieldname": "material_request_item",
}, "fieldtype": "Data",
{ "hidden": 1,
"fieldname": "sales_order_item", "label": "Material Request Item",
"fieldtype": "Data", "read_only": 1
"hidden": 1, },
"label": "Sales Order Item", {
"read_only": 1 "fieldname": "sales_order_item",
}, "fieldtype": "Data",
{ "hidden": 1,
"fieldname": "production_plan", "label": "Sales Order Item",
"fieldtype": "Link", "read_only": 1
"label": "Production Plan", },
"no_copy": 1, {
"options": "Production Plan", "fieldname": "production_plan",
"print_hide": 1, "fieldtype": "Link",
"read_only": 1 "label": "Production Plan",
}, "no_copy": 1,
{ "options": "Production Plan",
"fieldname": "production_plan_item", "print_hide": 1,
"fieldtype": "Data", "read_only": 1
"label": "Production Plan Item", },
"no_copy": 1, {
"print_hide": 1, "fieldname": "production_plan_item",
"read_only": 1 "fieldtype": "Data",
}, "label": "Production Plan Item",
{ "no_copy": 1,
"fieldname": "product_bundle_item", "print_hide": 1,
"fieldtype": "Link", "read_only": 1
"label": "Product Bundle Item", },
"no_copy": 1, {
"options": "Item", "fieldname": "product_bundle_item",
"print_hide": 1, "fieldtype": "Link",
"read_only": 1 "label": "Product Bundle Item",
}, "no_copy": 1,
{ "options": "Item",
"fieldname": "amended_from", "print_hide": 1,
"fieldtype": "Link", "read_only": 1
"ignore_user_permissions": 1, },
"label": "Amended From", {
"no_copy": 1, "fieldname": "amended_from",
"oldfieldname": "amended_from", "fieldtype": "Link",
"oldfieldtype": "Data", "ignore_user_permissions": 1,
"options": "Work Order", "label": "Amended From",
"read_only": 1 "no_copy": 1,
} "oldfieldname": "amended_from",
], "oldfieldtype": "Data",
"icon": "fa fa-cogs", "options": "Work Order",
"idx": 1, "read_only": 1
"image_field": "image", },
"is_submittable": 1, {
"modified": "2019-05-27 09:36:16.707719", "fieldname": "settings_section",
"modified_by": "Administrator", "fieldtype": "Section Break",
"module": "Manufacturing", "label": "Settings"
"name": "Work Order", },
"owner": "Administrator", {
"permissions": [ "fieldname": "column_break_18",
{ "fieldtype": "Column Break"
"amend": 1, },
"cancel": 1, {
"create": 1, "default": "1",
"delete": 1, "fieldname": "update_consumed_material_cost_in_project",
"email": 1, "fieldtype": "Check",
"export": 1, "label": "Update Consumed Material Cost In Project"
"import": 1, }
"print": 1, ],
"read": 1, "icon": "fa fa-cogs",
"report": 1, "idx": 1,
"role": "Manufacturing User", "image_field": "image",
"set_user_permissions": 1, "is_submittable": 1,
"share": 1, "modified": "2019-07-31 00:13:38.218277",
"submit": 1, "modified_by": "Administrator",
"write": 1 "module": "Manufacturing",
}, "name": "Work Order",
{ "owner": "Administrator",
"read": 1, "permissions": [
"report": 1, {
"role": "Stock User" "amend": 1,
} "cancel": 1,
], "create": 1,
"sort_order": "ASC", "delete": 1,
"title_field": "production_item", "email": 1,
"track_changes": 1, "export": 1,
"track_seen": 1 "import": 1,
} "print": 1,
"read": 1,
"report": 1,
"role": "Manufacturing User",
"set_user_permissions": 1,
"share": 1,
"submit": 1,
"write": 1
},
{
"read": 1,
"report": 1,
"role": "Stock User"
}
],
"sort_field": "modified",
"sort_order": "ASC",
"title_field": "production_item",
"track_changes": 1,
"track_seen": 1
}

View File

@ -0,0 +1,213 @@
frappe.pages['bom-comparison-tool'].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: __('BOM Comparison Tool'),
single_column: true
});
new erpnext.BOMComparisonTool(page);
}
erpnext.BOMComparisonTool = class BOMComparisonTool {
constructor(page) {
this.page = page;
this.make_form();
}
make_form() {
this.form = new frappe.ui.FieldGroup({
fields: [
{
label: __('BOM 1'),
fieldname: 'name1',
fieldtype: 'Link',
options: 'BOM',
change: () => this.fetch_and_render()
},
{
fieldtype: 'Column Break'
},
{
label: __('BOM 2'),
fieldname: 'name2',
fieldtype: 'Link',
options: 'BOM',
change: () => this.fetch_and_render()
},
{
fieldtype: 'Section Break'
},
{
fieldtype: 'HTML',
fieldname: 'preview'
}
],
body: this.page.body
});
this.form.make();
}
fetch_and_render() {
let { name1, name2 } = this.form.get_values();
if (!(name1 && name2)) {
this.form.get_field('preview').html('');
return;
}
// set working state
this.form.get_field('preview').html(`
<div class="text-muted margin-top">
${__("Fetching...")}
</div>
`);
frappe.call('erpnext.manufacturing.doctype.bom.bom.get_bom_diff', {
bom1: name1,
bom2: name2
}).then(r => {
let diff = r.message;
frappe.model.with_doctype('BOM', () => {
this.render('BOM', name1, name2, diff);
});
});
}
render(doctype, name1, name2, diff) {
let change_html = (title, doctype, changed) => {
let values_changed = this.get_changed_values(doctype, changed)
.map(change => {
let [fieldname, value1, value2] = change;
return `
<tr>
<td>${frappe.meta.get_label(doctype, fieldname)}</td>
<td>${value1}</td>
<td>${value2}</td>
</tr>
`;
})
.join('');
return `
<h4 class="margin-top">${title}</h4>
<div>
<table class="table table-bordered">
<tr>
<th width="33%">${__('Field')}</th>
<th width="33%">${name1}</th>
<th width="33%">${name2}</th>
</tr>
${values_changed}
</table>
</div>
`;
}
let value_changes = change_html(__('Values Changed'), doctype, diff.changed);
let row_changes_by_fieldname = group_items(diff.row_changed, change => change[0]);
let table_changes = Object.keys(row_changes_by_fieldname).map(fieldname => {
let changes = row_changes_by_fieldname[fieldname];
let df = frappe.meta.get_docfield(doctype, fieldname);
let html = changes.map(change => {
let [fieldname,, item_code, changes] = change;
let df = frappe.meta.get_docfield(doctype, fieldname);
let child_doctype = df.options;
let values_changed = this.get_changed_values(child_doctype, changes);
return values_changed.map((change, i) => {
let [fieldname, value1, value2] = change;
let th = i === 0
? `<th rowspan="${values_changed.length}">${item_code}</th>`
: '';
return `
<tr>
${th}
<td>${frappe.meta.get_label(child_doctype, fieldname)}</td>
<td>${value1}</td>
<td>${value2}</td>
</tr>
`;
}).join('');
}).join('');
return `
<h4 class="margin-top">${__('Changes in {0}', [df.label])}</h4>
<table class="table table-bordered">
<tr>
<th width="25%">${__('Item Code')}</th>
<th width="25%">${__('Field')}</th>
<th width="25%">${name1}</th>
<th width="25%">${name2}</th>
</tr>
${html}
</table>
`;
}).join('');
let get_added_removed_html = (title, grouped_items) => {
return Object.keys(grouped_items).map(fieldname => {
let rows = grouped_items[fieldname];
let df = frappe.meta.get_docfield(doctype, fieldname);
let fields = frappe.meta.get_docfields(df.options)
.filter(df => df.in_list_view);
let html = rows.map(row => {
let [, doc] = row;
let cells = fields
.map(df => `<td>${doc[df.fieldname]}</td>`)
.join('');
return `<tr>${cells}</tr>`;
}).join('');
let header = fields.map(df => `<th>${df.label}</th>`).join('');
return `
<h4 class="margin-top">${$.format(title, [df.label])}</h4>
<table class="table table-bordered">
<tr>${header}</tr>
${html}
</table>
`;
}).join('');
};
let added_by_fieldname = group_items(diff.added, change => change[0]);
let removed_by_fieldname = group_items(diff.removed, change => change[0]);
let added_html = get_added_removed_html(__('Rows Added in {0}'), added_by_fieldname);
let removed_html = get_added_removed_html(__('Rows Removed in {0}'), removed_by_fieldname);
let html = `
${value_changes}
${table_changes}
${added_html}
${removed_html}
`;
this.form.get_field('preview').html(html);
}
get_changed_values(doctype, changed) {
return changed.filter(change => {
let [fieldname, value1, value2] = change;
if (!value1) value1 = '';
if (!value2) value2 = '';
if (value1 === value2) return false;
let df = frappe.meta.get_docfield(doctype, fieldname);
if (!df) return false;
if (df.hidden) return false;
return true;
});
}
};
function group_items(array, fn) {
return array.reduce((acc, item) => {
let key = fn(item);
acc[key] = acc[key] || [];
acc[key].push(item);
return acc;
}, {});
}

View File

@ -0,0 +1,30 @@
{
"content": null,
"creation": "2019-07-29 13:24:38.201981",
"docstatus": 0,
"doctype": "Page",
"idx": 0,
"modified": "2019-07-29 13:24:38.201981",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "bom-comparison-tool",
"owner": "Administrator",
"page_name": "BOM Comparison Tool",
"restrict_to_domain": "Manufacturing",
"roles": [
{
"role": "System Manager"
},
{
"role": "Manufacturing User"
},
{
"role": "Manufacturing Manager"
}
],
"script": null,
"standard": "Yes",
"style": null,
"system_page": 0,
"title": "BOM Comparison Tool"
}

View File

@ -3,7 +3,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import getdate from frappe.utils import getdate, today
def execute(): def execute():
""" Generates leave ledger entries for leave allocation/application/encashment """ Generates leave ledger entries for leave allocation/application/encashment
@ -66,7 +66,8 @@ def generate_expiry_allocation_ledger_entries():
if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name, 'is_expired': 1}): if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name, 'is_expired': 1}):
allocation.update(dict(doctype="Leave Allocation")) allocation.update(dict(doctype="Leave Allocation"))
allocation_obj = frappe.get_doc(allocation) allocation_obj = frappe.get_doc(allocation)
expire_allocation(allocation_obj) if allocation_obj.to_date <= getdate(today()):
expire_allocation(allocation_obj)
def get_allocation_records(): def get_allocation_records():
return frappe.get_all("Leave Allocation", filters={ return frappe.get_all("Leave Allocation", filters={

View File

@ -60,7 +60,15 @@ $.extend(erpnext, {
var me = this; var me = this;
$btn.on("click", function() { $btn.on("click", function() {
me.show_serial_batch_selector(grid_row.frm, grid_row.doc); let callback = '';
let on_close = '';
if (grid_row.doc.serial_no) {
grid_row.doc.has_serial_no = true;
}
me.show_serial_batch_selector(grid_row.frm, grid_row.doc,
callback, on_close, true);
}); });
}, },
}); });

View File

@ -68,6 +68,28 @@ erpnext.child_docs.forEach((doctype) => {
}); });
}, },
accounts_add: function(frm, cdt, cdn) {
erpnext.dimension_filters.forEach((dimension) => {
var row = frappe.get_doc(cdt, cdn);
frm.script_manager.copy_from_first_row("accounts", row, [dimension['fieldname']]);
});
},
company: function(frm) {
if(frm.doc.company) {
erpnext.dimension_filters.forEach((dimension) => {
frm.set_value(dimension['fieldname'], erpnext.default_dimensions[frm.doc.company][dimension['document_type']]);
});
}
},
items_add: function(frm, cdt, cdn) {
erpnext.dimension_filters.forEach((dimension) => {
var row = frappe.get_doc(cdt, cdn);
frm.script_manager.copy_from_first_row("items", row, [dimension['fieldname']]);
});
},
accounts_add: function(frm, cdt, cdn) { accounts_add: function(frm, cdt, cdn) {
erpnext.dimension_filters.forEach((dimension) => { erpnext.dimension_filters.forEach((dimension) => {
var row = frappe.get_doc(cdt, cdn); var row = frappe.get_doc(cdt, cdn);

View File

@ -156,33 +156,21 @@ class Gstr1Report(object):
if self.filters.get("type_of_business") == "B2B": if self.filters.get("type_of_business") == "B2B":
customers = frappe.get_all("Customer", conditions += "and ifnull(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') and is_return != 1"
filters={
"gst_category": ["in", ["Registered Regular", "Deemed Export", "SEZ"]]
})
if customers:
conditions += """ and ifnull(gst_category, '') != 'Overseas' and is_return != 1
and customer in ({0})""".format(", ".join([frappe.db.escape(c.name) for c in customers]))
if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"): if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"):
b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit') b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit')
if not b2c_limit: if not b2c_limit:
frappe.throw(_("Please set B2C Limit in GST Settings.")) frappe.throw(_("Please set B2C Limit in GST Settings."))
customers = frappe.get_all("Customer",
filters={
"gst_category": ["in", ["Unregistered"]]
})
if self.filters.get("type_of_business") == "B2C Large" and customers: if self.filters.get("type_of_business") == "B2C Large" and customers:
conditions += """ and SUBSTR(place_of_supply, 1, 2) != SUBSTR(company_gstin, 1, 2) conditions += """ and SUBSTR(place_of_supply, 1, 2) != SUBSTR(company_gstin, 1, 2)
and grand_total > {0} and is_return != 1 and customer in ({1})""".\ and grand_total > {0} and is_return != 1 and gst_category ='Unregistered' """.\
format(flt(b2c_limit), ", ".join([frappe.db.escape(c.name) for c in customers])) format(flt(b2c_limit), ", ".join([frappe.db.escape(c.name) for c in customers]))
elif self.filters.get("type_of_business") == "B2C Small" and customers: elif self.filters.get("type_of_business") == "B2C Small" and customers:
conditions += """ and ( conditions += """ and (
SUBSTR(place_of_supply, 1, 2) = SUBSTR(company_gstin, 1, 2) SUBSTR(place_of_supply, 1, 2) = SUBSTR(company_gstin, 1, 2)
or grand_total <= {0}) and is_return != 1 and customer in ({1})""".\ or grand_total <= {0}) and is_return != 1 and gst_category ='Unregistered' """.\
format(flt(b2c_limit), ", ".join([frappe.db.escape(c.name) for c in customers])) format(flt(b2c_limit), ", ".join([frappe.db.escape(c.name) for c in customers]))
elif self.filters.get("type_of_business") == "CDNR": elif self.filters.get("type_of_business") == "CDNR":

View File

@ -34,7 +34,7 @@ class Quotation(SellingController):
self.with_items = 1 self.with_items = 1
def validate_valid_till(self): def validate_valid_till(self):
if self.valid_till and self.valid_till < self.transaction_date: if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date):
frappe.throw(_("Valid till date cannot be before transaction date")) frappe.throw(_("Valid till date cannot be before transaction date"))
def has_sales_order(self): def has_sales_order(self):

View File

@ -72,9 +72,7 @@ class SalesOrder(SellingController):
frappe.msgprint(_("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format(so[0][0], self.po_no)) frappe.msgprint(_("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format(so[0][0], self.po_no))
def validate_for_items(self): def validate_for_items(self):
check_list = []
for d in self.get('items'): for d in self.get('items'):
check_list.append(cstr(d.item_code))
# used for production plan # used for production plan
d.transaction_date = self.transaction_date d.transaction_date = self.transaction_date
@ -83,13 +81,6 @@ class SalesOrder(SellingController):
where item_code = %s and warehouse = %s", (d.item_code, d.warehouse)) where item_code = %s and warehouse = %s", (d.item_code, d.warehouse))
d.projected_qty = tot_avail_qty and flt(tot_avail_qty[0][0]) or 0 d.projected_qty = tot_avail_qty and flt(tot_avail_qty[0][0]) or 0
# check for same entry multiple times
unique_chk_list = set(check_list)
if len(unique_chk_list) != len(check_list) and \
not cint(frappe.db.get_single_value("Selling Settings", "allow_multiple_items")):
frappe.msgprint(_("Same item has been entered multiple times"),
title=_("Warning"), indicator='orange')
def product_bundle_has_stock_item(self, product_bundle): def product_bundle_has_stock_item(self, product_bundle):
"""Returns true if product bundle has stock item""" """Returns true if product bundle has stock item"""
ret = len(frappe.db.sql("""select i.name from tabItem i, `tabProduct Bundle Item` pbi ret = len(frappe.db.sql("""select i.name from tabItem i, `tabProduct Bundle Item` pbi

View File

@ -252,7 +252,7 @@ class Company(NestedSet):
def set_mode_of_payment_account(self): def set_mode_of_payment_account(self):
cash = frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name') cash = frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name')
if cash and self.default_cash_account \ if cash and self.default_cash_account \
and not frappe.db.get_value('Mode of Payment Account', {'company': self.name, 'parent': cash}): and not frappe.db.get_value('Mode of Payment Account', {'company': self.name, 'parent': cash}):
mode_of_payment = frappe.get_doc('Mode of Payment', cash) mode_of_payment = frappe.get_doc('Mode of Payment', cash)
mode_of_payment.append('accounts', { mode_of_payment.append('accounts', {
'company': self.name, 'company': self.name,

View File

@ -166,24 +166,7 @@ class DeliveryNote(SellingController):
frappe.throw(_("Customer {0} does not belong to project {1}").format(self.customer, self.project)) frappe.throw(_("Customer {0} does not belong to project {1}").format(self.customer, self.project))
def validate_for_items(self): def validate_for_items(self):
check_list, chk_dupl_itm = [], []
if cint(frappe.db.get_single_value("Selling Settings", "allow_multiple_items")):
return
for d in self.get('items'): for d in self.get('items'):
e = [d.item_code, d.description, d.warehouse, d.against_sales_order or d.against_sales_invoice, d.batch_no or '']
f = [d.item_code, d.description, d.against_sales_order or d.against_sales_invoice]
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1:
if e in check_list:
frappe.msgprint(_("Note: Item {0} entered multiple times").format(d.item_code))
else:
check_list.append(e)
else:
if f in chk_dupl_itm:
frappe.msgprint(_("Note: Item {0} entered multiple times").format(d.item_code))
else:
chk_dupl_itm.append(f)
#Customer Provided parts will have zero valuation rate #Customer Provided parts will have zero valuation rate
if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'): if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'):
d.allow_zero_valuation_rate = 1 d.allow_zero_valuation_rate = 1

View File

@ -145,6 +145,10 @@ class StockEntry(StockController):
self.precision("transfer_qty", item)) self.precision("transfer_qty", item))
def update_cost_in_project(self): def update_cost_in_project(self):
if (self.work_order and not frappe.db.get_value("Work Order",
self.work_order, "update_consumed_material_cost_in_project")):
return
if self.project: if self.project:
amount = frappe.db.sql(""" select ifnull(sum(sed.amount), 0) amount = frappe.db.sql(""" select ifnull(sum(sed.amount), 0)
from from

View File

@ -32,7 +32,7 @@ frappe.ready(function() {
if(r.message.product_info.in_stock===0) { if(r.message.product_info.in_stock===0) {
$(".item-stock").html("<div style='color: red'> <i class='fa fa-close'></i> {{ _("Not in stock") }}</div>"); $(".item-stock").html("<div style='color: red'> <i class='fa fa-close'></i> {{ _("Not in stock") }}</div>");
} }
else if(r.message.product_info.in_stock===1) { else if(r.message.product_info.in_stock===1 && r.message.cart_settings.show_stock_availability) {
var qty_display = "{{ _("In stock") }}"; var qty_display = "{{ _("In stock") }}";
if (r.message.product_info.show_stock_qty) { if (r.message.product_info.show_stock_qty) {
qty_display += " ("+r.message.product_info.stock_qty+")"; qty_display += " ("+r.message.product_info.stock_qty+")";

View File

@ -5,8 +5,7 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import formatdate from frappe.utils import formatdate
from erpnext.controllers.website_list_for_contact import (get_customers_suppliers, from erpnext.controllers.website_list_for_contact import get_customers_suppliers
get_party_details)
def get_context(context): def get_context(context):
context.no_cache = 1 context.no_cache = 1
@ -23,8 +22,8 @@ def get_supplier():
doctype = frappe.form_dict.doctype doctype = frappe.form_dict.doctype
parties_doctype = 'Request for Quotation Supplier' if doctype == 'Request for Quotation' else doctype parties_doctype = 'Request for Quotation Supplier' if doctype == 'Request for Quotation' else doctype
customers, suppliers = get_customers_suppliers(parties_doctype, frappe.session.user) customers, suppliers = get_customers_suppliers(parties_doctype, frappe.session.user)
key, parties = get_party_details(customers, suppliers)
return parties[0] if key == 'supplier' else '' return suppliers[0] if suppliers else ''
def check_supplier_has_docname_access(supplier): def check_supplier_has_docname_access(supplier):
status = True status = True