diff --git a/erpnext/accounts/custom/address.py b/erpnext/accounts/custom/address.py
index c417a493c6..834227bb58 100644
--- a/erpnext/accounts/custom/address.py
+++ b/erpnext/accounts/custom/address.py
@@ -1,7 +1,7 @@
import frappe
from frappe import _
from frappe.contacts.doctype.address.address import Address
-from frappe.contacts.doctype.address.address import get_address_templates
+from frappe.contacts.doctype.address.address import get_address_templates, get_address_display
class ERPNextAddress(Address):
def validate(self):
@@ -22,6 +22,16 @@ class ERPNextAddress(Address):
frappe.throw(_("Address needs to be linked to a Company. Please add a row for Company in the Links table."),
title=_("Company Not Linked"))
+ def on_update(self):
+ """
+ After Address is updated, update the related 'Primary Address' on Customer.
+ """
+ address_display = get_address_display(self.as_dict())
+ filters = { "customer_primary_address": self.name }
+ customers = frappe.db.get_all("Customer", filters=filters, as_list=True)
+ for customer_name in customers:
+ frappe.db.set_value("Customer", customer_name[0], "primary_address", address_display)
+
@frappe.whitelist()
def get_shipping_address(company, address = None):
filters = [
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index a246ae51a4..7d0ecfbafd 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -19,6 +19,7 @@
"delete_linked_ledger_entries",
"book_asset_depreciation_entry_automatically",
"unlink_advance_payment_on_cancelation_of_order",
+ "enable_common_party_accounting",
"post_change_gl_entries",
"enable_discount_accounting",
"tax_settings_section",
@@ -268,6 +269,12 @@
"fieldname": "enable_discount_accounting",
"fieldtype": "Check",
"label": "Enable Discount Accounting"
+ },
+ {
+ "default": "0",
+ "fieldname": "enable_common_party_accounting",
+ "fieldtype": "Check",
+ "label": "Enable Common Party Accounting"
}
],
"icon": "icon-cog",
@@ -275,7 +282,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-08-09 13:08:04.335416",
+ "modified": "2021-08-19 11:17:38.788054",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index 31cfb2da1d..7ea71fc103 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -21,6 +21,10 @@ class BankTransaction(StatusUpdater):
self.update_allocations()
self.clear_linked_payment_entries()
self.set_status(update=True)
+
+ def on_cancel(self):
+ self.clear_linked_payment_entries(for_cancel=True)
+ self.set_status(update=True)
def update_allocations(self):
if self.payment_entries:
@@ -41,21 +45,46 @@ class BankTransaction(StatusUpdater):
frappe.db.set_value(self.doctype, self.name, "status", "Reconciled")
self.reload()
-
- def clear_linked_payment_entries(self):
+
+ def clear_linked_payment_entries(self, for_cancel=False):
for payment_entry in self.payment_entries:
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, for_cancel=for_cancel)
elif payment_entry.payment_document == "Sales Invoice":
- self.clear_sales_invoice(payment_entry)
+ self.clear_sales_invoice(payment_entry, for_cancel=for_cancel)
- def clear_simple_entry(self, payment_entry):
- frappe.db.set_value(payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", self.date)
+ def clear_simple_entry(self, payment_entry, for_cancel=False):
+ if payment_entry.payment_document == "Payment Entry":
+ if frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type") == "Internal Transfer":
+ if len(get_reconciled_bank_transactions(payment_entry)) < 2:
+ return
- def clear_sales_invoice(self, payment_entry):
- frappe.db.set_value("Sales Invoice Payment", dict(parenttype=payment_entry.payment_document,
- parent=payment_entry.payment_entry), "clearance_date", self.date)
+ clearance_date = self.date if not for_cancel else None
+ frappe.db.set_value(
+ payment_entry.payment_document, payment_entry.payment_entry,
+ "clearance_date", clearance_date)
+
+ def clear_sales_invoice(self, payment_entry, for_cancel=False):
+ clearance_date = self.date if not for_cancel else None
+ frappe.db.set_value(
+ "Sales Invoice Payment",
+ dict(
+ parenttype=payment_entry.payment_document,
+ parent=payment_entry.payment_entry
+ ),
+ "clearance_date", clearance_date)
+
+def get_reconciled_bank_transactions(payment_entry):
+ reconciled_bank_transactions = frappe.get_all(
+ 'Bank Transaction Payments',
+ filters = {
+ 'payment_entry': payment_entry.payment_entry
+ },
+ fields = ['parent']
+ )
+
+ return reconciled_bank_transactions
def get_total_allocated_amount(payment_entry):
return frappe.db.sql("""
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js b/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js
index bff41d5539..2585ee9c92 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js
@@ -4,10 +4,12 @@
frappe.listview_settings['Bank Transaction'] = {
add_fields: ["unallocated_amount"],
get_indicator: function(doc) {
- if(flt(doc.unallocated_amount)>0) {
- return [__("Unreconciled"), "orange", "unallocated_amount,>,0"];
+ if(doc.docstatus == 2) {
+ return [__("Cancelled"), "red", "docstatus,=,2"];
} else if(flt(doc.unallocated_amount)<=0) {
return [__("Reconciled"), "green", "unallocated_amount,=,0"];
+ } else if(flt(doc.unallocated_amount)>0) {
+ return [__("Unreconciled"), "orange", "unallocated_amount,>,0"];
}
}
};
diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
index ce149f96e6..439d489119 100644
--- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
@@ -25,7 +25,8 @@ class TestBankTransaction(unittest.TestCase):
def tearDownClass(cls):
for bt in frappe.get_all("Bank Transaction"):
doc = frappe.get_doc("Bank Transaction", bt.name)
- doc.cancel()
+ if doc.docstatus == 1:
+ doc.cancel()
doc.delete()
# Delete directly in DB to avoid validation errors for countries not allowing deletion
@@ -57,6 +58,12 @@ class TestBankTransaction(unittest.TestCase):
clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date")
self.assertTrue(clearance_date is not None)
+ bank_transaction.reload()
+ bank_transaction.cancel()
+
+ clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date")
+ self.assertFalse(clearance_date)
+
# Check if ERPNext can correctly filter a linked payments based on the debit/credit amount
def test_debit_credit_output(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"))
diff --git a/erpnext/accounts/doctype/party_link/__init__.py b/erpnext/accounts/doctype/party_link/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/accounts/doctype/party_link/party_link.js b/erpnext/accounts/doctype/party_link/party_link.js
new file mode 100644
index 0000000000..6da9291d64
--- /dev/null
+++ b/erpnext/accounts/doctype/party_link/party_link.js
@@ -0,0 +1,33 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Party Link', {
+ refresh: function(frm) {
+ frm.set_query('primary_role', () => {
+ return {
+ filters: {
+ name: ['in', ['Customer', 'Supplier']]
+ }
+ };
+ });
+
+ frm.set_query('secondary_role', () => {
+ let party_types = Object.keys(frappe.boot.party_account_types)
+ .filter(p => p != frm.doc.primary_role);
+ return {
+ filters: {
+ name: ['in', party_types]
+ }
+ };
+ });
+ },
+
+ primary_role(frm) {
+ frm.set_value('primary_party', '');
+ frm.set_value('secondary_role', '');
+ },
+
+ secondary_role(frm) {
+ frm.set_value('secondary_party', '');
+ }
+});
diff --git a/erpnext/accounts/doctype/party_link/party_link.json b/erpnext/accounts/doctype/party_link/party_link.json
new file mode 100644
index 0000000000..a1bb15f0d6
--- /dev/null
+++ b/erpnext/accounts/doctype/party_link/party_link.json
@@ -0,0 +1,102 @@
+{
+ "actions": [],
+ "autoname": "ACC-PT-LNK-.###.",
+ "creation": "2021-08-18 21:06:53.027695",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "primary_role",
+ "secondary_role",
+ "column_break_2",
+ "primary_party",
+ "secondary_party"
+ ],
+ "fields": [
+ {
+ "fieldname": "primary_role",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Primary Role",
+ "options": "DocType",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "primary_role",
+ "fieldname": "secondary_role",
+ "fieldtype": "Link",
+ "label": "Secondary Role",
+ "mandatory_depends_on": "primary_role",
+ "options": "DocType"
+ },
+ {
+ "depends_on": "primary_role",
+ "fieldname": "primary_party",
+ "fieldtype": "Dynamic Link",
+ "label": "Primary Party",
+ "mandatory_depends_on": "primary_role",
+ "options": "primary_role"
+ },
+ {
+ "depends_on": "secondary_role",
+ "fieldname": "secondary_party",
+ "fieldtype": "Dynamic Link",
+ "label": "Secondary Party",
+ "mandatory_depends_on": "secondary_role",
+ "options": "secondary_role"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-08-25 20:08:56.761150",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Party Link",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "primary_party",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/party_link/party_link.py b/erpnext/accounts/doctype/party_link/party_link.py
new file mode 100644
index 0000000000..7d58506ce7
--- /dev/null
+++ b/erpnext/accounts/doctype/party_link/party_link.py
@@ -0,0 +1,26 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+
+class PartyLink(Document):
+ def validate(self):
+ if self.primary_role not in ['Customer', 'Supplier']:
+ frappe.throw(_("Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only."),
+ title=_("Invalid Primary Role"))
+
+ existing_party_link = frappe.get_all('Party Link', {
+ 'primary_party': self.secondary_party
+ }, pluck="primary_role")
+ if existing_party_link:
+ frappe.throw(_('{} {} is already linked with another {}')
+ .format(self.secondary_role, self.secondary_party, existing_party_link[0]))
+
+ existing_party_link = frappe.get_all('Party Link', {
+ 'secondary_party': self.primary_party
+ }, pluck="primary_role")
+ if existing_party_link:
+ frappe.throw(_('{} {} is already linked with another {}')
+ .format(self.primary_role, self.primary_party, existing_party_link[0]))
diff --git a/erpnext/accounts/doctype/party_link/test_party_link.py b/erpnext/accounts/doctype/party_link/test_party_link.py
new file mode 100644
index 0000000000..a3ea3959ba
--- /dev/null
+++ b/erpnext/accounts/doctype/party_link/test_party_link.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+class TestPartyLink(unittest.TestCase):
+ pass
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
index b819537400..19c6c8f347 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
@@ -1564,7 +1564,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
- "modified": "2021-08-18 16:13:52.080543",
+ "modified": "2021-08-24 18:19:20.728433",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index a16795e628..e2f02f37ee 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -415,6 +415,8 @@ class PurchaseInvoice(BuyingController):
self.update_project()
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
+ self.process_common_party_accounting()
+
def make_gl_entries(self, gl_entries=None, from_repost=False):
if not gl_entries:
gl_entries = self.get_gl_entries()
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 2071827d99..2dd3d690e9 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -154,9 +154,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
return
}
- $.each(doc["items"], function(i, row) {
+ doc.items.forEach((row) => {
if(row.delivery_note) frappe.model.clear_doc("Delivery Note", row.delivery_note)
- })
+ });
}
set_default_print_format() {
@@ -446,13 +446,25 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
}
currency() {
- super.currency();
+ this._super();
$.each(cur_frm.doc.timesheets, function(i, d) {
let row = frappe.get_doc(d.doctype, d.name)
set_timesheet_detail_rate(row.doctype, row.name, cur_frm.doc.currency, row.timesheet_detail)
});
calculate_total_billing_amount(cur_frm)
}
+
+ currency() {
+ var me = this;
+ super.currency();
+ if (this.frm.doc.timesheets) {
+ this.frm.doc.timesheets.forEach((d) => {
+ let row = frappe.get_doc(d.doctype, d.name)
+ set_timesheet_detail_rate(row.doctype, row.name, me.frm.doc.currency, row.timesheet_detail)
+ });
+ calculate_total_billing_amount(this.frm);
+ }
+ }
};
// for backward compatibility: combine new and previous states
@@ -974,9 +986,9 @@ var calculate_total_billing_amount = function(frm) {
doc.total_billing_amount = 0.0
if (doc.timesheets) {
- $.each(doc.timesheets, function(index, data){
- doc.total_billing_amount += flt(data.billing_amount)
- })
+ doc.timesheets.forEach((d) => {
+ doc.total_billing_amount += flt(d.billing_amount)
+ });
}
refresh_field('total_billing_amount')
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 5023c9c61a..d8aa32e224 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -247,7 +247,7 @@
"depends_on": "customer",
"fetch_from": "customer.customer_name",
"fieldname": "customer_name",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"hide_days": 1,
"hide_seconds": 1,
"in_global_search": 1,
@@ -692,10 +692,11 @@
{
"fieldname": "scan_barcode",
"fieldtype": "Data",
- "options": "Barcode",
"hide_days": 1,
"hide_seconds": 1,
- "label": "Scan Barcode"
+ "label": "Scan Barcode",
+ "length": 1,
+ "options": "Barcode"
},
{
"allow_bulk_edit": 1,
@@ -1059,6 +1060,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Apply Additional Discount On",
+ "length": 15,
"options": "\nGrand Total\nNet Total",
"print_hide": 1
},
@@ -1145,7 +1147,7 @@
{
"description": "In Words will be visible once you save the Sales Invoice.",
"fieldname": "base_in_words",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"hide_days": 1,
"hide_seconds": 1,
"label": "In Words (Company Currency)",
@@ -1205,7 +1207,7 @@
},
{
"fieldname": "in_words",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"hide_days": 1,
"hide_seconds": 1,
"label": "In Words",
@@ -1558,6 +1560,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Print Language",
+ "length": 6,
"print_hide": 1,
"read_only": 1
},
@@ -1645,6 +1648,7 @@
"hide_seconds": 1,
"in_standard_filter": 1,
"label": "Status",
+ "length": 30,
"no_copy": 1,
"options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer",
"print_hide": 1,
@@ -1704,6 +1708,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Is Opening Entry",
+ "length": 4,
"oldfieldname": "is_opening",
"oldfieldtype": "Select",
"options": "No\nYes",
@@ -1715,6 +1720,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "C-Form Applicable",
+ "length": 4,
"no_copy": 1,
"options": "No\nYes",
"print_hide": 1
@@ -2015,7 +2021,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2021-08-18 16:07:45.122570",
+ "modified": "2021-08-25 14:46:05.279588",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 1cf0df00db..fe3ed1670d 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -253,6 +253,8 @@ class SalesInvoice(SellingController):
if "Healthcare" in active_domains:
manage_invoice_submit_cancel(self, "on_submit")
+ self.process_common_party_accounting()
+
def validate_pos_return(self):
if self.is_pos and self.is_return:
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index c3d83c7d74..5a19426eb0 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1140,6 +1140,18 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(loss_for_si['credit'], loss_for_return_si['debit'])
self.assertEqual(loss_for_si['debit'], loss_for_return_si['credit'])
+ def test_incoming_rate_for_stand_alone_credit_note(self):
+ return_si = create_sales_invoice(is_return=1, update_stock=1, qty=-1, rate=90000, incoming_rate=10,
+ company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', debit_to='Debtors - TCP1',
+ income_account='Sales - TCP1', expense_account='Cost of Goods Sold - TCP1', cost_center='Main - TCP1')
+
+ incoming_rate = frappe.db.get_value('Stock Ledger Entry', {'voucher_no': return_si.name}, 'incoming_rate')
+ debit_amount = frappe.db.get_value('GL Entry',
+ {'voucher_no': return_si.name, 'account': 'Stock In Hand - TCP1'}, 'debit')
+
+ self.assertEqual(debit_amount, 10.0)
+ self.assertEqual(incoming_rate, 10.0)
+
def test_discount_on_net_total(self):
si = frappe.copy_doc(test_records[2])
si.apply_discount_on = "Net Total"
@@ -2163,6 +2175,50 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
self.assertTrue(schedule.journal_entry)
+ def test_sales_invoice_against_supplier(self):
+ from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import make_customer
+ from erpnext.buying.doctype.supplier.test_supplier import create_supplier
+
+ # create a customer
+ customer = make_customer(customer="_Test Common Supplier")
+ # create a supplier
+ supplier = create_supplier(supplier_name="_Test Common Supplier").name
+
+ # create a party link between customer & supplier
+ # set primary role as supplier
+ party_link = frappe.new_doc("Party Link")
+ party_link.primary_role = "Supplier"
+ party_link.primary_party = supplier
+ party_link.secondary_role = "Customer"
+ party_link.secondary_party = customer
+ party_link.save()
+
+ # enable common party accounting
+ frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 1)
+
+ # create a sales invoice
+ si = create_sales_invoice(customer=customer, parent_cost_center="_Test Cost Center - _TC")
+
+ # check outstanding of sales invoice
+ si.reload()
+ self.assertEqual(si.status, 'Paid')
+ self.assertEqual(flt(si.outstanding_amount), 0.0)
+
+ # check creation of journal entry
+ jv = frappe.get_all('Journal Entry Account', {
+ 'account': si.debit_to,
+ 'party_type': 'Customer',
+ 'party': si.customer,
+ 'reference_type': si.doctype,
+ 'reference_name': si.name
+ }, pluck='credit_in_account_currency')
+
+ self.assertTrue(jv)
+ self.assertEqual(jv[0], si.grand_total)
+
+ party_link.delete()
+ frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 0)
+
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####'
@@ -2375,7 +2431,8 @@ def create_sales_invoice(**args):
"asset": args.asset or None,
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no,
- "conversion_factor": 1
+ "conversion_factor": 1,
+ "incoming_rate": args.incoming_rate or 0
})
if not args.do_not_save:
diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
index c77076cb90..b90f3f0904 100644
--- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
+++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
@@ -53,7 +53,6 @@
"column_break_24",
"base_net_rate",
"base_net_amount",
- "incoming_rate",
"drop_ship",
"delivered_by_supplier",
"accounting",
@@ -81,6 +80,7 @@
"target_warehouse",
"quality_inspection",
"batch_no",
+ "incoming_rate",
"col_break5",
"allow_zero_valuation_rate",
"serial_no",
@@ -807,12 +807,12 @@
"read_only": 1
},
{
+ "depends_on": "eval:parent.is_return && parent.update_stock && !parent.return_against",
"fieldname": "incoming_rate",
"fieldtype": "Currency",
- "label": "Incoming Rate",
+ "label": "Incoming Rate (Costing)",
"no_copy": 1,
- "print_hide": 1,
- "read_only": 1
+ "print_hide": 1
},
{
"depends_on": "eval: doc.uom != doc.stock_uom",
@@ -833,7 +833,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-08-12 20:15:47.668399",
+ "modified": "2021-08-19 13:41:53.435827",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index 7c4ff73d90..8bf7b78f58 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -367,21 +367,25 @@ class Subscription(Document):
)
# Discounts
- if self.additional_discount_percentage:
- invoice.additional_discount_percentage = self.additional_discount_percentage
+ if self.is_trialling():
+ invoice.additional_discount_percentage = 100
+ else:
+ if self.additional_discount_percentage:
+ invoice.additional_discount_percentage = self.additional_discount_percentage
- if self.additional_discount_amount:
- invoice.discount_amount = self.additional_discount_amount
+ if self.additional_discount_amount:
+ invoice.discount_amount = self.additional_discount_amount
- if self.additional_discount_percentage or self.additional_discount_amount:
- discount_on = self.apply_additional_discount
- invoice.apply_discount_on = discount_on if discount_on else 'Grand Total'
+ if self.additional_discount_percentage or self.additional_discount_amount:
+ discount_on = self.apply_additional_discount
+ invoice.apply_discount_on = discount_on if discount_on else 'Grand Total'
# Subscription period
invoice.from_date = self.current_invoice_start
invoice.to_date = self.current_invoice_end
invoice.flags.ignore_mandatory = True
+
invoice.save()
if self.submit_invoice:
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index 1536a237de..0cb872c4b8 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -240,14 +240,15 @@ def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details):
def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers):
tds_amount = 0
invoice_filters = {
- 'name': ('in', vouchers),
- 'docstatus': 1
+ 'name': ('in', vouchers),
+ 'docstatus': 1,
+ 'apply_tds': 1
}
field = 'sum(net_total)'
- if not cint(tax_details.consider_party_ledger_amount):
- invoice_filters.update({'apply_tds': 1})
+ if cint(tax_details.consider_party_ledger_amount):
+ invoice_filters.pop('apply_tds', None)
field = 'sum(grand_total)'
supp_credit_amt = frappe.db.get_value('Purchase Invoice', invoice_filters, field) or 0.0
diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
index 1c687e5cb1..0f921db678 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
@@ -145,6 +145,36 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in invoices:
d.cancel()
+ def test_tds_calculation_on_net_total(self):
+ frappe.db.set_value("Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS")
+ invoices = []
+
+ pi = create_purchase_invoice(supplier = "Test TDS Supplier4", rate = 20000, do_not_save=True)
+ pi.append('taxes', {
+ "category": "Total",
+ "charge_type": "Actual",
+ "account_head": '_Test Account VAT - _TC',
+ "cost_center": 'Main - _TC',
+ "tax_amount": 1000,
+ "description": "Test",
+ "add_deduct_tax": "Add"
+
+ })
+ pi.save()
+ pi.submit()
+ invoices.append(pi)
+
+ # Second Invoice will apply TDS checked
+ pi1 = create_purchase_invoice(supplier = "Test TDS Supplier4", rate = 20000)
+ pi1.submit()
+ invoices.append(pi1)
+
+ self.assertEqual(pi1.taxes[0].tax_amount, 4000)
+
+ #delete invoices to avoid clashing
+ for d in invoices:
+ d.cancel()
+
def cancel_invoices():
purchase_invoices = frappe.get_all("Purchase Invoice", {
'supplier': ['in', ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']],
@@ -220,7 +250,7 @@ def create_sales_invoice(**args):
def create_records():
# create a new suppliers
- for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3']:
+ for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3', 'Test TDS Supplier4']:
if frappe.db.exists('Supplier', name):
continue
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 5d8d49d6a6..3723c8e0d2 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -78,13 +78,10 @@ def validate_filters(filters, account_details):
def validate_party(filters):
party_type, party = filters.get("party_type"), filters.get("party")
- if party:
- if not party_type:
- frappe.throw(_("To filter based on Party, select Party Type first"))
- else:
- for d in party:
- if not frappe.db.exists(party_type, d):
- frappe.throw(_("Invalid {0}: {1}").format(party_type, d))
+ if party and party_type:
+ for d in party:
+ if not frappe.db.exists(party_type, d):
+ frappe.throw(_("Invalid {0}: {1}").format(party_type, d))
def set_account_currency(filters):
if filters.get("account") or (filters.get('party') and len(filters.party) == 1):
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index 8fdbbf95d4..9a61b79ed3 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -59,7 +59,7 @@ def make_depreciation_entry(asset_name, date=None):
"credit_in_account_currency": d.depreciation_amount,
"reference_type": "Asset",
"reference_name": asset.name,
- "cost_center": ""
+ "cost_center": depreciation_cost_center
}
debit_entry = {
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 01d354df81..f4af8932b6 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -14,7 +14,7 @@ from erpnext.accounts.utils import get_fiscal_years, validate_fiscal_year, get_a
from erpnext.utilities.transaction_base import TransactionBase
from erpnext.buying.utils import update_last_purchase_rate
from erpnext.controllers.sales_and_purchase_return import validate_return
-from erpnext.accounts.party import get_party_account_currency, validate_party_frozen_disabled
+from erpnext.accounts.party import get_party_account_currency, validate_party_frozen_disabled, get_party_account
from erpnext.accounts.doctype.pricing_rule.utils import (apply_pricing_rule_on_transaction,
apply_pricing_rule_for_free_items, get_applied_pricing_rules)
from erpnext.exceptions import InvalidCurrency
@@ -159,7 +159,8 @@ class AccountsController(TransactionBase):
self.set_due_date()
self.set_payment_schedule()
self.validate_payment_schedule_amount()
- self.validate_due_date()
+ if not self.get('ignore_default_payment_terms_template'):
+ self.validate_due_date()
self.validate_advance_entries()
def validate_non_invoice_documents_schedule(self):
@@ -1362,6 +1363,67 @@ class AccountsController(TransactionBase):
return False
+ def process_common_party_accounting(self):
+ is_invoice = self.doctype in ['Sales Invoice', 'Purchase Invoice']
+ if not is_invoice:
+ return
+
+ if frappe.db.get_single_value('Accounts Settings', 'enable_common_party_accounting'):
+ party_link = self.get_common_party_link()
+ if party_link and self.outstanding_amount:
+ self.create_advance_and_reconcile(party_link)
+
+ def get_common_party_link(self):
+ party_type, party = self.get_party()
+ return frappe.db.get_value(
+ doctype='Party Link',
+ filters={'secondary_role': party_type, 'secondary_party': party},
+ fieldname=['primary_role', 'primary_party'],
+ as_dict=True
+ )
+
+ def create_advance_and_reconcile(self, party_link):
+ secondary_party_type, secondary_party = self.get_party()
+ primary_party_type, primary_party = party_link.primary_role, party_link.primary_party
+
+ primary_account = get_party_account(primary_party_type, primary_party, self.company)
+ secondary_account = get_party_account(secondary_party_type, secondary_party, self.company)
+
+ jv = frappe.new_doc('Journal Entry')
+ jv.voucher_type = 'Journal Entry'
+ jv.posting_date = self.posting_date
+ jv.company = self.company
+ jv.remark = 'Adjustment for {} {}'.format(self.doctype, self.name)
+
+ reconcilation_entry = frappe._dict()
+ advance_entry = frappe._dict()
+
+ reconcilation_entry.account = secondary_account
+ reconcilation_entry.party_type = secondary_party_type
+ reconcilation_entry.party = secondary_party
+ reconcilation_entry.reference_type = self.doctype
+ reconcilation_entry.reference_name = self.name
+ reconcilation_entry.cost_center = self.cost_center
+
+ advance_entry.account = primary_account
+ advance_entry.party_type = primary_party_type
+ advance_entry.party = primary_party
+ advance_entry.cost_center = self.cost_center
+ advance_entry.is_advance = 'Yes'
+
+ if self.doctype == 'Sales Invoice':
+ reconcilation_entry.credit_in_account_currency = self.outstanding_amount
+ advance_entry.debit_in_account_currency = self.outstanding_amount
+ else:
+ advance_entry.credit_in_account_currency = self.outstanding_amount
+ reconcilation_entry.debit_in_account_currency = self.outstanding_amount
+
+ jv.append('accounts', reconcilation_entry)
+ jv.append('accounts', advance_entry)
+
+ jv.save()
+ jv.submit()
+
@frappe.whitelist()
def get_tax_rate(account_head):
return frappe.db.get_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True)
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 5ee1f2f7fb..01486fcd65 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -394,19 +394,6 @@ def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None
if not return_against:
return_against = frappe.get_cached_value(voucher_type, voucher_no, "return_against")
- if not return_against and voucher_type == 'Sales Invoice' and sle:
- return get_incoming_rate({
- "item_code": sle.item_code,
- "warehouse": sle.warehouse,
- "posting_date": sle.get('posting_date'),
- "posting_time": sle.get('posting_time'),
- "qty": sle.actual_qty,
- "serial_no": sle.get('serial_no'),
- "company": sle.company,
- "voucher_type": sle.voucher_type,
- "voucher_no": sle.voucher_no
- }, raise_error_if_no_rate=False)
-
return_against_item_field = get_return_against_item_fields(voucher_type)
filters = get_filters(voucher_type, voucher_no, voucher_detail_no,
@@ -417,7 +404,24 @@ def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None
else:
select_field = "abs(stock_value_difference / actual_qty)"
- return flt(frappe.db.get_value("Stock Ledger Entry", filters, select_field))
+ rate = flt(frappe.db.get_value("Stock Ledger Entry", filters, select_field))
+ if not (rate and return_against) and voucher_type in ['Sales Invoice', 'Delivery Note']:
+ rate = frappe.db.get_value(f'{voucher_type} Item', voucher_detail_no, 'incoming_rate')
+
+ if not rate and sle:
+ rate = get_incoming_rate({
+ "item_code": sle.item_code,
+ "warehouse": sle.warehouse,
+ "posting_date": sle.get('posting_date'),
+ "posting_time": sle.get('posting_time'),
+ "qty": sle.actual_qty,
+ "serial_no": sle.get('serial_no'),
+ "company": sle.company,
+ "voucher_type": sle.voucher_type,
+ "voucher_no": sle.voucher_no
+ }, raise_error_if_no_rate=False)
+
+ return rate
def get_return_against_item_fields(voucher_type):
return_against_item_fields = {
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index da2765dede..4ea0e114b4 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import cint, flt, cstr, get_link_to_form, nowtime
-from frappe import _, throw
+from frappe import _, bold, throw
from erpnext.stock.get_item_details import get_bin_details
from erpnext.stock.utils import get_incoming_rate
from erpnext.stock.get_item_details import get_conversion_factor
@@ -16,7 +16,6 @@ from erpnext.controllers.stock_controller import StockController
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
class SellingController(StockController):
-
def get_feed(self):
return _("To {0} | {1} {2}").format(self.customer_name, self.currency,
self.grand_total)
@@ -169,39 +168,96 @@ class SellingController(StockController):
def validate_selling_price(self):
def throw_message(idx, item_name, rate, ref_rate_field):
- bold_net_rate = frappe.bold("net rate")
- msg = (_("""Row #{}: Selling rate for item {} is lower than its {}. Selling {} should be atleast {}""")
- .format(idx, frappe.bold(item_name), frappe.bold(ref_rate_field), bold_net_rate, frappe.bold(rate)))
- msg += "
"
- msg += (_("""You can alternatively disable selling price validation in {} to bypass this validation.""")
- .format(get_link_to_form("Selling Settings", "Selling Settings")))
- frappe.throw(msg, title=_("Invalid Selling Price"))
+ throw(_("""Row #{0}: Selling rate for item {1} is lower than its {2}.
+ Selling {3} should be atleast {4}.
Alternatively,
+ you can disable selling price validation in {5} to bypass
+ this validation.""").format(
+ idx,
+ bold(item_name),
+ bold(ref_rate_field),
+ bold("net rate"),
+ bold(rate),
+ get_link_to_form("Selling Settings", "Selling Settings"),
+ ), title=_("Invalid Selling Price"))
- if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"):
- return
- if hasattr(self, "is_return") and self.is_return:
+ if (
+ self.get("is_return")
+ or not frappe.db.get_single_value("Selling Settings", "validate_selling_price")
+ ):
return
- for it in self.get("items"):
- if not it.item_code:
+ is_internal_customer = self.get('is_internal_customer')
+ valuation_rate_map = {}
+
+ for item in self.items:
+ if not item.item_code:
continue
- last_purchase_rate, is_stock_item = frappe.get_cached_value("Item", it.item_code, ["last_purchase_rate", "is_stock_item"])
- last_purchase_rate_in_sales_uom = last_purchase_rate * (it.conversion_factor or 1)
- if flt(it.base_net_rate) < flt(last_purchase_rate_in_sales_uom):
- throw_message(it.idx, frappe.bold(it.item_name), last_purchase_rate_in_sales_uom, "last purchase rate")
+ last_purchase_rate, is_stock_item = frappe.get_cached_value(
+ "Item", item.item_code, ("last_purchase_rate", "is_stock_item")
+ )
- last_valuation_rate = frappe.db.sql("""
- SELECT valuation_rate FROM `tabStock Ledger Entry` WHERE item_code = %s
- AND warehouse = %s AND valuation_rate > 0
- ORDER BY posting_date DESC, posting_time DESC, creation DESC LIMIT 1
- """, (it.item_code, it.warehouse))
- if last_valuation_rate:
- last_valuation_rate_in_sales_uom = last_valuation_rate[0][0] * (it.conversion_factor or 1)
- if is_stock_item and flt(it.base_net_rate) < flt(last_valuation_rate_in_sales_uom) \
- and not self.get('is_internal_customer'):
- throw_message(it.idx, frappe.bold(it.item_name), last_valuation_rate_in_sales_uom, "valuation rate")
+ last_purchase_rate_in_sales_uom = (
+ last_purchase_rate * (item.conversion_factor or 1)
+ )
+ if flt(item.base_net_rate) < flt(last_purchase_rate_in_sales_uom):
+ throw_message(
+ item.idx,
+ item.item_name,
+ last_purchase_rate_in_sales_uom,
+ "last purchase rate"
+ )
+
+ if is_internal_customer or not is_stock_item:
+ continue
+
+ valuation_rate_map[(item.item_code, item.warehouse)] = None
+
+ if not valuation_rate_map:
+ return
+
+ or_conditions = (
+ f"""(item_code = {frappe.db.escape(valuation_rate[0])}
+ and warehouse = {frappe.db.escape(valuation_rate[1])})"""
+ for valuation_rate in valuation_rate_map
+ )
+
+ valuation_rates = frappe.db.sql(f"""
+ select
+ item_code, warehouse, valuation_rate
+ from
+ `tabBin`
+ where
+ ({" or ".join(or_conditions)})
+ and valuation_rate > 0
+ """, as_dict=True)
+
+ for rate in valuation_rates:
+ valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate
+
+ for item in self.items:
+ if not item.item_code:
+ continue
+
+ last_valuation_rate = valuation_rate_map.get(
+ (item.item_code, item.warehouse)
+ )
+
+ if not last_valuation_rate:
+ continue
+
+ last_valuation_rate_in_sales_uom = (
+ last_valuation_rate * (item.conversion_factor or 1)
+ )
+
+ if flt(item.base_net_rate) < flt(last_valuation_rate_in_sales_uom):
+ throw_message(
+ item.idx,
+ item.item_name,
+ last_valuation_rate_in_sales_uom,
+ "valuation rate"
+ )
def get_item_list(self):
il = []
@@ -306,7 +362,7 @@ class SellingController(StockController):
sales_order.update_reserved_qty(so_item_rows)
def set_incoming_rate(self):
- if self.doctype not in ("Delivery Note", "Sales Invoice", "Sales Order"):
+ if self.doctype not in ("Delivery Note", "Sales Invoice"):
return
items = self.get("items") + (self.get("packed_items") or [])
@@ -315,18 +371,19 @@ class SellingController(StockController):
# Get incoming rate based on original item cost based on valuation method
qty = flt(d.get('stock_qty') or d.get('actual_qty'))
- d.incoming_rate = get_incoming_rate({
- "item_code": d.item_code,
- "warehouse": d.warehouse,
- "posting_date": self.get('posting_date') or self.get('transaction_date'),
- "posting_time": self.get('posting_time') or nowtime(),
- "qty": qty if cint(self.get("is_return")) else (-1 * qty),
- "serial_no": d.get('serial_no'),
- "company": self.company,
- "voucher_type": self.doctype,
- "voucher_no": self.name,
- "allow_zero_valuation": d.get("allow_zero_valuation")
- }, raise_error_if_no_rate=False)
+ if not d.incoming_rate:
+ d.incoming_rate = get_incoming_rate({
+ "item_code": d.item_code,
+ "warehouse": d.warehouse,
+ "posting_date": self.get('posting_date') or self.get('transaction_date'),
+ "posting_time": self.get('posting_time') or nowtime(),
+ "qty": qty if cint(self.get("is_return")) else (-1 * qty),
+ "serial_no": d.get('serial_no'),
+ "company": self.company,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "allow_zero_valuation": d.get("allow_zero_valuation")
+ }, raise_error_if_no_rate=False)
# For internal transfers use incoming rate as the valuation rate
if self.is_internal_transfer():
diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py
index b1f89b08d7..7b24e50b14 100644
--- a/erpnext/controllers/status_updater.py
+++ b/erpnext/controllers/status_updater.py
@@ -86,7 +86,8 @@ status_map = {
],
"Bank Transaction": [
["Unreconciled", "eval:self.docstatus == 1 and self.unallocated_amount>0"],
- ["Reconciled", "eval:self.docstatus == 1 and self.unallocated_amount<=0"]
+ ["Reconciled", "eval:self.docstatus == 1 and self.unallocated_amount<=0"],
+ ["Cancelled", "eval:self.docstatus == 2"]
],
"POS Opening Entry": [
["Draft", None],
diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js
index 263005ef6c..7aa0b77759 100644
--- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js
+++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js
@@ -2,8 +2,8 @@
// For license information, please see license.txt
frappe.ui.form.on('LinkedIn Settings', {
- onload: function(frm){
- if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){
+ onload: function(frm) {
+ if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret) {
frappe.confirm(
__('Session not valid, Do you want to login?'),
function(){
@@ -14,8 +14,9 @@ frappe.ui.form.on('LinkedIn Settings', {
}
);
}
+ frm.dashboard.set_headline(__("For more information, {0}.", [`${__('Click here')}`]));
},
- refresh: function(frm){
+ refresh: function(frm) {
if (frm.doc.session_status=="Expired"){
let msg = __("Session Not Active. Save doc to login.");
frm.dashboard.set_headline_alert(
@@ -53,7 +54,7 @@ frappe.ui.form.on('LinkedIn Settings', {
);
}
},
- login: function(frm){
+ login: function(frm) {
if (frm.doc.consumer_key && frm.doc.consumer_secret){
frappe.dom.freeze();
frappe.call({
@@ -67,7 +68,7 @@ frappe.ui.form.on('LinkedIn Settings', {
});
}
},
- after_save: function(frm){
+ after_save: function(frm) {
frm.trigger("login");
}
});
diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json
index 9eacb0011c..f882e36c32 100644
--- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json
+++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json
@@ -2,6 +2,7 @@
"actions": [],
"creation": "2020-01-30 13:36:39.492931",
"doctype": "DocType",
+ "documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/linkedin-settings",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -87,7 +88,7 @@
],
"issingle": 1,
"links": [],
- "modified": "2020-04-16 23:22:51.966397",
+ "modified": "2021-02-18 15:19:21.920725",
"modified_by": "Administrator",
"module": "CRM",
"name": "LinkedIn Settings",
diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
index d8c6fb4f90..9b88d78c1f 100644
--- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
+++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
@@ -3,11 +3,12 @@
# For license information, please see license.txt
from __future__ import unicode_literals
-import frappe, requests, json
+import frappe
+import requests
from frappe import _
-from frappe.utils import get_site_url, get_url_to_form, get_link_to_form
+from frappe.utils import get_url_to_form
from frappe.model.document import Document
-from frappe.utils.file_manager import get_file, get_file_path
+from frappe.utils.file_manager import get_file_path
from six.moves.urllib.parse import urlencode
class LinkedInSettings(Document):
@@ -42,11 +43,7 @@ class LinkedInSettings(Document):
self.db_set("access_token", response["access_token"])
def get_member_profile(self):
- headers = {
- "Authorization": "Bearer {}".format(self.access_token)
- }
- url = "https://api.linkedin.com/v2/me"
- response = requests.get(url=url, headers=headers)
+ response = requests.get(url="https://api.linkedin.com/v2/me", headers=self.get_headers())
response = frappe.parse_json(response.content.decode())
frappe.db.set_value(self.doctype, self.name, {
@@ -55,16 +52,16 @@ class LinkedInSettings(Document):
"session_status": "Active"
})
frappe.local.response["type"] = "redirect"
- frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings")
+ frappe.local.response["location"] = get_url_to_form("LinkedIn Settings", "LinkedIn Settings")
- def post(self, text, media=None):
+ def post(self, text, title, media=None):
if not media:
- return self.post_text(text)
+ return self.post_text(text, title)
else:
media_id = self.upload_image(media)
if media_id:
- return self.post_text(text, media_id=media_id)
+ return self.post_text(text, title, media_id=media_id)
else:
frappe.log_error("Failed to upload media.","LinkedIn Upload Error")
@@ -82,9 +79,7 @@ class LinkedInSettings(Document):
}]
}
}
- headers = {
- "Authorization": "Bearer {}".format(self.access_token)
- }
+ headers = self.get_headers()
response = self.http_post(url=register_url, body=body, headers=headers)
if response.status_code == 200:
@@ -100,24 +95,33 @@ class LinkedInSettings(Document):
return None
- def post_text(self, text, media_id=None):
+ def post_text(self, text, title, media_id=None):
url = "https://api.linkedin.com/v2/shares"
- headers = {
- "X-Restli-Protocol-Version": "2.0.0",
- "Authorization": "Bearer {}".format(self.access_token),
- "Content-Type": "application/json; charset=UTF-8"
- }
+ headers = self.get_headers()
+ headers["X-Restli-Protocol-Version"] = "2.0.0"
+ headers["Content-Type"] = "application/json; charset=UTF-8"
+
body = {
"distribution": {
"linkedInDistributionTarget": {}
},
"owner":"urn:li:organization:{0}".format(self.company_id),
- "subject": "Test Share Subject",
+ "subject": title,
"text": {
"text": text
}
}
+ reference_url = self.get_reference_url(text)
+ if reference_url:
+ body["content"] = {
+ "contentEntities": [
+ {
+ "entityLocation": reference_url
+ }
+ ]
+ }
+
if media_id:
body["content"]= {
"contentEntities": [{
@@ -141,20 +145,60 @@ class LinkedInSettings(Document):
raise
except Exception as e:
- content = json.loads(response.content)
-
- if response.status_code == 401:
- self.db_set("session_status", "Expired")
- frappe.db.commit()
- frappe.throw(content["message"], title="LinkedIn Error - Unauthorized")
- elif response.status_code == 403:
- frappe.msgprint(_("You Didn't have permission to access this API"))
- frappe.throw(content["message"], title="LinkedIn Error - Access Denied")
- else:
- frappe.throw(response.reason, title=response.status_code)
-
+ self.api_error(response)
+
return response
+ def get_headers(self):
+ return {
+ "Authorization": "Bearer {}".format(self.access_token)
+ }
+
+ def get_reference_url(self, text):
+ import re
+ regex_url = r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+"
+ urls = re.findall(regex_url, text)
+ if urls:
+ return urls[0]
+
+ def delete_post(self, post_id):
+ try:
+ response = requests.delete(url="https://api.linkedin.com/v2/shares/urn:li:share:{0}".format(post_id), headers=self.get_headers())
+ if response.status_code !=200:
+ raise
+ except Exception:
+ self.api_error(response)
+
+ def get_post(self, post_id):
+ url = "https://api.linkedin.com/v2/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=urn:li:organization:{0}&shares[0]=urn:li:share:{1}".format(self.company_id, post_id)
+
+ try:
+ response = requests.get(url=url, headers=self.get_headers())
+ if response.status_code !=200:
+ raise
+
+ except Exception:
+ self.api_error(response)
+
+ response = frappe.parse_json(response.content.decode())
+ if len(response.elements):
+ return response.elements[0]
+
+ return None
+
+ def api_error(self, response):
+ content = frappe.parse_json(response.content.decode())
+
+ if response.status_code == 401:
+ self.db_set("session_status", "Expired")
+ frappe.db.commit()
+ frappe.throw(content["message"], title=_("LinkedIn Error - Unauthorized"))
+ elif response.status_code == 403:
+ frappe.msgprint(_("You didn't have permission to access this API"))
+ frappe.throw(content["message"], title=_("LinkedIn Error - Access Denied"))
+ else:
+ frappe.throw(response.reason, title=response.status_code)
+
@frappe.whitelist(allow_guest=True)
def callback(code=None, error=None, error_description=None):
if not error:
diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js
index 632012b31d..cb95881cb4 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.js
+++ b/erpnext/crm/doctype/opportunity/opportunity.js
@@ -95,9 +95,17 @@ frappe.ui.form.on("Opportunity", {
}, __('Create'));
}
- frm.add_custom_button(__('Quotation'),
- cur_frm.cscript.create_quotation, __('Create'));
+ if (frm.doc.opportunity_from != "Customer") {
+ frm.add_custom_button(__('Customer'),
+ function() {
+ frm.trigger("make_customer")
+ }, __('Create'));
+ }
+ frm.add_custom_button(__('Quotation'),
+ function() {
+ frm.trigger("create_quotation")
+ }, __('Create'));
}
if(!frm.doc.__islocal && frm.perm[0].write && frm.doc.docstatus==0) {
@@ -195,6 +203,13 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
frm: cur_frm
})
}
+
+ make_customer() {
+ frappe.model.open_mapped_doc({
+ method: "erpnext.crm.doctype.opportunity.opportunity.make_customer",
+ frm: cur_frm
+ })
+ }
};
extend_cscript(cur_frm.cscript, new erpnext.crm.Opportunity({frm: cur_frm}));
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index 8ce482a3f9..a74a94afd6 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -287,6 +287,24 @@ def make_request_for_quotation(source_name, target_doc=None):
return doclist
+@frappe.whitelist()
+def make_customer(source_name, target_doc=None):
+ def set_missing_values(source, target):
+ if source.opportunity_from == "Lead":
+ target.lead_name = source.party_name
+
+ doclist = get_mapped_doc("Opportunity", source_name, {
+ "Opportunity": {
+ "doctype": "Customer",
+ "field_map": {
+ "currency": "default_currency",
+ "customer_name": "customer_name"
+ }
+ }
+ }, target_doc, set_missing_values)
+
+ return doclist
+
@frappe.whitelist()
def make_supplier_quotation(source_name, target_doc=None):
doclist = get_mapped_doc("Opportunity", source_name, {
diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js
index 6fb0f975f4..a8f5deea53 100644
--- a/erpnext/crm/doctype/social_media_post/social_media_post.js
+++ b/erpnext/crm/doctype/social_media_post/social_media_post.js
@@ -1,67 +1,139 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Social Media Post', {
- validate: function(frm){
- if (frm.doc.twitter === 0 && frm.doc.linkedin === 0){
- frappe.throw(__("Select atleast one Social Media from Share on."))
- }
- if (frm.doc.scheduled_time) {
- let scheduled_time = new Date(frm.doc.scheduled_time);
- let date_time = new Date();
- if (scheduled_time.getTime() < date_time.getTime()){
- frappe.throw(__("Invalid Scheduled Time"));
- }
- }
- if (frm.doc.text?.length > 280){
- frappe.throw(__("Length Must be less than 280."))
- }
- },
- refresh: function(frm){
- if (frm.doc.docstatus === 1){
- if (frm.doc.post_status != "Posted"){
- add_post_btn(frm);
- }
- else if (frm.doc.post_status == "Posted"){
- frm.set_df_property('sheduled_time', 'read_only', 1);
- }
+ validate: function(frm) {
+ if (frm.doc.twitter === 0 && frm.doc.linkedin === 0) {
+ frappe.throw(__("Select atleast one Social Media Platform to Share on."));
+ }
+ if (frm.doc.scheduled_time) {
+ let scheduled_time = new Date(frm.doc.scheduled_time);
+ let date_time = new Date();
+ if (scheduled_time.getTime() < date_time.getTime()) {
+ frappe.throw(__("Scheduled Time must be a future time."));
+ }
+ }
+ frm.trigger('validate_tweet_length');
+ },
- let html='';
- if (frm.doc.twitter){
- let color = frm.doc.twitter_post_id ? "green" : "red";
- let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted";
- html += `
{{ message }}
+{{ message }}
+You don't have no upcoming holidays this {{ frequency }}.
+ {% endif %} +{% endif %} diff --git a/erpnext/templates/includes/rfq/rfq_items.html b/erpnext/templates/includes/rfq/rfq_items.html index caa15f386b..04cf922664 100644 --- a/erpnext/templates/includes/rfq/rfq_items.html +++ b/erpnext/templates/includes/rfq/rfq_items.html @@ -1,4 +1,4 @@ -{% from "erpnext/templates/includes/rfq/rfq_macros.html" import item_name_and_description %} +{% from "templates/includes/rfq/rfq_macros.html" import item_name_and_description %} {% for d in doc.items %}