diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index 971d308368..29d83783d0 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -389,8 +389,7 @@ "fieldname": "rate_or_discount", "fieldtype": "Select", "label": "Rate or Discount", - "options": "\nRate\nDiscount Percentage\nDiscount Amount", - "reqd": 1 + "options": "\nRate\nDiscount Percentage\nDiscount Amount" }, { "default": "Grand Total", @@ -439,19 +438,20 @@ }, { "default": "0", - "depends_on": "eval:!doc.mixed_conditions", + "depends_on": "eval:!doc.mixed_conditions && doc.apply_on != 'Transaction'", "fieldname": "same_item", "fieldtype": "Check", "label": "Same Item" }, { - "depends_on": "eval:!doc.same_item || doc.mixed_conditions", + "depends_on": "eval:(!doc.same_item || doc.apply_on == 'Transaction') || doc.mixed_conditions", "fieldname": "free_item", "fieldtype": "Link", "label": "Free Item", "options": "Item" }, { + "default": "0", "fieldname": "free_qty", "fieldtype": "Float", "label": "Qty" @@ -554,7 +554,7 @@ ], "icon": "fa fa-gift", "idx": 1, - "modified": "2019-10-15 12:39:40.399792", + "modified": "2019-12-18 17:29:22.957077", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index e871d98af6..3c14819e6f 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -47,6 +47,9 @@ class PricingRule(Document): if tocheck and not self.get(tocheck): throw(_("{0} is required").format(self.meta.get_label(tocheck)), frappe.MandatoryError) + if self.price_or_product_discount == 'Price' and not self.rate_or_discount: + throw(_("Rate or Discount is required for the price discount."), frappe.MandatoryError) + def validate_applicable_for_selling_or_buying(self): if not self.selling and not self.buying: throw(_("Atleast one of the Selling or Buying must be selected")) @@ -182,7 +185,7 @@ def get_serial_no_for_item(args): def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False): from erpnext.accounts.doctype.pricing_rule.utils import (get_pricing_rules, - get_applied_pricing_rules, get_pricing_rule_items) + get_applied_pricing_rules, get_pricing_rule_items, get_product_discount_rule) if isinstance(doc, string_types): doc = json.loads(doc) @@ -241,9 +244,11 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa if pricing_rule.coupon_code_based==1 and args.coupon_code==None: return item_details - if (not pricing_rule.validate_applied_rule and - pricing_rule.price_or_product_discount == "Price"): - apply_price_discount_pricing_rule(pricing_rule, item_details, args) + if not pricing_rule.validate_applied_rule: + if pricing_rule.price_or_product_discount == "Price": + apply_price_discount_rule(pricing_rule, item_details, args) + else: + get_product_discount_rule(pricing_rule, item_details, doc) item_details.has_pricing_rule = 1 @@ -293,7 +298,7 @@ def get_pricing_rule_details(args, pricing_rule): 'child_docname': args.get('child_docname') }) -def apply_price_discount_pricing_rule(pricing_rule, item_details, args): +def apply_price_discount_rule(pricing_rule, item_details, args): item_details.pricing_rule_for = pricing_rule.rate_or_discount if ((pricing_rule.margin_type == 'Amount' and pricing_rule.currency == args.currency) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 637e503e65..87f68225fe 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -7,7 +7,7 @@ from __future__ import unicode_literals import frappe, copy, json from frappe import throw, _ from six import string_types -from frappe.utils import flt, cint, get_datetime +from frappe.utils import flt, cint, get_datetime, get_link_to_form, today from erpnext.setup.doctype.item_group.item_group import get_child_item_groups from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses from erpnext.stock.get_item_details import get_conversion_factor @@ -284,7 +284,7 @@ def filter_pricing_rules_for_qty_amount(qty, rate, pricing_rules, args=None): status = True # if user has created item price against the transaction UOM - if rule.get("uom") == args.get("uom"): + if args and rule.get("uom") == args.get("uom"): conversion_factor = 1.0 if status and (flt(rate) >= (flt(rule.min_amt) * conversion_factor) @@ -408,7 +408,8 @@ def apply_pricing_rule_on_transaction(doc): conditions = get_other_conditions(conditions, values, doc) pricing_rules = frappe.db.sql(""" Select `tabPricing Rule`.* from `tabPricing Rule` - where {conditions} """.format(conditions = conditions), values, as_dict=1) + where {conditions} and `tabPricing Rule`.disable = 0 + """.format(conditions = conditions), values, as_dict=1) if pricing_rules: pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty, @@ -420,39 +421,65 @@ def apply_pricing_rule_on_transaction(doc): doc.set('apply_discount_on', d.apply_discount_on) for field in ['additional_discount_percentage', 'discount_amount']: - if not d.get(field): continue - pr_field = ('discount_percentage' if field == 'additional_discount_percentage' else field) + if not d.get(pr_field): continue + if d.validate_applied_rule and doc.get(field) < d.get(pr_field): frappe.msgprint(_("User has not applied rule on the invoice {0}") .format(doc.name)) else: doc.set(field, d.get(pr_field)) + + doc.calculate_taxes_and_totals() elif d.price_or_product_discount == 'Product': - apply_pricing_rule_for_free_items(doc, d) + item_details = frappe._dict({'parenttype': doc.doctype}) + get_product_discount_rule(d, item_details, doc) + apply_pricing_rule_for_free_items(doc, item_details.free_item_data) + doc.set_missing_values() def get_applied_pricing_rules(item_row): return (item_row.get("pricing_rules").split(',') if item_row.get("pricing_rules") else []) -def apply_pricing_rule_for_free_items(doc, pricing_rule): - if pricing_rule.get('free_item'): +def get_product_discount_rule(pricing_rule, item_details, doc=None): + free_item = (pricing_rule.free_item + if not pricing_rule.same_item or pricing_rule.apply_on == 'Transaction' else item_details.item_code) + + if not free_item: + frappe.throw(_("Free item not set in the pricing rule {0}") + .format(get_link_to_form("Pricing Rule", pricing_rule.name))) + + item_details.free_item_data = { + 'item_code': free_item, + 'qty': pricing_rule.free_qty or 1, + 'rate': pricing_rule.free_item_rate or 0, + 'price_list_rate': pricing_rule.free_item_rate or 0, + 'is_free_item': 1 + } + + item_data = frappe.get_cached_value('Item', free_item, ['item_name', + 'description', 'stock_uom'], as_dict=1) + + item_details.free_item_data.update(item_data) + item_details.free_item_data['uom'] = pricing_rule.free_item_uom or item_data.stock_uom + item_details.free_item_data['conversion_factor'] = get_conversion_factor(free_item, + item_details.free_item_data['uom']).get("conversion_factor", 1) + + if item_details.get("parenttype") == 'Purchase Order': + item_details.free_item_data['schedule_date'] = doc.schedule_date if doc else today() + + if item_details.get("parenttype") == 'Sales Order': + item_details.free_item_data['delivery_date'] = doc.delivery_date if doc else today() + +def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False): + if pricing_rule_args.get('item_code'): items = [d.item_code for d in doc.items - if d.item_code == (d.item_code - if pricing_rule.get('same_item') else pricing_rule.get('free_item')) and d.is_free_item] + if d.item_code == (pricing_rule_args.get("item_code")) and d.is_free_item] if not items: - doc.append('items', { - 'item_code': pricing_rule.get('free_item'), - 'qty': pricing_rule.get('free_qty'), - 'uom': pricing_rule.get('free_item_uom'), - 'rate': pricing_rule.get('free_item_rate') or 0, - 'is_free_item': 1 - }) - - doc.set_missing_values() + doc.append('items', pricing_rule_args) def get_pricing_rule_items(pr_doc): apply_on_data = [] diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index d7e64cf36f..643de7d300 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -382,21 +382,11 @@ cur_frm.fields_dict['items'].grid.get_field("item_code").get_query = function(do cur_frm.fields_dict['credit_to'].get_query = function(doc) { // filter on Account - if (doc.supplier) { - return { - filters: { - 'account_type': 'Payable', - 'is_group': 0, - 'company': doc.company - } - } - } else { - return { - filters: { - 'report_type': 'Balance Sheet', - 'is_group': 0, - 'company': doc.company - } + return { + filters: { + 'account_type': 'Payable', + 'is_group': 0, + 'company': doc.company } } } diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 7f4ae3c1fc..db6ac55a11 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -556,22 +556,11 @@ cur_frm.cscript.cost_center = function(doc, cdt, cdn) { } cur_frm.set_query("debit_to", function(doc) { - // filter on Account - if (doc.customer) { - return { - filters: { - 'account_type': 'Receivable', - 'is_group': 0, - 'company': doc.company - } - } - } else { - return { - filters: { - 'report_type': 'Balance Sheet', - 'is_group': 0, - 'company': doc.company - } + return { + filters: { + 'account_type': 'Receivable', + 'is_group': 0, + 'company': doc.company } } }); diff --git a/erpnext/accounts/doctype/share_transfer/share_transfer.json b/erpnext/accounts/doctype/share_transfer/share_transfer.json index f17bf04caf..59a305317d 100644 --- a/erpnext/accounts/doctype/share_transfer/share_transfer.json +++ b/erpnext/accounts/doctype/share_transfer/share_transfer.json @@ -188,7 +188,7 @@ } ], "is_submittable": 1, - "modified": "2019-11-07 13:31:17.999744", + "modified": "2019-12-20 14:48:01.990600", "modified_by": "Administrator", "module": "Accounts", "name": "Share Transfer", @@ -196,6 +196,7 @@ "permissions": [ { "amend": 1, + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -221,6 +222,7 @@ "write": 1 }, { + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -230,6 +232,7 @@ "report": 1, "role": "Accounts Manager", "share": 1, + "submit": 1, "write": 1 } ], diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index feb598a2e5..bb1b7e392d 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -90,8 +90,12 @@ def merge_similar_entries(gl_map): else: merged_gl_map.append(entry) + company = gl_map[0].company if gl_map else erpnext.get_default_company() + company_currency = erpnext.get_company_currency(company) + precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency) + # filter zero debit and credit entries - merged_gl_map = filter(lambda x: flt(x.debit, 9)!=0 or flt(x.credit, 9)!=0, merged_gl_map) + merged_gl_map = filter(lambda x: flt(x.debit, precision)!=0 or flt(x.credit, precision)!=0, merged_gl_map) merged_gl_map = list(merged_gl_map) return merged_gl_map diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 2c53f6e997..c70a2cd1a7 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -171,7 +171,7 @@ class ReceivablePayableReport(object): row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision) row.invoice_grand_total = row.invoiced - if abs(row.outstanding) > 0.1/10 ** self.currency_precision: + if abs(row.outstanding) > 1.0/10 ** self.currency_precision: # non-zero oustanding, we must consider this row if self.is_invoice(row) and self.filters.based_on_payment_terms: diff --git a/erpnext/accounts/report/financial_statements.html b/erpnext/accounts/report/financial_statements.html index 4081723bf0..50947ecf5e 100644 --- a/erpnext/accounts/report/financial_statements.html +++ b/erpnext/accounts/report/financial_statements.html @@ -1,5 +1,6 @@ {% var report_columns = report.get_columns_for_print(); + report_columns = report_columns.filter(col => !col.hidden); if (report_columns.length > 8) { frappe.throw(__("Too many columns. Export the report and print it using a spreadsheet application.")); @@ -15,34 +16,35 @@ height: 37px; } -{% var letterhead= filters.letter_head || (frappe.get_doc(":Company", filters.company) && frappe.get_doc(":Company", filters.company).default_letter_head) %} -{% if(letterhead) { %} -
- {%= frappe.boot.letter_heads[letterhead].header %} -
-{% } %} +

{%= __(report.report_name) %}

{%= filters.company %}

+ {% if 'cost_center' in filters %}

{%= filters.cost_center %}

{% endif %} +

{%= filters.fiscal_year %}

-
{%= __("Currency") %} : {%= filters.presentation_currency || erpnext.get_currency(filters.company) %}
+
+ {%= __("Currency") %} : {%= filters.presentation_currency || erpnext.get_currency(filters.company) %} +
{% if (filters.from_date) { %} -

{%= frappe.datetime.str_to_user(filters.from_date) %} - {%= frappe.datetime.str_to_user(filters.to_date) %}

+
+ {%= frappe.datetime.str_to_user(filters.from_date) %} - {%= frappe.datetime.str_to_user(filters.to_date) %} +
{% } %}
- - {% for(var i=2, l=report_columns.length; i + {% for (let i=1, l=report_columns.length; i{%= report_columns[i].label %} {% } %} - {% for(var j=0, k=data.length-1; j {%= row.account_name %} - {% for(var i=2, l=report_columns.length; i - {% var fieldname = report_columns[i].fieldname; %} + {% const fieldname = report_columns[i].fieldname; %} {% if (!is_null(row[fieldname])) { %} - {%= format_currency(row[fieldname], filters.presentation_currency) %} + {%= frappe.format(row[fieldname], report_columns[i], {}, row) %} {% } %} {% } %} @@ -64,4 +66,6 @@ {% } %}
-

Printed On {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}

+

+ Printed On {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %} +

diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 3c8de6026a..40d5682726 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -264,8 +264,8 @@ def filter_out_zero_value_rows(data, parent_children_map, show_zero_values=False def add_total_row(out, root_type, balance_must_be, period_list, company_currency): total_row = { - "account_name": "'" + _("Total {0} ({1})").format(_(root_type), _(balance_must_be)) + "'", - "account": "'" + _("Total {0} ({1})").format(_(root_type), _(balance_must_be)) + "'", + "account_name": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)), + "account": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)), "currency": company_currency } diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index 0e2821ac16..afdd31df16 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -38,32 +38,46 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No cost_center = list(set(invoice_cc_wh_map.get(inv.name, {}).get("cost_center", []))) warehouse = list(set(invoice_cc_wh_map.get(inv.name, {}).get("warehouse", []))) - row = [ - inv.name, inv.posting_date, inv.customer, inv.customer_name - ] + row = { + 'invoice': inv.name, + 'posting_date': inv.posting_date, + 'customer': inv.customer, + 'customer_name': inv.customer_name + } if additional_query_columns: for col in additional_query_columns: - row.append(inv.get(col)) + row.update({ + col: inv.get(col) + }) + + row.update({ + 'customer_group': inv.get("customer_group"), + 'territory': inv.get("territory"), + 'tax_id': inv.get("tax_id"), + 'receivable_account': inv.debit_to, + 'mode_of_payment': ", ".join(mode_of_payments.get(inv.name, [])), + 'project': inv.project, + 'owner': inv.owner, + 'remarks': inv.remarks, + 'sales_order': ", ".join(sales_order), + 'delivery_note': ", ".join(delivery_note), + 'cost_center': ", ".join(cost_center), + 'warehouse': ", ".join(warehouse), + 'currency': company_currency + }) - row +=[ - inv.get("customer_group"), - inv.get("territory"), - inv.get("tax_id"), - inv.debit_to, ", ".join(mode_of_payments.get(inv.name, [])), - inv.project, inv.owner, inv.remarks, - ", ".join(sales_order), ", ".join(delivery_note),", ".join(cost_center), - ", ".join(warehouse), company_currency - ] # map income values base_net_total = 0 for income_acc in income_accounts: income_amount = flt(invoice_income_map.get(inv.name, {}).get(income_acc)) base_net_total += income_amount - row.append(income_amount) + row.update({ + frappe.scrub(income_acc): income_amount + }) # net total - row.append(base_net_total or inv.base_net_total) + row.update({'net_total': base_net_total or inv.base_net_total}) # tax account total_tax = 0 @@ -72,10 +86,18 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No tax_amount_precision = get_field_precision(frappe.get_meta("Sales Taxes and Charges").get_field("tax_amount"), currency=company_currency) or 2 tax_amount = flt(invoice_tax_map.get(inv.name, {}).get(tax_acc), tax_amount_precision) total_tax += tax_amount - row.append(tax_amount) + row.update({ + frappe.scrub(tax_acc): tax_amount + }) # total tax, grand total, outstanding amount & rounded total - row += [total_tax, inv.base_grand_total, inv.base_rounded_total, inv.outstanding_amount] + + row.update({ + 'tax_total': total_tax, + 'grand_total': inv.base_grand_total, + 'rounded_total': inv.base_rounded_total, + 'outstanding_amount': inv.outstanding_amount + }) data.append(row) @@ -84,19 +106,118 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No def get_columns(invoice_list, additional_table_columns): """return columns based on filters""" columns = [ - _("Invoice") + ":Link/Sales Invoice:120", _("Posting Date") + ":Date:80", - _("Customer") + ":Link/Customer:120", _("Customer Name") + "::120" + { + 'label': _("Invoice"), + 'fieldname': 'invoice', + 'fieldtype': 'Link', + 'options': 'Sales Invoice', + 'width': 120 + }, + { + 'label': _("Posting Date"), + 'fieldname': 'posting_date', + 'fieldtype': 'Date', + 'width': 80 + }, + { + 'label': _("Customer"), + 'fieldname': 'customer', + 'fieldtype': 'Link', + 'options': 'Customer', + 'width': 120 + }, + { + 'label': _("Customer Name"), + 'fieldname': 'customer_name', + 'fieldtype': 'Data', + 'width': 120 + }, ] if additional_table_columns: columns += additional_table_columns columns +=[ - _("Customer Group") + ":Link/Customer Group:120", _("Territory") + ":Link/Territory:80", - _("Tax Id") + "::80", _("Receivable Account") + ":Link/Account:120", _("Mode of Payment") + "::120", - _("Project") +":Link/Project:80", _("Owner") + "::150", _("Remarks") + "::150", - _("Sales Order") + ":Link/Sales Order:100", _("Delivery Note") + ":Link/Delivery Note:100", - _("Cost Center") + ":Link/Cost Center:100", _("Warehouse") + ":Link/Warehouse:100", + { + 'label': _("Custmer Group"), + 'fieldname': 'customer_group', + 'fieldtype': 'Link', + 'options': 'Customer Group', + 'width': 120 + }, + { + 'label': _("Territory"), + 'fieldname': 'territory', + 'fieldtype': 'Link', + 'options': 'Territory', + 'width': 80 + }, + { + 'label': _("Tax Id"), + 'fieldname': 'tax_id', + 'fieldtype': 'Data', + 'width': 120 + }, + { + 'label': _("Receivable Account"), + 'fieldname': 'receivable_account', + 'fieldtype': 'Link', + 'options': 'Account', + 'width': 80 + }, + { + 'label': _("Mode Of Payment"), + 'fieldname': 'mode_of_payment', + 'fieldtype': 'Data', + 'width': 120 + }, + { + 'label': _("Project"), + 'fieldname': 'project', + 'fieldtype': 'Link', + 'options': 'project', + 'width': 80 + }, + { + 'label': _("Owner"), + 'fieldname': 'owner', + 'fieldtype': 'Data', + 'width': 150 + }, + { + 'label': _("Remarks"), + 'fieldname': 'remarks', + 'fieldtype': 'Data', + 'width': 150 + }, + { + 'label': _("Sales Order"), + 'fieldname': 'sales_order', + 'fieldtype': 'Link', + 'options': 'Sales Order', + 'width': 100 + }, + { + 'label': _("Delivery Note"), + 'fieldname': 'delivery_note', + 'fieldtype': 'Link', + 'options': 'Delivery Note', + 'width': 100 + }, + { + 'label': _("Cost Center"), + 'fieldname': 'cost_center', + 'fieldtype': 'Link', + 'options': 'Cost Center', + 'width': 100 + }, + { + 'label': _("Warehouse"), + 'fieldname': 'warehouse', + 'fieldtype': 'Link', + 'options': 'Warehouse', + 'width': 100 + }, { "fieldname": "currency", "label": _("Currency"), @@ -105,7 +226,10 @@ def get_columns(invoice_list, additional_table_columns): } ] - income_accounts = tax_accounts = income_columns = tax_columns = [] + income_accounts = [] + tax_accounts = [] + income_columns = [] + tax_columns = [] if invoice_list: income_accounts = frappe.db.sql_list("""select distinct income_account @@ -119,14 +243,65 @@ def get_columns(invoice_list, additional_table_columns): and parent in (%s) order by account_head""" % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) - income_columns = [(account + ":Currency/currency:120") for account in income_accounts] + for account in income_accounts: + income_columns.append({ + "label": account, + "fieldname": frappe.scrub(account), + "fieldtype": "Currency", + "options": 'currency', + "width": 120 + }) + for account in tax_accounts: if account not in income_accounts: - tax_columns.append(account + ":Currency/currency:120") + tax_columns.append({ + "label": account, + "fieldname": frappe.scrub(account), + "fieldtype": "Currency", + "options": 'currency', + "width": 120 + }) - columns = columns + income_columns + [_("Net Total") + ":Currency/currency:120"] + tax_columns + \ - [_("Total Tax") + ":Currency/currency:120", _("Grand Total") + ":Currency/currency:120", - _("Rounded Total") + ":Currency/currency:120", _("Outstanding Amount") + ":Currency/currency:120"] + net_total_column = [{ + "label": _("Net Total"), + "fieldname": "net_total", + "fieldtype": "Currency", + "options": 'currency', + "width": 120 + }] + + total_columns = [ + { + "label": _("Tax Total"), + "fieldname": "tax_total", + "fieldtype": "Currency", + "options": 'currency', + "width": 120 + }, + { + "label": _("Grand Total"), + "fieldname": "grand_total", + "fieldtype": "Currency", + "options": 'currency', + "width": 120 + }, + { + "label": _("Rounded Total"), + "fieldname": "rounded_total", + "fieldtype": "Currency", + "options": 'currency', + "width": 120 + }, + { + "label": _("Outstanding Amount"), + "fieldname": "outstanding_amount", + "fieldtype": "Currency", + "options": 'currency', + "width": 120 + } + ] + + columns = columns + income_columns + net_total_column + tax_columns + total_columns return columns, income_accounts, tax_accounts diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 6b3f2c777c..f6a7fa20d0 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -144,6 +144,10 @@ frappe.ui.form.on('Asset', { frm.set_df_property('purchase_invoice', 'read_only', 1); frm.set_df_property('purchase_receipt', 'read_only', 1); } + else if (frm.doc.is_existing_asset) { + frm.toggle_reqd('purchase_receipt', 0); + frm.toggle_reqd('purchase_invoice', 0); + } else if (frm.doc.purchase_receipt) { // if purchase receipt link is set then set PI disabled frm.toggle_reqd('purchase_invoice', 0); @@ -256,6 +260,7 @@ frappe.ui.form.on('Asset', { }, is_existing_asset: function(frm) { + frm.trigger("toggle_reference_doc"); // frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation)); }, diff --git a/erpnext/buying/onboarding_slide/add_a_few_suppliers/add_a_few_suppliers.json b/erpnext/buying/onboarding_slide/add_a_few_suppliers/add_a_few_suppliers.json index d3adcb7981..ce3d8cfb7b 100644 --- a/erpnext/buying/onboarding_slide/add_a_few_suppliers/add_a_few_suppliers.json +++ b/erpnext/buying/onboarding_slide/add_a_few_suppliers/add_a_few_suppliers.json @@ -12,9 +12,10 @@ } ], "idx": 0, - "image_src": "/assets/erpnext/images/illustrations/supplier-onboard.png", + "image_src": "", + "is_completed": 0, "max_count": 3, - "modified": "2019-12-03 22:53:50.552445", + "modified": "2019-12-09 17:54:18.452038", "modified_by": "Administrator", "name": "Add A Few Suppliers", "owner": "Administrator", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 75564afe59..6150516ac8 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -319,8 +319,8 @@ class AccountsController(TransactionBase): if item.get('discount_amount'): item.rate = item.price_list_rate - item.discount_amount - elif pricing_rule_args.get('free_item'): - apply_pricing_rule_for_free_items(self, pricing_rule_args) + elif pricing_rule_args.get('free_item_data'): + apply_pricing_rule_for_free_items(self, pricing_rule_args.get('free_item_data')) elif pricing_rule_args.get("validate_applied_rule"): for pricing_rule in get_applied_pricing_rules(item): diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 3ec7aff9cb..75b896bb13 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -265,16 +265,17 @@ class BuyingController(StockController): fg_yet_to_be_received = qty_to_be_received_map.get(item_key) - raw_material_data = backflushed_raw_materials_map.get(item_key, {}) - - consumed_qty = raw_material_data.get('qty', 0) - consumed_serial_nos = raw_material_data.get('serial_nos', '') - consumed_batch_nos = raw_material_data.get('batch_nos', '') - transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code) backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code) for raw_material in transferred_raw_materials + non_stock_items: + rm_item_key = '{}{}'.format(raw_material.rm_item_code, item.purchase_order) + raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {}) + + consumed_qty = raw_material_data.get('qty', 0) + consumed_serial_nos = raw_material_data.get('serial_nos', '') + consumed_batch_nos = raw_material_data.get('batch_nos', '') + transferred_qty = raw_material.qty rm_qty_to_be_consumed = transferred_qty - consumed_qty diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 2a5e6d8f49..c99ae7da5e 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -180,6 +180,7 @@ standard_portal_menu_items = [ {"title": _("Admission"), "route": "/admissions", "reference_doctype": "Student Admission", "role": "Student"}, {"title": _("Certification"), "route": "/certification", "reference_doctype": "Certification Application", "role": "Non Profit Portal User"}, {"title": _("Material Request"), "route": "/material-requests", "reference_doctype": "Material Request", "role": "Customer"}, + {"title": _("Appointment Booking"), "route": "/book_appointment"}, ] default_roles = [ diff --git a/erpnext/hr/notification/training_scheduled/training_scheduled.html b/erpnext/hr/notification/training_scheduled/training_scheduled.html index b1aeb2c873..374038ac20 100644 --- a/erpnext/hr/notification/training_scheduled/training_scheduled.html +++ b/erpnext/hr/notification/training_scheduled/training_scheduled.html @@ -1,9 +1,44 @@ -

{{_("Training Event")}}

+ + + + + + + + +
+
+ {{_("Training Event:")}} {{ doc.event_name }} +
+
-

{{ doc.introduction }}

- -

{{_("Details")}}

-{{_("Event Name")}}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }} -
{{_("Event Location")}}: {{ doc.location }} -
{{_("Start Time")}}: {{ doc.start_time }} -
{{_("End Time")}}: {{ doc.end_time }} + + + + + + + + +
+
+ {{ doc.introduction }} +
    +
  • {{_("Event Location")}}: {{ doc.location }}
  • + {% set start = frappe.utils.get_datetime(doc.start_time) %} + {% set end = frappe.utils.get_datetime(doc.end_time) %} + {% if start.date() == end.date() %} +
  • {{_("Date")}}: {{ start.strftime("%A, %d %b %Y") }}
  • +
  • + {{_("Timing")}}: {{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }} +
  • + {% else %} +
  • {{_("Start Time")}}: {{ start.strftime("%A, %d %b %Y at %I:%M %p") }} +
  • +
  • {{_("End Time")}}: {{ end.strftime("%A, %d %b %Y at %I:%M %p") }} +
  • + {% endif %} +
  • {{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
  • +
+
+
\ No newline at end of file diff --git a/erpnext/hr/notification/training_scheduled/training_scheduled.json b/erpnext/hr/notification/training_scheduled/training_scheduled.json index c07e1a6285..966b887572 100644 --- a/erpnext/hr/notification/training_scheduled/training_scheduled.json +++ b/erpnext/hr/notification/training_scheduled/training_scheduled.json @@ -1,5 +1,7 @@ { "attach_print": 0, + "channel": "Email", + "condition": "", "creation": "2017-08-11 03:13:40.519614", "days_in_advance": 0, "docstatus": 0, @@ -9,8 +11,8 @@ "event": "Submit", "idx": 0, "is_standard": 1, - "message": "

{{_(\"Training Event\")}}

\n\n

{{ doc.introduction }}

\n\n

{{_(\"Details\")}}

\n{{_(\"Event Name\")}}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}\n
{{_(\"Event Location\")}}: {{ doc.location }}\n
{{_(\"Start Time\")}}: {{ doc.start_time }}\n
{{_(\"End Time\")}}: {{ doc.end_time }}\n", - "modified": "2017-08-13 22:49:42.338881", + "message": "\n \n \n \n \n \n \n \n
\n
\n {{_(\"Training Event:\")}} {{ doc.event_name }}\n
\n
\n\n\n \n \n \n \n \n \n \n
\n
\n
    \n
  • {{ doc.introduction }}
  • \n
  • {{_(\"Event Location\")}}: {{ doc.location }}
  • \n {% set start = frappe.utils.get_datetime(doc.start_time) %}\n {% set end = frappe.utils.get_datetime(doc.end_time) %}\n {% if start.date() == end.date() %}\n
  • {{_(\"Date\")}}: {{ start.strftime(\"%A, %d %b %Y\") }}
  • \n
  • \n {{_(\"Timing\")}}: {{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}\n
  • \n {% else %}\n
  • {{_(\"Start Time\")}}: {{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}\n
  • \n
  • {{_(\"End Time\")}}: {{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}\n
  • \n {% endif %}\n
\n {{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}\n
\n
", + "modified": "2019-11-29 15:38:31.805409", "modified_by": "Administrator", "module": "HR", "name": "Training Scheduled", diff --git a/erpnext/hr/notification/training_scheduled/training_scheduled.md b/erpnext/hr/notification/training_scheduled/training_scheduled.md index bcadf7df59..374038ac20 100644 --- a/erpnext/hr/notification/training_scheduled/training_scheduled.md +++ b/erpnext/hr/notification/training_scheduled/training_scheduled.md @@ -1,9 +1,44 @@ -

{{_("Training Event")}}

-

{{ message }}

+ + + + + + + + +
+
+ {{_("Training Event:")}} {{ doc.event_name }} +
+
-

{{_("Details")}}

-{{_("Event Name")}}: {{ name }} -
{{_("Event Location")}}: {{ location }} -
{{_("Start Time")}}: {{ start_time }} -
{{_("End Time")}}: {{ end_time }} -
{{_("Attendance")}}: {{ attendance }} + + + + + + + + +
+
+ {{ doc.introduction }} +
    +
  • {{_("Event Location")}}: {{ doc.location }}
  • + {% set start = frappe.utils.get_datetime(doc.start_time) %} + {% set end = frappe.utils.get_datetime(doc.end_time) %} + {% if start.date() == end.date() %} +
  • {{_("Date")}}: {{ start.strftime("%A, %d %b %Y") }}
  • +
  • + {{_("Timing")}}: {{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }} +
  • + {% else %} +
  • {{_("Start Time")}}: {{ start.strftime("%A, %d %b %Y at %I:%M %p") }} +
  • +
  • {{_("End Time")}}: {{ end.strftime("%A, %d %b %Y at %I:%M %p") }} +
  • + {% endif %} +
  • {{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
  • +
+
+
\ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index e3ece56964..f8146bb01e 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -65,6 +65,7 @@ class BOM(WebsiteGenerator): context.parents = [{'name': 'boms', 'title': _('All BOMs') }] def on_update(self): + frappe.cache().hdel('bom_children', self.name) self.check_recursion() self.update_stock_qty() self.update_exploded_items() diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 8ca89171b6..176ca2e4f5 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -605,6 +605,8 @@ erpnext.work_order = { description: __('Max: {0}', [max]), default: max }, data => { + max += (max * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100; + if (data.qty > max) { frappe.msgprint(__('Quantity must not be more than {0}', [max])); reject(); diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 227ef787ca..c4238accac 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -38,7 +38,7 @@ class WorkOrder(Document): ms = frappe.get_doc("Manufacturing Settings") self.set_onload("material_consumption", ms.material_consumption) self.set_onload("backflush_raw_materials_based_on", ms.backflush_raw_materials_based_on) - + self.set_onload("overproduction_percentage", ms.overproduction_percentage_for_work_order) def validate(self): self.validate_production_item() @@ -657,8 +657,9 @@ def make_work_order(item, qty=0, project=None): wo_doc = frappe.new_doc("Work Order") wo_doc.production_item = item wo_doc.update(item_details) - if qty > 0: - wo_doc.qty = qty + + if flt(qty) > 0: + wo_doc.qty = flt(qty) wo_doc.get_items_and_operations_from_bom() return wo_doc diff --git a/erpnext/patches/v12_0/add_export_type_field_in_party_master.py b/erpnext/patches/v12_0/add_export_type_field_in_party_master.py index c565b7ecd8..5bb6e3fb33 100644 --- a/erpnext/patches/v12_0/add_export_type_field_in_party_master.py +++ b/erpnext/patches/v12_0/add_export_type_field_in_party_master.py @@ -27,6 +27,8 @@ def execute(): tax_category = inter_state_category.name for doctype in ('Sales Taxes and Charges Template', 'Purchase Taxes and Charges Template'): + if not frappe.get_meta(doctype).has_field('is_inter_state'): continue + template = frappe.db.get_value(doctype, {'is_inter_state': 1, 'disabled': 0}, ['name']) if template: frappe.db.set_value(doctype, template, 'tax_category', tax_category) diff --git a/erpnext/patches/v12_0/set_gst_category.py b/erpnext/patches/v12_0/set_gst_category.py index 54bc5b3c74..55bbdee7ed 100644 --- a/erpnext/patches/v12_0/set_gst_category.py +++ b/erpnext/patches/v12_0/set_gst_category.py @@ -7,6 +7,8 @@ def execute(): if not company: return + frappe.reload_doc('accounts', 'doctype', 'Tax Category') + make_custom_fields() for doctype in ['Sales Invoice', 'Purchase Invoice']: diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index 3a373a4ab1..0993e69e04 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -52,7 +52,6 @@ def get_attribute_filter_data(): def get_products_for_website(field_filters=None, attribute_filters=None, search=None): - if attribute_filters: item_codes = get_item_codes_by_attributes(attribute_filters) items_by_attributes = get_items([['name', 'in', item_codes]]) @@ -302,6 +301,8 @@ def get_items(filters=None, search=None): if isinstance(filters, dict): filters = [['Item', fieldname, '=', value] for fieldname, value in filters.items()] + enabled_items_filter = get_conditions({ 'disabled': 0 }, 'and') + show_in_website_condition = '' if products_settings.hide_variants: show_in_website_condition = get_conditions({'show_in_website': 1 }, 'and') @@ -337,7 +338,8 @@ def get_items(filters=None, search=None): filter_condition = get_conditions(filters, 'and') where_conditions = ' and '.join( - [condition for condition in [show_in_website_condition, search_condition, filter_condition] if condition] + [condition for condition in [enabled_items_filter, show_in_website_condition, \ + search_condition, filter_condition] if condition] ) left_joins = [] diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 7083d694f8..45f26814a6 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -47,11 +47,11 @@ class Task(NestedSet): if not self.project or frappe.flags.in_test: return - expected_end_date = getdate(frappe.db.get_value("Project", self.project, "expected_end_date")) + expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date") if expected_end_date: - validate_project_dates(expected_end_date, self, "exp_start_date", "exp_end_date", "Expected") - validate_project_dates(expected_end_date, self, "act_start_date", "act_end_date", "Actual") + validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected") + validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual") def validate_status(self): if self.status!=self.get_db_value("status") and self.status == "Completed": @@ -278,4 +278,4 @@ def validate_project_dates(project_end_date, task, task_start, task_end, actual_ frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date)) if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0: - frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date)) \ No newline at end of file + frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date)) diff --git a/erpnext/public/images/illustrations/customers-onboard.png b/erpnext/public/images/illustrations/customers-onboard.png deleted file mode 100644 index 4a517bde29..0000000000 Binary files a/erpnext/public/images/illustrations/customers-onboard.png and /dev/null differ diff --git a/erpnext/public/images/illustrations/desk-onboard.png b/erpnext/public/images/illustrations/desk-onboard.png deleted file mode 100644 index 74b632dc30..0000000000 Binary files a/erpnext/public/images/illustrations/desk-onboard.png and /dev/null differ diff --git a/erpnext/public/images/illustrations/letterhead-onboard.png b/erpnext/public/images/illustrations/letterhead-onboard.png deleted file mode 100644 index fdfd16ad9d..0000000000 Binary files a/erpnext/public/images/illustrations/letterhead-onboard.png and /dev/null differ diff --git a/erpnext/public/images/illustrations/products-onboard.png b/erpnext/public/images/illustrations/products-onboard.png deleted file mode 100644 index 2dee2039e7..0000000000 Binary files a/erpnext/public/images/illustrations/products-onboard.png and /dev/null differ diff --git a/erpnext/public/images/illustrations/supplier-onboard.png b/erpnext/public/images/illustrations/supplier-onboard.png deleted file mode 100644 index 30335f2b63..0000000000 Binary files a/erpnext/public/images/illustrations/supplier-onboard.png and /dev/null differ diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 46a58fba7c..6db849aca7 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -500,6 +500,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ () => { var d = locals[cdt][cdn]; me.add_taxes_from_item_tax_template(d.item_tax_rate); + if (d.free_item_data) { + me.apply_product_discount(d.free_item_data); + } }, () => me.frm.script_manager.trigger("price_list_rate", cdt, cdn), () => me.toggle_conversion_factor(item), @@ -1305,6 +1308,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ me.remove_pricing_rule(frappe.get_doc(d.doctype, d.name)); } + if (d.free_item_data) { + me.apply_product_discount(d.free_item_data); + } + if (d.apply_rule_on_other_items) { items_rule_dict[d.name] = d; } @@ -1334,6 +1341,20 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }, + apply_product_discount: function(free_item_data) { + const items = this.frm.doc.items.filter(d => (d.item_code == free_item_data.item_code + && d.is_free_item)) || []; + + if (!items.length) { + let row_to_modify = frappe.model.add_child(this.frm.doc, + this.frm.doc.doctype + ' Item', 'items'); + + for (let key in free_item_data) { + row_to_modify[key] = free_item_data[key]; + } + } + }, + apply_price_list: function(item, reset_plc_conversion) { // We need to reset plc_conversion_rate sometimes because the call to // `erpnext.stock.get_item_details.apply_price_list` is sensitive to its value diff --git a/erpnext/regional/report/gstr_2/gstr_2.py b/erpnext/regional/report/gstr_2/gstr_2.py index a362269007..f326fe07ca 100644 --- a/erpnext/regional/report/gstr_2/gstr_2.py +++ b/erpnext/regional/report/gstr_2/gstr_2.py @@ -44,12 +44,16 @@ class Gstr2Report(Gstr1Report): for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): invoice_details = self.invoices.get(inv) for rate, items in items_based_on_rate.items(): - row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items) - tax_amount = taxable_value * rate / 100 - if inv in self.igst_invoices: - row += [tax_amount, 0, 0] + if inv not in self.igst_invoices: + rate = rate / 2 + row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items) + tax_amount = taxable_value * rate / 100 + row += [0, tax_amount, tax_amount] else: - row += [0, tax_amount / 2, tax_amount / 2] + row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items) + tax_amount = taxable_value * rate / 100 + row += [tax_amount, 0, 0] + row += [ self.invoice_cess.get(inv), diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index cca8efeca4..aa1b92f9a4 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -5,13 +5,13 @@ frappe.ui.form.on("Customer", { setup: function(frm) { frm.make_methods = { - 'Quotation': () => erpnext.utils.create_new_doc('Quotation', { - 'quotation_to': frm.doc.doctype, - 'party_name': frm.doc.name + 'Quotation': () => frappe.model.open_mapped_doc({ + method: "erpnext.selling.doctype.customer.customer.make_quotation", + frm: cur_frm }), - 'Opportunity': () => erpnext.utils.create_new_doc('Opportunity', { - 'opportunity_from': frm.doc.doctype, - 'party_name': frm.doc.name + 'Opportunity': () => frappe.model.open_mapped_doc({ + method: "erpnext.selling.doctype.customer.customer.make_opportunity", + frm: cur_frm }) } diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 57308cea41..136236c417 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -12,6 +12,7 @@ from erpnext.utilities.transaction_base import TransactionBase from erpnext.accounts.party import validate_party_accounts, get_dashboard_info, get_timeline_data # keep this from frappe.contacts.address_and_contact import load_address_and_contact, delete_contact_and_address from frappe.model.rename_doc import update_linked_doctypes +from frappe.model.mapper import get_mapped_doc class Customer(TransactionBase): def get_feed(self): @@ -238,6 +239,66 @@ def create_contact(contact, party_type, party, email): contact.append('links', dict(link_doctype=party_type, link_name=party)) contact.insert() +@frappe.whitelist() +def make_quotation(source_name, target_doc=None): + + def set_missing_values(source, target): + _set_missing_values(source, target) + + target_doc = get_mapped_doc("Customer", source_name, + {"Customer": { + "doctype": "Quotation", + "field_map": { + "name":"party_name" + } + }}, target_doc, set_missing_values) + + target_doc.quotation_to = "Customer" + target_doc.run_method("set_missing_values") + target_doc.run_method("set_other_charges") + target_doc.run_method("calculate_taxes_and_totals") + + price_list = frappe.get_value("Customer", source_name, 'default_price_list') + if price_list: + target_doc.selling_price_list = price_list + + return target_doc + +@frappe.whitelist() +def make_opportunity(source_name, target_doc=None): + def set_missing_values(source, target): + _set_missing_values(source, target) + + target_doc = get_mapped_doc("Customer", source_name, + {"Customer": { + "doctype": "Opportunity", + "field_map": { + "name": "party_name", + "doctype": "opportunity_from", + } + }}, target_doc, set_missing_values) + + return target_doc + +def _set_missing_values(source, target): + address = frappe.get_all('Dynamic Link', { + 'link_doctype': source.doctype, + 'link_name': source.name, + 'parenttype': 'Address', + }, ['parent'], limit=1) + + contact = frappe.get_all('Dynamic Link', { + 'link_doctype': source.doctype, + 'link_name': source.name, + 'parenttype': 'Contact', + }, ['parent'], limit=1) + + if address: + target.customer_address = address[0].parent + + if contact: + target.contact_person = contact[0].parent + @frappe.whitelist() def get_loyalty_programs(doc): ''' returns applicable loyalty programs for a customer ''' diff --git a/erpnext/selling/onboarding_slide/add_a_few_customers/add_a_few_customers.json b/erpnext/selling/onboarding_slide/add_a_few_customers/add_a_few_customers.json index f39fea4896..92d00bcb38 100644 --- a/erpnext/selling/onboarding_slide/add_a_few_customers/add_a_few_customers.json +++ b/erpnext/selling/onboarding_slide/add_a_few_customers/add_a_few_customers.json @@ -12,9 +12,10 @@ } ], "idx": 0, - "image_src": "/assets/erpnext/images/illustrations/customers-onboard.png", + "image_src": "", + "is_completed": 0, "max_count": 3, - "modified": "2019-12-03 22:54:28.959549", + "modified": "2019-12-09 17:54:01.686006", "modified_by": "Administrator", "name": "Add A Few Customers", "owner": "Administrator", diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index b213a29ae7..33fbc229b6 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -286,14 +286,14 @@ erpnext.pos.PointOfSale = class PointOfSale { if (in_list(['serial_no', 'batch_no'], field)) { args[field] = value; } - + // add to cur_frm const item = this.frm.add_child('items', args); frappe.flags.hide_serial_batch_dialog = true; frappe.run_serially([ () => { - this.frm.script_manager.trigger('item_code', item.doctype, item.name) + return this.frm.script_manager.trigger('item_code', item.doctype, item.name) .then(() => { this.frm.script_manager.trigger('qty', item.doctype, item.name) .then(() => { diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 2eee919b53..ff3515485c 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -14,6 +14,7 @@ from frappe.model.document import Document from frappe.contacts.address_and_contact import load_address_and_contact from frappe.utils.nestedset import NestedSet +from past.builtins import cmp import functools class Company(NestedSet): diff --git a/erpnext/setup/doctype/company/delete_company_transactions.py b/erpnext/setup/doctype/company/delete_company_transactions.py index 637e65578a..1503adb504 100644 --- a/erpnext/setup/doctype/company/delete_company_transactions.py +++ b/erpnext/setup/doctype/company/delete_company_transactions.py @@ -106,7 +106,10 @@ def delete_lead_addresses(company_name): frappe.db.sql("""update tabCustomer set lead_name=NULL where lead_name in ({leads})""".format(leads=",".join(leads))) def delete_communications(doctype, company_name, company_fieldname): - frappe.db.sql(""" - DELETE FROM `tabCommunication` WHERE reference_doctype = %s AND - EXISTS (SELECT name FROM `tab{0}` WHERE {1} = %s AND `tabCommunication`.reference_name = name) - """.format(doctype, company_fieldname), (doctype, company_name)) + reference_docs = frappe.get_all(doctype, filters={company_fieldname:company_name}) + reference_doc_names = [r.name for r in reference_docs] + + communications = frappe.get_all("Communication", filters={"reference_doctype":doctype,"reference_name":["in", reference_doc_names]}) + communication_names = [c.name for c in communications] + + frappe.delete_doc("Communication", communication_names) diff --git a/erpnext/setup/doctype/company/test_company.py b/erpnext/setup/doctype/company/test_company.py index 8d9c23a37d..1664b660b8 100644 --- a/erpnext/setup/doctype/company/test_company.py +++ b/erpnext/setup/doctype/company/test_company.py @@ -88,6 +88,57 @@ class TestCompany(unittest.TestCase): self.delete_mode_of_payment(template) frappe.delete_doc("Company", template) + def test_delete_communication(self): + from erpnext.setup.doctype.company.delete_company_transactions import delete_communications + company = create_child_company() + lead = create_test_lead_in_company(company) + communication = create_company_communication("Lead", lead) + delete_communications("Lead", "Test Company", "company") + self.assertFalse(frappe.db.exists("Communcation", communication)) + self.assertFalse(frappe.db.exists({"doctype":"Comunication Link", "link_name": communication})) + def delete_mode_of_payment(self, company): frappe.db.sql(""" delete from `tabMode of Payment Account` where company =%s """, (company)) + +def create_company_communication(doctype, docname): + comm = frappe.get_doc({ + "doctype": "Communication", + "communication_type": "Communication", + "content": "Deduplication of Links", + "communication_medium": "Email", + "reference_doctype":doctype, + "reference_name":docname + }) + comm.insert() + +def create_child_company(): + child_company = frappe.db.exists("Company", "Test Company") + if not child_company: + child_company = frappe.get_doc({ + "doctype":"Company", + "company_name":"Test Company", + "abbr":"test_company", + "default_currency":"INR" + }) + child_company.insert() + else: + child_company = frappe.get_doc("Company", child_company) + + return child_company.name + +def create_test_lead_in_company(company): + lead = frappe.db.exists("Lead", "Test Lead in new company") + if not lead: + lead = frappe.get_doc({ + "doctype": "Lead", + "lead_name": "Test Lead in new company", + "scompany": company + }) + lead.insert() + else: + lead = frappe.get_doc("Lead", lead) + lead.company = company + lead.save() + return lead.name + diff --git a/erpnext/setup/onboarding_slide/welcome_back_to_erpnext!/welcome_back_to_erpnext!.json b/erpnext/setup/onboarding_slide/welcome_back_to_erpnext!/welcome_back_to_erpnext!.json index bf330d09de..f00dc947d2 100644 --- a/erpnext/setup/onboarding_slide/welcome_back_to_erpnext!/welcome_back_to_erpnext!.json +++ b/erpnext/setup/onboarding_slide/welcome_back_to_erpnext!/welcome_back_to_erpnext!.json @@ -7,10 +7,10 @@ "domains": [], "help_links": [], "idx": 0, - "image_src": "/assets/erpnext/images/illustrations/desk-onboard.png", + "image_src": "", "is_completed": 0, "max_count": 3, - "modified": "2019-12-04 19:21:39.995776", + "modified": "2019-12-09 17:53:53.849953", "modified_by": "Administrator", "name": "Welcome back to ERPNext!", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_slide/welcome_to_erpnext!/welcome_to_erpnext!.json b/erpnext/setup/onboarding_slide/welcome_to_erpnext!/welcome_to_erpnext!.json index 4ea69852af..37eb67b1d7 100644 --- a/erpnext/setup/onboarding_slide/welcome_to_erpnext!/welcome_to_erpnext!.json +++ b/erpnext/setup/onboarding_slide/welcome_to_erpnext!/welcome_to_erpnext!.json @@ -7,13 +7,14 @@ "domains": [], "help_links": [], "idx": 0, - "image_src": "/assets/erpnext/images/illustrations/desk-onboard.png", + "image_src": "", + "is_completed": 0, "max_count": 0, - "modified": "2019-12-03 22:49:12.871260", + "modified": "2019-12-22 21:26:28.414597", "modified_by": "Administrator", "name": "Welcome to ERPNext!", "owner": "Administrator", - "slide_desc": "Setting up an ERP can be overwhelming. But don't worry, we have got your back!\nLet's setup your company.\nThis wizard will help you onboard to ERPNext in a short time!", + "slide_desc": "
Setting up an ERP can be overwhelming. But don't worry, we have got your back! This wizard will help you onboard to ERPNext in a short time!
", "slide_fields": [], "slide_module": "Setup", "slide_order": 1, diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index a2aab3f69e..af8e13288a 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -135,8 +135,7 @@ "publish_in_hub", "hub_category_to_publish", "hub_warehouse", - "synced_with_hub", - "manufacturers" + "synced_with_hub" ], "fields": [ { @@ -1016,12 +1015,6 @@ "label": "Synced With Hub", "read_only": 1 }, - { - "fieldname": "manufacturers", - "fieldtype": "Table", - "label": "Manufacturers", - "options": "Item Manufacturer" - }, { "depends_on": "eval:!doc.__islocal", "fieldname": "over_delivery_receipt_allowance", @@ -1049,7 +1042,7 @@ "idx": 2, "image_field": "image", "max_attachments": 1, - "modified": "2019-10-09 17:05:59.576119", + "modified": "2019-12-13 12:15:56.197246", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 189261cb2d..151be110fc 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -125,7 +125,6 @@ class Item(WebsiteGenerator): self.validate_auto_reorder_enabled_in_stock_settings() self.cant_change() self.update_show_in_website() - self.validate_manufacturer() if not self.get("__islocal"): self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") @@ -145,13 +144,6 @@ class Item(WebsiteGenerator): if cint(frappe.db.get_single_value('Stock Settings', 'clean_description_html')): self.description = clean_html(self.description) - def validate_manufacturer(self): - list_man = [(x.manufacturer, x.manufacturer_part_no) for x in self.get('manufacturers')] - set_man = set(list_man) - - if len(list_man) != len(set_man): - frappe.throw(_("Duplicate entry in Manufacturers table")) - def validate_customer_provided_part(self): if self.is_customer_provided_item: if self.is_purchase_item: diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 4770471259..79ce2312f6 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -622,7 +622,7 @@ frappe.ui.form.on('Stock Entry Detail', { if(r.message) { var d = locals[cdt][cdn]; $.each(r.message, function(k, v) { - d[k] = v; + frappe.model.set_value(cdt, cdn, k, v); // qty and it's subsequent fields weren't triggered }); refresh_field("items"); erpnext.stock.select_batch_and_serial_no(frm, d); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index f81fa683ba..1b9660e6d2 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -372,7 +372,7 @@ class StockEntry(StockController): elif d.t_warehouse and not d.basic_rate: d.basic_rate = get_valuation_rate(d.item_code, d.t_warehouse, self.doctype, self.name, d.allow_zero_valuation_rate, - currency=erpnext.get_company_currency(self.company)) + currency=erpnext.get_company_currency(self.company), company=self.company) def set_actual_qty(self): allow_negative_stock = cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock")) @@ -649,6 +649,12 @@ class StockEntry(StockController): gl_entries = super(StockEntry, self).get_gl_entries(warehouse_account) total_basic_amount = sum([flt(t.basic_amount) for t in self.get("items") if t.t_warehouse]) + divide_based_on = total_basic_amount + + if self.get("additional_costs") and not total_basic_amount: + # if total_basic_amount is 0, distribute additional charges based on qty + divide_based_on = sum(item.qty for item in list(self.get("items"))) + item_account_wise_additional_cost = {} for t in self.get("additional_costs"): @@ -656,8 +662,11 @@ class StockEntry(StockController): if d.t_warehouse: item_account_wise_additional_cost.setdefault((d.item_code, d.name), {}) item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, 0.0) + + multiply_based_on = d.basic_amount if total_basic_amount else d.qty + item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account] += \ - (t.amount * d.basic_amount) / total_basic_amount + (t.amount * multiply_based_on) / divide_based_on if item_account_wise_additional_cost: for d in self.get("items"): diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index eddab5d79d..ee5f237098 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -790,6 +790,50 @@ class TestStockEntry(unittest.TestCase): filters={"voucher_type": "Stock Entry", "voucher_no": mr.name}, fieldname="is_opening") self.assertEqual(is_opening, "Yes") + def test_total_basic_amount_zero(self): + se = frappe.get_doc({"doctype":"Stock Entry", + "purpose":"Material Receipt", + "stock_entry_type":"Material Receipt", + "posting_date": nowdate(), + "company":"_Test Company with perpetual inventory", + "items":[ + { + "item_code":"Basil Leaves", + "description":"Basil Leaves", + "qty": 1, + "basic_rate": 0, + "uom":"Nos", + "t_warehouse": "Stores - TCP1", + "allow_zero_valuation_rate": 1, + "cost_center": "Main - TCP1" + }, + { + "item_code":"Basil Leaves", + "description":"Basil Leaves", + "qty": 2, + "basic_rate": 0, + "uom":"Nos", + "t_warehouse": "Stores - TCP1", + "allow_zero_valuation_rate": 1, + "cost_center": "Main - TCP1" + }, + ], + "additional_costs":[ + {"expense_account":"Miscellaneous Expenses - TCP1", + "amount":100, + "description": "miscellanous"} + ] + }) + se.insert() + se.submit() + + self.check_gl_entries("Stock Entry", se.name, + sorted([ + ["Stock Adjustment - TCP1", 100.0, 0.0], + ["Miscellaneous Expenses - TCP1", 0.0, 100.0] + ]) + ) + def make_serialized_item(item_code=None, serial_no=None, target_warehouse=None): se = frappe.copy_doc(test_records[0]) se.get("items")[0].item_code = item_code or "_Test Serialized Item With Series" diff --git a/erpnext/stock/onboarding_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json b/erpnext/stock/onboarding_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json index 27a30627ee..5ee316786c 100644 --- a/erpnext/stock/onboarding_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json +++ b/erpnext/stock/onboarding_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json @@ -7,9 +7,10 @@ "domains": [], "help_links": [], "idx": 0, - "image_src": "/assets/erpnext/images/illustrations/products-onboard.png", + "image_src": "", + "is_completed": 0, "max_count": 3, - "modified": "2019-12-03 22:54:07.558632", + "modified": "2019-12-09 17:54:09.602885", "modified_by": "Administrator", "name": "Add A Few Products You Buy Or Sell", "owner": "Administrator", diff --git a/erpnext/www/book_appointment/index.js b/erpnext/www/book_appointment/index.js index c8dd5013d5..262e31b3e4 100644 --- a/erpnext/www/book_appointment/index.js +++ b/erpnext/www/book_appointment/index.js @@ -24,20 +24,15 @@ async function get_global_variables() { } function setup_timezone_selector() { - /** - * window.timezones is a dictionary with the following structure - * { IANA name: Pretty name} - * For example : { Asia/Kolkata : "India Time - Asia/Kolkata"} - */ let timezones_element = document.getElementById('appointment-timezone'); - let offset = new Date().getTimezoneOffset(); - Object.keys(window.timezones).forEach((timezone) => { + let local_timezone = moment.tz.guess() + window.timezones.forEach(timezone => { let opt = document.createElement('option'); opt.value = timezone; - if (timezone == moment.tz.guess()) { + if (timezone == local_timezone) { opt.selected = true; } - opt.innerHTML = window.timezones[timezone] + opt.innerHTML = timezone; timezones_element.appendChild(opt) }); } @@ -114,7 +109,7 @@ function get_timeslot_div_layout(timeslot) { timeslot_div.classList.add('unavailable') } timeslot_div.innerHTML = get_slot_layout(start_time); - timeslot_div.id = timeslot.time.substr(11, 20); + timeslot_div.id = timeslot.time.substring(11, 19); timeslot_div.addEventListener('click', select_time); return timeslot_div } diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py index 5b60dd5e7b..7bfac89f30 100644 --- a/erpnext/www/book_appointment/index.py +++ b/erpnext/www/book_appointment/index.py @@ -25,18 +25,8 @@ def get_appointment_settings(): @frappe.whitelist(allow_guest=True) def get_timezones(): - from babel.dates import get_timezone, get_timezone_name, Locale - from frappe.utils.momentjs import get_all_timezones - - translated_dict = {} - locale = Locale.parse(frappe.local.lang, sep="-") - - for tz in get_all_timezones(): - timezone_name = get_timezone_name(get_timezone(tz), locale=locale, width='short') - if timezone_name: - translated_dict[tz] = timezone_name + ' - ' + tz - - return translated_dict + import pytz + return pytz.all_timezones @frappe.whitelist(allow_guest=True) def get_appointment_slots(date, timezone): @@ -90,7 +80,7 @@ def get_available_slots_between(query_start_time, query_end_time, settings): @frappe.whitelist(allow_guest=True) def create_appointment(date, time, tz, contact): - format_string = '%Y-%m-%d %H:%M:%S%z' + format_string = '%Y-%m-%d %H:%M:%S' scheduled_time = datetime.datetime.strptime(date + " " + time, format_string) # Strip tzinfo from datetime objects since it's handled by the doctype scheduled_time = scheduled_time.replace(tzinfo = None)