fix: conflicts

This commit is contained in:
Anupam
2021-08-26 00:15:35 +05:30
1323 changed files with 3846 additions and 4944 deletions

View File

@@ -13,3 +13,6 @@
# This commit just changes spaces to tabs for indentation in some files # This commit just changes spaces to tabs for indentation in some files
5f473611bd6ed57703716244a054d3fb5ba9cd23 5f473611bd6ed57703716244a054d3fb5ba9cd23
# Whitespace fix throughout codebase
4551d7d6029b6f587f6c99d4f8df5519241c6a86

View File

@@ -450,5 +450,3 @@ def get_deferred_booking_accounts(doctype, voucher_detail_no, dr_or_cr):
return debit_account return debit_account
else: else:
return credit_account return credit_account

View File

@@ -113,5 +113,3 @@ def disable_dimension():
dimension2 = frappe.get_doc("Accounting Dimension", "Location") dimension2 = frappe.get_doc("Accounting Dimension", "Location")
dimension2.disabled = 1 dimension2.disabled = 1
dimension2.save() dimension2.save()

View File

@@ -22,6 +22,10 @@ class BankTransaction(StatusUpdater):
self.clear_linked_payment_entries() self.clear_linked_payment_entries()
self.set_status(update=True) 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): def update_allocations(self):
if self.payment_entries: if self.payment_entries:
allocated_amount = reduce(lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries]) allocated_amount = reduce(lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries])
@@ -42,20 +46,45 @@ class BankTransaction(StatusUpdater):
self.reload() self.reload()
def clear_linked_payment_entries(self): def clear_linked_payment_entries(self, for_cancel=False):
for payment_entry in self.payment_entries: for payment_entry in self.payment_entries:
if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]: if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]:
self.clear_simple_entry(payment_entry) self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
elif payment_entry.payment_document == "Sales Invoice": 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): def clear_simple_entry(self, payment_entry, for_cancel=False):
frappe.db.set_value(payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", self.date) 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): clearance_date = self.date if not for_cancel else None
frappe.db.set_value("Sales Invoice Payment", dict(parenttype=payment_entry.payment_document, frappe.db.set_value(
parent=payment_entry.payment_entry), "clearance_date", self.date) 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): def get_total_allocated_amount(payment_entry):
return frappe.db.sql(""" return frappe.db.sql("""
@@ -105,4 +134,3 @@ def unclear_reference_payment(doctype, docname):
frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None) frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None)
return doc.payment_entry return doc.payment_entry

View File

@@ -4,10 +4,12 @@
frappe.listview_settings['Bank Transaction'] = { frappe.listview_settings['Bank Transaction'] = {
add_fields: ["unallocated_amount"], add_fields: ["unallocated_amount"],
get_indicator: function(doc) { get_indicator: function(doc) {
if(flt(doc.unallocated_amount)>0) { if(doc.docstatus == 2) {
return [__("Unreconciled"), "orange", "unallocated_amount,>,0"]; return [__("Cancelled"), "red", "docstatus,=,2"];
} else if(flt(doc.unallocated_amount)<=0) { } else if(flt(doc.unallocated_amount)<=0) {
return [__("Reconciled"), "green", "unallocated_amount,=,0"]; return [__("Reconciled"), "green", "unallocated_amount,=,0"];
} else if(flt(doc.unallocated_amount)>0) {
return [__("Unreconciled"), "orange", "unallocated_amount,>,0"];
} }
} }
}; };

View File

@@ -25,7 +25,8 @@ class TestBankTransaction(unittest.TestCase):
def tearDownClass(cls): def tearDownClass(cls):
for bt in frappe.get_all("Bank Transaction"): for bt in frappe.get_all("Bank Transaction"):
doc = frappe.get_doc("Bank Transaction", bt.name) doc = frappe.get_doc("Bank Transaction", bt.name)
doc.cancel() if doc.docstatus == 1:
doc.cancel()
doc.delete() doc.delete()
# Delete directly in DB to avoid validation errors for countries not allowing deletion # 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") clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date")
self.assertTrue(clearance_date is not None) 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 # Check if ERPNext can correctly filter a linked payments based on the debit/credit amount
def test_debit_credit_output(self): 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")) bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"))

View File

@@ -18,5 +18,3 @@ class CashFlowMapping(Document):
frappe._('You can only select a maximum of one option from the list of check boxes.'), frappe._('You can only select a maximum of one option from the list of check boxes.'),
title='Error' title='Error'
) )

View File

@@ -62,6 +62,3 @@ def create_cost_center(**args):
cc.is_group = args.is_group or 0 cc.is_group = args.is_group or 0
cc.parent_cost_center = args.parent_cost_center or "_Test Company - _TC" cc.parent_cost_center = args.parent_cost_center or "_Test Company - _TC"
cc.insert() cc.insert()

View File

@@ -124,6 +124,3 @@ class TestCouponCode(unittest.TestCase):
so.submit() so.submit()
self.assertEqual(frappe.db.get_value("Coupon Code", "SAVE30", "used"), 1) self.assertEqual(frappe.db.get_value("Coupon Code", "SAVE30", "used"), 1)

View File

@@ -9,19 +9,8 @@ import frappe
import unittest import unittest
class TestFinanceBook(unittest.TestCase): class TestFinanceBook(unittest.TestCase):
def create_finance_book(self):
if not frappe.db.exists("Finance Book", "_Test Finance Book"):
finance_book = frappe.get_doc({
"doctype": "Finance Book",
"finance_book_name": "_Test Finance Book"
}).insert()
else:
finance_book = frappe.get_doc("Finance Book", "_Test Finance Book")
return finance_book
def test_finance_book(self): def test_finance_book(self):
finance_book = self.create_finance_book() finance_book = create_finance_book()
# create jv entry # create jv entry
jv = make_journal_entry("_Test Bank - _TC", jv = make_journal_entry("_Test Bank - _TC",
@@ -41,3 +30,14 @@ class TestFinanceBook(unittest.TestCase):
for gl_entry in gl_entries: for gl_entry in gl_entries:
self.assertEqual(gl_entry.finance_book, finance_book.name) self.assertEqual(gl_entry.finance_book, finance_book.name)
def create_finance_book():
if not frappe.db.exists("Finance Book", "_Test Finance Book"):
finance_book = frappe.get_doc({
"doctype": "Finance Book",
"finance_book_name": "_Test Finance Book"
}).insert()
else:
finance_book = frappe.get_doc("Finance Book", "_Test Finance Book")
return finance_book

View File

@@ -39,4 +39,3 @@ class ModeofPayment(Document):
message = "POS Profile " + frappe.bold(", ".join(pos_profiles)) + " contains \ message = "POS Profile " + frappe.bold(", ".join(pos_profiles)) + " contains \
Mode of Payment " + frappe.bold(str(self.name)) + ". Please remove them to disable this mode." Mode of Payment " + frappe.bold(str(self.name)) + ". Please remove them to disable this mode."
frappe.throw(_(message), title="Not Allowed") frappe.throw(_(message), title="Not Allowed")

View File

@@ -241,4 +241,3 @@ def get_temporary_opening_account(company=None):
frappe.throw(_("Please add a Temporary Opening account in Chart of Accounts")) frappe.throw(_("Please add a Temporary Opening account in Chart of Accounts"))
return accounts[0].name return accounts[0].name

View File

@@ -533,8 +533,8 @@ frappe.ui.form.on('Payment Entry', {
source_exchange_rate: function(frm) { source_exchange_rate: function(frm) {
if (frm.doc.paid_amount) { if (frm.doc.paid_amount) {
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate)); frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
if(!frm.set_paid_amount_based_on_received_amount && // target exchange rate should always be same as source if both account currencies is same
(frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency)) { if(frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate); frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate);
frm.set_value("base_received_amount", frm.doc.base_paid_amount); frm.set_value("base_received_amount", frm.doc.base_paid_amount);
} }

View File

@@ -55,14 +55,17 @@ class PaymentEntry(AccountsController):
self.validate_mandatory() self.validate_mandatory()
self.validate_reference_documents() self.validate_reference_documents()
self.set_tax_withholding() self.set_tax_withholding()
self.apply_taxes()
self.set_amounts() self.set_amounts()
self.validate_amounts()
self.apply_taxes()
self.set_amounts_after_tax()
self.clear_unallocated_reference_document_rows() self.clear_unallocated_reference_document_rows()
self.validate_payment_against_negative_invoice() self.validate_payment_against_negative_invoice()
self.validate_transaction_reference() self.validate_transaction_reference()
self.set_title() self.set_title()
self.set_remarks() self.set_remarks()
self.validate_duplicate_entry() self.validate_duplicate_entry()
self.validate_payment_type_with_outstanding()
self.validate_allocated_amount() self.validate_allocated_amount()
self.validate_paid_invoices() self.validate_paid_invoices()
self.ensure_supplier_is_not_blocked() self.ensure_supplier_is_not_blocked()
@@ -118,6 +121,11 @@ class PaymentEntry(AccountsController):
if not self.get(field): if not self.get(field):
self.set(field, bank_data.account) self.set(field, bank_data.account)
def validate_payment_type_with_outstanding(self):
total_outstanding = sum(d.allocated_amount for d in self.get('references'))
if total_outstanding < 0 and self.party_type == 'Customer' and self.payment_type == 'Receive':
frappe.throw(_("Cannot receive from customer against negative outstanding"), title=_("Incorrect Payment Type"))
def validate_allocated_amount(self): def validate_allocated_amount(self):
for d in self.get("references"): for d in self.get("references"):
if (flt(d.allocated_amount))> 0: if (flt(d.allocated_amount))> 0:
@@ -236,7 +244,9 @@ class PaymentEntry(AccountsController):
self.company_currency, self.posting_date) self.company_currency, self.posting_date)
def set_target_exchange_rate(self, ref_doc=None): def set_target_exchange_rate(self, ref_doc=None):
if self.paid_to and not self.target_exchange_rate: if self.paid_from_account_currency == self.paid_to_account_currency:
self.target_exchange_rate = self.source_exchange_rate
elif self.paid_to and not self.target_exchange_rate:
if ref_doc: if ref_doc:
if self.paid_to_account_currency == ref_doc.currency: if self.paid_to_account_currency == ref_doc.currency:
self.target_exchange_rate = ref_doc.get("exchange_rate") self.target_exchange_rate = ref_doc.get("exchange_rate")
@@ -468,13 +478,22 @@ class PaymentEntry(AccountsController):
def set_amounts(self): def set_amounts(self):
self.set_received_amount() self.set_received_amount()
self.set_amounts_in_company_currency() self.set_amounts_in_company_currency()
self.set_amounts_after_tax()
self.set_total_allocated_amount() self.set_total_allocated_amount()
self.set_unallocated_amount() self.set_unallocated_amount()
self.set_difference_amount() self.set_difference_amount()
def validate_amounts(self):
self.validate_received_amount()
def validate_received_amount(self):
if self.paid_from_account_currency == self.paid_to_account_currency:
if self.paid_amount != self.received_amount:
frappe.throw(_("Received Amount cannot be greater than Paid Amount"))
def set_received_amount(self): def set_received_amount(self):
self.base_received_amount = self.base_paid_amount self.base_received_amount = self.base_paid_amount
if self.paid_from_account_currency == self.paid_to_account_currency:
self.received_amount = self.paid_amount
def set_amounts_after_tax(self): def set_amounts_after_tax(self):
applicable_tax = 0 applicable_tax = 0

View File

@@ -107,7 +107,7 @@ class TestPaymentEntry(unittest.TestCase):
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC") pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1" pe.reference_no = "1"
pe.reference_date = "2016-01-01" pe.reference_date = "2016-01-01"
pe.target_exchange_rate = 50 pe.source_exchange_rate = 50
pe.insert() pe.insert()
pe.submit() pe.submit()
@@ -154,7 +154,7 @@ class TestPaymentEntry(unittest.TestCase):
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC") pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1" pe.reference_no = "1"
pe.reference_date = "2016-01-01" pe.reference_date = "2016-01-01"
pe.target_exchange_rate = 50 pe.source_exchange_rate = 50
pe.insert() pe.insert()
pe.submit() pe.submit()
@@ -491,7 +491,7 @@ class TestPaymentEntry(unittest.TestCase):
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC") pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1" pe.reference_no = "1"
pe.reference_date = "2016-01-01" pe.reference_date = "2016-01-01"
pe.target_exchange_rate = 55 pe.source_exchange_rate = 55
pe.append("deductions", { pe.append("deductions", {
"account": "_Test Exchange Gain/Loss - _TC", "account": "_Test Exchange Gain/Loss - _TC",

View File

@@ -50,9 +50,13 @@ class PeriodClosingVoucher(AccountsController):
.format(pce[0][0], self.posting_date)) .format(pce[0][0], self.posting_date))
def make_gl_entries(self): def make_gl_entries(self):
gl_entries = [] gl_entries = self.get_gl_entries()
net_pl_balance = 0 if gl_entries:
from erpnext.accounts.general_ledger import make_gl_entries
make_gl_entries(gl_entries)
def get_gl_entries(self):
gl_entries = []
pl_accounts = self.get_pl_balances() pl_accounts = self.get_pl_balances()
for acc in pl_accounts: for acc in pl_accounts:
@@ -60,6 +64,7 @@ class PeriodClosingVoucher(AccountsController):
gl_entries.append(self.get_gl_dict({ gl_entries.append(self.get_gl_dict({
"account": acc.account, "account": acc.account,
"cost_center": acc.cost_center, "cost_center": acc.cost_center,
"finance_book": acc.finance_book,
"account_currency": acc.account_currency, "account_currency": acc.account_currency,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) < 0 else 0, "debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) < 0 else 0,
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0, "debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
@@ -67,35 +72,13 @@ class PeriodClosingVoucher(AccountsController):
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0 "credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0
}, item=acc)) }, item=acc))
net_pl_balance += flt(acc.bal_in_company_currency) if gl_entries:
gle_for_net_pl_bal = self.get_pnl_gl_entry(pl_accounts)
gl_entries += gle_for_net_pl_bal
if net_pl_balance: return gl_entries
if self.cost_center_wise_pnl:
costcenter_wise_gl_entries = self.get_costcenter_wise_pnl_gl_entries(pl_accounts)
gl_entries += costcenter_wise_gl_entries
else:
gl_entry = self.get_pnl_gl_entry(net_pl_balance)
gl_entries.append(gl_entry)
from erpnext.accounts.general_ledger import make_gl_entries def get_pnl_gl_entry(self, pl_accounts):
make_gl_entries(gl_entries)
def get_pnl_gl_entry(self, net_pl_balance):
cost_center = frappe.db.get_value("Company", self.company, "cost_center")
gl_entry = self.get_gl_dict({
"account": self.closing_account_head,
"debit_in_account_currency": abs(net_pl_balance) if net_pl_balance > 0 else 0,
"debit": abs(net_pl_balance) if net_pl_balance > 0 else 0,
"credit_in_account_currency": abs(net_pl_balance) if net_pl_balance < 0 else 0,
"credit": abs(net_pl_balance) if net_pl_balance < 0 else 0,
"cost_center": cost_center
})
self.update_default_dimensions(gl_entry)
return gl_entry
def get_costcenter_wise_pnl_gl_entries(self, pl_accounts):
company_cost_center = frappe.db.get_value("Company", self.company, "cost_center") company_cost_center = frappe.db.get_value("Company", self.company, "cost_center")
gl_entries = [] gl_entries = []
@@ -104,6 +87,7 @@ class PeriodClosingVoucher(AccountsController):
gl_entry = self.get_gl_dict({ gl_entry = self.get_gl_dict({
"account": self.closing_account_head, "account": self.closing_account_head,
"cost_center": acc.cost_center or company_cost_center, "cost_center": acc.cost_center or company_cost_center,
"finance_book": acc.finance_book,
"account_currency": acc.account_currency, "account_currency": acc.account_currency,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0, "debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0,
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0, "debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
@@ -130,7 +114,7 @@ class PeriodClosingVoucher(AccountsController):
def get_pl_balances(self): def get_pl_balances(self):
"""Get balance for dimension-wise pl accounts""" """Get balance for dimension-wise pl accounts"""
dimension_fields = ['t1.cost_center'] dimension_fields = ['t1.cost_center', 't1.finance_book']
self.accounting_dimensions = get_accounting_dimensions() self.accounting_dimensions = get_accounting_dimensions()
for dimension in self.accounting_dimensions: for dimension in self.accounting_dimensions:

View File

@@ -8,6 +8,7 @@ import frappe
from frappe.utils import flt, today from frappe.utils import flt, today
from erpnext.accounts.utils import get_fiscal_year, now from erpnext.accounts.utils import get_fiscal_year, now
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
class TestPeriodClosingVoucher(unittest.TestCase): class TestPeriodClosingVoucher(unittest.TestCase):
@@ -118,6 +119,58 @@ class TestPeriodClosingVoucher(unittest.TestCase):
self.assertTrue(pcv_gle, expected_gle) self.assertTrue(pcv_gle, expected_gle)
def test_period_closing_with_finance_book_entries(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
company = create_company()
surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1")
create_sales_invoice(
company=company,
income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC",
cost_center=cost_center,
rate=400,
debit_to="Debtors - TPC"
)
jv = make_journal_entry(
account1="Cash - TPC",
account2="Sales - TPC",
amount=400,
cost_center=cost_center,
posting_date=now()
)
jv.company = company
jv.finance_book = create_finance_book().name
jv.save()
jv.submit()
pcv = frappe.get_doc({
"transaction_date": today(),
"posting_date": today(),
"fiscal_year": get_fiscal_year(today())[0],
"company": company,
"closing_account_head": surplus_account,
"remarks": "Test",
"doctype": "Period Closing Voucher"
})
pcv.insert()
pcv.submit()
expected_gle = (
(surplus_account, 0.0, 400.0, ''),
(surplus_account, 0.0, 400.0, jv.finance_book),
('Sales - TPC', 400.0, 0.0, ''),
('Sales - TPC', 400.0, 0.0, jv.finance_book)
)
pcv_gle = frappe.db.sql("""
select account, debit, credit, finance_book from `tabGL Entry` where voucher_no=%s
""", (pcv.name))
self.assertTrue(pcv_gle, expected_gle)
def make_period_closing_voucher(self): def make_period_closing_voucher(self):
pcv = frappe.get_doc({ pcv = frappe.get_doc({
"doctype": "Period Closing Voucher", "doctype": "Period Closing Voucher",

View File

@@ -85,9 +85,15 @@ class TestPOSClosingEntry(unittest.TestCase):
pcv_doc.load_from_db() pcv_doc.load_from_db()
pcv_doc.cancel() pcv_doc.cancel()
si_doc.load_from_db()
cancelled_invoice = frappe.db.get_value(
'POS Invoice Merge Log', {'pos_closing_entry': pcv_doc.name},
'consolidated_invoice'
)
docstatus = frappe.db.get_value("Sales Invoice", cancelled_invoice, 'docstatus')
self.assertEqual(docstatus, 2)
pos_inv1.load_from_db() pos_inv1.load_from_db()
self.assertEqual(si_doc.docstatus, 2)
self.assertEqual(pos_inv1.status, 'Paid') self.assertEqual(pos_inv1.status, 'Paid')

View File

@@ -16,7 +16,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
onload(doc) { onload(doc) {
super.onload(); super.onload();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log']; this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log', 'POS Closing Entry'];
if(doc.__islocal && doc.is_pos && frappe.get_route_str() !== 'point-of-sale') { if(doc.__islocal && doc.is_pos && frappe.get_route_str() !== 'point-of-sale') {
this.frm.script_manager.trigger("is_pos"); this.frm.script_manager.trigger("is_pos");
this.frm.refresh_fields(); this.frm.refresh_fields();
@@ -111,16 +111,12 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
} }
write_off_outstanding_amount_automatically() { write_off_outstanding_amount_automatically() {
if(cint(this.frm.doc.write_off_outstanding_amount_automatically)) { if (cint(this.frm.doc.write_off_outstanding_amount_automatically)) {
frappe.model.round_floats_in(this.frm.doc, ["grand_total", "paid_amount"]); frappe.model.round_floats_in(this.frm.doc, ["grand_total", "paid_amount"]);
// this will make outstanding amount 0 // this will make outstanding amount 0
this.frm.set_value("write_off_amount", this.frm.set_value("write_off_amount",
flt(this.frm.doc.grand_total - this.frm.doc.paid_amount - this.frm.doc.total_advance, precision("write_off_amount")) flt(this.frm.doc.grand_total - this.frm.doc.paid_amount - this.frm.doc.total_advance, precision("write_off_amount"))
); );
this.frm.toggle_enable("write_off_amount", false);
} else {
this.frm.toggle_enable("write_off_amount", true);
} }
this.calculate_outstanding_amount(false); this.calculate_outstanding_amount(false);

View File

@@ -99,6 +99,7 @@
"loyalty_redemption_account", "loyalty_redemption_account",
"loyalty_redemption_cost_center", "loyalty_redemption_cost_center",
"section_break_49", "section_break_49",
"coupon_code",
"apply_discount_on", "apply_discount_on",
"base_discount_amount", "base_discount_amount",
"column_break_51", "column_break_51",
@@ -595,7 +596,8 @@
{ {
"fieldname": "scan_barcode", "fieldname": "scan_barcode",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Scan Barcode" "label": "Scan Barcode",
"options": "Barcode"
}, },
{ {
"allow_bulk_edit": 1, "allow_bulk_edit": 1,
@@ -1182,7 +1184,8 @@
"label": "Write Off Amount", "label": "Write Off Amount",
"no_copy": 1, "no_copy": 1,
"options": "currency", "options": "currency",
"print_hide": 1 "print_hide": 1,
"read_only_depends_on": "eval: doc.write_off_outstanding_amount_automatically"
}, },
{ {
"fieldname": "base_write_off_amount", "fieldname": "base_write_off_amount",
@@ -1548,12 +1551,20 @@
"no_copy": 1, "no_copy": 1,
"options": "Sales Invoice", "options": "Sales Invoice",
"read_only": 1 "read_only": 1
},
{
"depends_on": "coupon_code",
"fieldname": "coupon_code",
"fieldtype": "Link",
"label": "Coupon Code",
"options": "Coupon Code",
"print_hide": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-07-29 13:37:20.636171", "modified": "2021-08-24 18:19:20.728433",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice", "name": "POS Invoice",

View File

@@ -44,6 +44,9 @@ class POSInvoice(SalesInvoice):
self.validate_pos() self.validate_pos()
self.validate_payment_amount() self.validate_payment_amount()
self.validate_loyalty_transaction() self.validate_loyalty_transaction()
if self.coupon_code:
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
validate_coupon_code(self.coupon_code)
def on_submit(self): def on_submit(self):
# create the loyalty point ledger entry if the customer is enrolled in any loyalty program # create the loyalty point ledger entry if the customer is enrolled in any loyalty program
@@ -58,6 +61,10 @@ class POSInvoice(SalesInvoice):
self.check_phone_payments() self.check_phone_payments()
self.set_status(update=True) self.set_status(update=True)
if self.coupon_code:
from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count
update_coupon_code_count(self.coupon_code,'used')
def before_cancel(self): def before_cancel(self):
if self.consolidated_invoice and frappe.db.get_value('Sales Invoice', self.consolidated_invoice, 'docstatus') == 1: if self.consolidated_invoice and frappe.db.get_value('Sales Invoice', self.consolidated_invoice, 'docstatus') == 1:
pos_closing_entry = frappe.get_all( pos_closing_entry = frappe.get_all(
@@ -84,6 +91,10 @@ class POSInvoice(SalesInvoice):
against_psi_doc.delete_loyalty_point_entry() against_psi_doc.delete_loyalty_point_entry()
against_psi_doc.make_loyalty_point_entry() against_psi_doc.make_loyalty_point_entry()
if self.coupon_code:
from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count
update_coupon_code_count(self.coupon_code,'cancelled')
def check_phone_payments(self): def check_phone_payments(self):
for pay in self.payments: for pay in self.payments:
if pay.type == "Phone" and pay.amount >= 0: if pay.type == "Phone" and pay.amount >= 0:
@@ -127,7 +138,7 @@ class POSInvoice(SalesInvoice):
.format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable")) .format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable"))
def validate_stock_availablility(self): def validate_stock_availablility(self):
if self.is_return: if self.is_return or self.docstatus != 1:
return return
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock') allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')

View File

@@ -320,7 +320,8 @@ class TestPOSInvoice(unittest.TestCase):
pos2.get("items")[0].serial_no = serial_nos[0] pos2.get("items")[0].serial_no = serial_nos[0]
pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000}) pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
self.assertRaises(frappe.ValidationError, pos2.insert) pos2.insert()
self.assertRaises(frappe.ValidationError, pos2.submit)
def test_delivered_serialized_item_transaction(self): def test_delivered_serialized_item_transaction(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
@@ -348,7 +349,8 @@ class TestPOSInvoice(unittest.TestCase):
pos2.get("items")[0].serial_no = serial_nos[0] pos2.get("items")[0].serial_no = serial_nos[0]
pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000}) pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
self.assertRaises(frappe.ValidationError, pos2.insert) pos2.insert()
self.assertRaises(frappe.ValidationError, pos2.submit)
def test_loyalty_points(self): def test_loyalty_points(self):
from erpnext.accounts.doctype.loyalty_program.test_loyalty_program import create_records from erpnext.accounts.doctype.loyalty_program.test_loyalty_program import create_records

View File

@@ -147,4 +147,3 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
frappe.set_user("Administrator") frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`") frappe.db.sql("delete from `tabPOS Invoice`")

View File

@@ -198,12 +198,19 @@ def apply_pricing_rule(args, doc=None):
set_serial_nos_based_on_fifo = frappe.db.get_single_value("Stock Settings", set_serial_nos_based_on_fifo = frappe.db.get_single_value("Stock Settings",
"automatically_set_serial_nos_based_on_fifo") "automatically_set_serial_nos_based_on_fifo")
item_code_list = tuple(item.get('item_code') for item in item_list)
query_items = frappe.get_all('Item', fields=['item_code','has_serial_no'], filters=[['item_code','in',item_code_list]],as_list=1)
serialized_items = dict()
for item_code, val in query_items:
serialized_items.setdefault(item_code, val)
for item in item_list: for item in item_list:
args_copy = copy.deepcopy(args) args_copy = copy.deepcopy(args)
args_copy.update(item) args_copy.update(item)
data = get_pricing_rule_for_item(args_copy, item.get('price_list_rate'), doc=doc) data = get_pricing_rule_for_item(args_copy, item.get('price_list_rate'), doc=doc)
out.append(data) out.append(data)
if not item.get("serial_no") and set_serial_nos_based_on_fifo and not args.get('is_return'):
if serialized_items.get(item.get('item_code')) and not item.get("serial_no") and set_serial_nos_based_on_fifo and not args.get('is_return'):
out[0].update(get_serial_no_for_item(args_copy)) out[0].update(get_serial_no_for_item(args_copy))
return out return out

View File

@@ -26,4 +26,3 @@ QUnit.test("test pricing rule", function(assert) {
() => done() () => done()
]); ]);
}); });

View File

@@ -106,7 +106,6 @@
"depends_on": "eval:doc.rate_or_discount==\"Rate\"", "depends_on": "eval:doc.rate_or_discount==\"Rate\"",
"fieldname": "rate", "fieldname": "rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1,
"label": "Rate" "label": "Rate"
}, },
{ {
@@ -170,7 +169,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-03-07 11:56:23.424137", "modified": "2021-08-19 15:49:29.598727",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Promotional Scheme Price Discount", "name": "Promotional Scheme Price Discount",

View File

@@ -668,8 +668,7 @@
"fieldname": "scan_barcode", "fieldname": "scan_barcode",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Scan Barcode", "label": "Scan Barcode",
"show_days": 1, "options": "Barcode"
"show_seconds": 1
}, },
{ {
"allow_bulk_edit": 1, "allow_bulk_edit": 1,
@@ -1715,7 +1714,7 @@
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-08-07 17:53:14.351439", "modified": "2021-08-17 20:16:12.737743",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@@ -72,4 +72,3 @@ QUnit.test("test purchase invoice", function(assert) {
() => done() () => done()
]); ]);
}); });

View File

@@ -26,4 +26,3 @@ QUnit.test("test sales taxes and charges template", function(assert) {
() => done() () => done()
]); ]);
}); });

View File

@@ -1,8 +1,6 @@
{% include "erpnext/regional/india/taxes.js" %} {% include "erpnext/regional/india/taxes.js" %}
{% include "erpnext/regional/india/e_invoice/einvoice.js" %}
erpnext.setup_auto_gst_taxation('Sales Invoice'); erpnext.setup_auto_gst_taxation('Sales Invoice');
erpnext.setup_einvoice_actions('Sales Invoice')
frappe.ui.form.on("Sales Invoice", { frappe.ui.form.on("Sales Invoice", {
setup: function(frm) { setup: function(frm) {

View File

@@ -36,139 +36,4 @@ frappe.listview_settings['Sales Invoice'].onload = function (list_view) {
}; };
list_view.page.add_actions_menu_item(__('Generate E-Way Bill JSON'), action, false); list_view.page.add_actions_menu_item(__('Generate E-Way Bill JSON'), action, false);
const generate_irns = () => {
const docnames = list_view.get_checked_items(true);
if (docnames && docnames.length) {
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.generate_einvoices',
args: { docnames },
freeze: true,
freeze_message: __('Generating E-Invoices...')
});
} else {
frappe.msgprint({
message: __('Please select at least one sales invoice to generate IRN'),
title: __('No Invoice Selected'),
indicator: 'red'
});
}
};
const cancel_irns = () => {
const docnames = list_view.get_checked_items(true);
const fields = [
{
"label": "Reason",
"fieldname": "reason",
"fieldtype": "Select",
"reqd": 1,
"default": "1-Duplicate",
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
},
{
"label": "Remark",
"fieldname": "remark",
"fieldtype": "Data",
"reqd": 1
}
];
const d = new frappe.ui.Dialog({
title: __("Cancel IRN"),
fields: fields,
primary_action: function() {
const data = d.get_values();
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_irns',
args: {
doctype: list_view.doctype,
docnames,
reason: data.reason.split('-')[0],
remark: data.remark
},
freeze: true,
freeze_message: __('Cancelling E-Invoices...'),
});
d.hide();
},
primary_action_label: __('Submit')
});
d.show();
};
let einvoicing_enabled = false;
frappe.db.get_single_value("E Invoice Settings", "enable").then(enabled => {
einvoicing_enabled = enabled;
});
list_view.$result.on("change", "input[type=checkbox]", () => {
if (einvoicing_enabled) {
const docnames = list_view.get_checked_items(true);
// show/hide e-invoicing actions when no sales invoices are checked
if (docnames && docnames.length) {
// prevent adding actions twice if e-invoicing action group already exists
if (list_view.page.get_inner_group_button(__('E-Invoicing')).length == 0) {
list_view.page.add_inner_button(__('Generate IRNs'), generate_irns, __('E-Invoicing'));
list_view.page.add_inner_button(__('Cancel IRNs'), cancel_irns, __('E-Invoicing'));
}
} else {
list_view.page.remove_inner_button(__('Generate IRNs'), __('E-Invoicing'));
list_view.page.remove_inner_button(__('Cancel IRNs'), __('E-Invoicing'));
}
}
});
frappe.realtime.on("bulk_einvoice_generation_complete", (data) => {
const { failures, user, invoices } = data;
if (invoices.length != failures.length) {
frappe.msgprint({
message: __('{0} e-invoices generated successfully', [invoices.length]),
title: __('Bulk E-Invoice Generation Complete'),
indicator: 'orange'
});
}
if (failures && failures.length && user == frappe.session.user) {
let message = `
Failed to generate IRNs for following ${failures.length} sales invoices:
<ul style="padding-left: 20px; padding-top: 5px;">
${failures.map(d => `<li>${d.docname}</li>`).join('')}
</ul>
`;
frappe.msgprint({
message: message,
title: __('Bulk E-Invoice Generation Complete'),
indicator: 'orange'
});
}
});
frappe.realtime.on("bulk_einvoice_cancellation_complete", (data) => {
const { failures, user, invoices } = data;
if (invoices.length != failures.length) {
frappe.msgprint({
message: __('{0} e-invoices cancelled successfully', [invoices.length]),
title: __('Bulk E-Invoice Cancellation Complete'),
indicator: 'orange'
});
}
if (failures && failures.length && user == frappe.session.user) {
let message = `
Failed to cancel IRNs for following ${failures.length} sales invoices:
<ul style="padding-left: 20px; padding-top: 5px;">
${failures.map(d => `<li>${d.docname}</li>`).join('')}
</ul>
`;
frappe.msgprint({
message: message,
title: __('Bulk E-Invoice Cancellation Complete'),
indicator: 'orange'
});
}
});
}; };

View File

@@ -154,9 +154,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
return return
} }
$.each(doc["items"], function(i, row) { doc.items.forEach((row) => {
if(row.delivery_note) frappe.model.clear_doc("Delivery Note", row.delivery_note) if(row.delivery_note) frappe.model.clear_doc("Delivery Note", row.delivery_note)
}) });
} }
set_default_print_format() { set_default_print_format() {
@@ -324,16 +324,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
} }
write_off_outstanding_amount_automatically() { write_off_outstanding_amount_automatically() {
if(cint(this.frm.doc.write_off_outstanding_amount_automatically)) { if (cint(this.frm.doc.write_off_outstanding_amount_automatically)) {
frappe.model.round_floats_in(this.frm.doc, ["grand_total", "paid_amount"]); frappe.model.round_floats_in(this.frm.doc, ["grand_total", "paid_amount"]);
// this will make outstanding amount 0 // this will make outstanding amount 0
this.frm.set_value("write_off_amount", this.frm.set_value("write_off_amount",
flt(this.frm.doc.grand_total - this.frm.doc.paid_amount - this.frm.doc.total_advance, precision("write_off_amount")) flt(this.frm.doc.grand_total - this.frm.doc.paid_amount - this.frm.doc.total_advance, precision("write_off_amount"))
); );
this.frm.toggle_enable("write_off_amount", false);
} else {
this.frm.toggle_enable("write_off_amount", true);
} }
this.calculate_outstanding_amount(false); this.calculate_outstanding_amount(false);
@@ -450,13 +446,25 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
} }
currency() { currency() {
super.currency(); this._super();
$.each(cur_frm.doc.timesheets, function(i, d) { $.each(cur_frm.doc.timesheets, function(i, d) {
let row = frappe.get_doc(d.doctype, d.name) let row = frappe.get_doc(d.doctype, d.name)
set_timesheet_detail_rate(row.doctype, row.name, cur_frm.doc.currency, row.timesheet_detail) set_timesheet_detail_rate(row.doctype, row.name, cur_frm.doc.currency, row.timesheet_detail)
}); });
calculate_total_billing_amount(cur_frm) 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 // for backward compatibility: combine new and previous states
@@ -787,8 +795,6 @@ frappe.ui.form.on('Sales Invoice', {
if (frappe.boot.sysdefaults.country == 'India') unhide_field(['c_form_applicable', 'c_form_no']); if (frappe.boot.sysdefaults.country == 'India') unhide_field(['c_form_applicable', 'c_form_no']);
else hide_field(['c_form_applicable', 'c_form_no']); else hide_field(['c_form_applicable', 'c_form_no']);
frm.toggle_enable("write_off_amount", !!!cint(doc.write_off_outstanding_amount_automatically));
frm.refresh_fields(); frm.refresh_fields();
}, },
@@ -980,9 +986,9 @@ var calculate_total_billing_amount = function(frm) {
doc.total_billing_amount = 0.0 doc.total_billing_amount = 0.0
if (doc.timesheets) { if (doc.timesheets) {
$.each(doc.timesheets, function(index, data){ doc.timesheets.forEach((d) => {
doc.total_billing_amount += flt(data.billing_amount) doc.total_billing_amount += flt(d.billing_amount)
}) });
} }
refresh_field('total_billing_amount') refresh_field('total_billing_amount')

View File

@@ -247,7 +247,7 @@
"depends_on": "customer", "depends_on": "customer",
"fetch_from": "customer.customer_name", "fetch_from": "customer.customer_name",
"fieldname": "customer_name", "fieldname": "customer_name",
"fieldtype": "Data", "fieldtype": "Small Text",
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"in_global_search": 1, "in_global_search": 1,
@@ -694,7 +694,9 @@
"fieldtype": "Data", "fieldtype": "Data",
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "Scan Barcode" "label": "Scan Barcode",
"length": 1,
"options": "Barcode"
}, },
{ {
"allow_bulk_edit": 1, "allow_bulk_edit": 1,
@@ -1058,6 +1060,7 @@
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "Apply Additional Discount On", "label": "Apply Additional Discount On",
"length": 15,
"options": "\nGrand Total\nNet Total", "options": "\nGrand Total\nNet Total",
"print_hide": 1 "print_hide": 1
}, },
@@ -1144,7 +1147,7 @@
{ {
"description": "In Words will be visible once you save the Sales Invoice.", "description": "In Words will be visible once you save the Sales Invoice.",
"fieldname": "base_in_words", "fieldname": "base_in_words",
"fieldtype": "Data", "fieldtype": "Small Text",
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "In Words (Company Currency)", "label": "In Words (Company Currency)",
@@ -1204,7 +1207,7 @@
}, },
{ {
"fieldname": "in_words", "fieldname": "in_words",
"fieldtype": "Data", "fieldtype": "Small Text",
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "In Words", "label": "In Words",
@@ -1443,7 +1446,8 @@
"label": "Write Off Amount", "label": "Write Off Amount",
"no_copy": 1, "no_copy": 1,
"options": "currency", "options": "currency",
"print_hide": 1 "print_hide": 1,
"read_only_depends_on": "eval:doc.write_off_outstanding_amount_automatically"
}, },
{ {
"fieldname": "base_write_off_amount", "fieldname": "base_write_off_amount",
@@ -1556,6 +1560,7 @@
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "Print Language", "label": "Print Language",
"length": 6,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@@ -1643,6 +1648,7 @@
"hide_seconds": 1, "hide_seconds": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Status", "label": "Status",
"length": 30,
"no_copy": 1, "no_copy": 1,
"options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer", "options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer",
"print_hide": 1, "print_hide": 1,
@@ -1702,6 +1708,7 @@
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "Is Opening Entry", "label": "Is Opening Entry",
"length": 4,
"oldfieldname": "is_opening", "oldfieldname": "is_opening",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "No\nYes", "options": "No\nYes",
@@ -1713,6 +1720,7 @@
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "C-Form Applicable", "label": "C-Form Applicable",
"length": 4,
"no_copy": 1, "no_copy": 1,
"options": "No\nYes", "options": "No\nYes",
"print_hide": 1 "print_hide": 1
@@ -1934,6 +1942,7 @@
"description": "Unrealized Profit / Loss account for intra-company transfers", "description": "Unrealized Profit / Loss account for intra-company transfers",
"fieldname": "unrealized_profit_loss_account", "fieldname": "unrealized_profit_loss_account",
"fieldtype": "Link", "fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Unrealized Profit / Loss Account", "label": "Unrealized Profit / Loss Account",
"options": "Account" "options": "Account"
}, },
@@ -2012,7 +2021,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2021-08-17 19:00:32.230701", "modified": "2021-08-25 14:46:05.279588",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@@ -285,8 +285,6 @@ class SalesInvoice(SellingController):
def before_cancel(self): def before_cancel(self):
self.check_if_consolidated_invoice() self.check_if_consolidated_invoice()
super(SalesInvoice, self).before_cancel()
self.update_time_sheet(None) self.update_time_sheet(None)
def on_cancel(self): def on_cancel(self):

View File

@@ -26,6 +26,7 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
from erpnext.stock.utils import get_incoming_rate from erpnext.stock.utils import get_incoming_rate
from erpnext.accounts.utils import PaymentEntryUnlinkError
class TestSalesInvoice(unittest.TestCase): class TestSalesInvoice(unittest.TestCase):
def make(self): def make(self):
@@ -136,7 +137,7 @@ class TestSalesInvoice(unittest.TestCase):
pe.paid_to_account_currency = si.currency pe.paid_to_account_currency = si.currency
pe.source_exchange_rate = 1 pe.source_exchange_rate = 1
pe.target_exchange_rate = 1 pe.target_exchange_rate = 1
pe.paid_amount = si.grand_total pe.paid_amount = si.outstanding_amount
pe.insert() pe.insert()
pe.submit() pe.submit()
@@ -145,6 +146,42 @@ class TestSalesInvoice(unittest.TestCase):
self.assertRaises(frappe.LinkExistsError, si.cancel) self.assertRaises(frappe.LinkExistsError, si.cancel)
unlink_payment_on_cancel_of_invoice() unlink_payment_on_cancel_of_invoice()
def test_payment_entry_unlink_against_standalone_credit_note(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
si1 = create_sales_invoice(rate=1000)
si2 = create_sales_invoice(rate=300)
si3 = create_sales_invoice(qty=-1, rate=300, is_return=1)
pe = get_payment_entry("Sales Invoice", si1.name, bank_account="_Test Bank - _TC")
pe.append('references', {
'reference_doctype': 'Sales Invoice',
'reference_name': si2.name,
'total_amount': si2.grand_total,
'outstanding_amount': si2.outstanding_amount,
'allocated_amount': si2.outstanding_amount
})
pe.append('references', {
'reference_doctype': 'Sales Invoice',
'reference_name': si3.name,
'total_amount': si3.grand_total,
'outstanding_amount': si3.outstanding_amount,
'allocated_amount': si3.outstanding_amount
})
pe.reference_no = 'Test001'
pe.reference_date = nowdate()
pe.save()
pe.submit()
si2.load_from_db()
si2.cancel()
si1.load_from_db()
self.assertRaises(PaymentEntryUnlinkError, si1.cancel)
def test_sales_invoice_calculation_export_currency(self): def test_sales_invoice_calculation_export_currency(self):
si = frappe.copy_doc(test_records[2]) si = frappe.copy_doc(test_records[2])
si.currency = "USD" si.currency = "USD"
@@ -2014,7 +2051,7 @@ class TestSalesInvoice(unittest.TestCase):
data = get_ewb_data("Sales Invoice", [si.name]) data = get_ewb_data("Sales Invoice", [si.name])
self.assertEqual(data['version'], '1.0.1118') self.assertEqual(data['version'], '1.0.0421')
self.assertEqual(data['billLists'][0]['fromGstin'], '27AAECE4835E1ZR') self.assertEqual(data['billLists'][0]['fromGstin'], '27AAECE4835E1ZR')
self.assertEqual(data['billLists'][0]['fromTrdName'], '_Test Company') self.assertEqual(data['billLists'][0]['fromTrdName'], '_Test Company')
self.assertEqual(data['billLists'][0]['toTrdName'], '_Test Customer') self.assertEqual(data['billLists'][0]['toTrdName'], '_Test Customer')
@@ -2027,54 +2064,6 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(data['billLists'][0]['actualFromStateCode'],7) self.assertEqual(data['billLists'][0]['actualFromStateCode'],7)
self.assertEqual(data['billLists'][0]['fromStateCode'],27) self.assertEqual(data['billLists'][0]['fromStateCode'],27)
def test_einvoice_submission_without_irn(self):
# init
einvoice_settings = frappe.get_doc('E Invoice Settings')
einvoice_settings.enable = 1
einvoice_settings.applicable_from = nowdate()
einvoice_settings.append('credentials', {
'company': '_Test Company',
'gstin': '27AAECE4835E1ZR',
'username': 'test',
'password': 'test'
})
einvoice_settings.save()
country = frappe.flags.country
frappe.flags.country = 'India'
si = make_sales_invoice_for_ewaybill()
self.assertRaises(frappe.ValidationError, si.submit)
si.irn = 'test_irn'
si.submit()
# reset
einvoice_settings = frappe.get_doc('E Invoice Settings')
einvoice_settings.enable = 0
frappe.flags.country = country
def test_einvoice_json(self):
from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals
si = get_sales_invoice_for_e_invoice()
si.discount_amount = 100
si.save()
einvoice = make_einvoice(si)
self.assertTrue(einvoice['EwbDtls'])
validate_totals(einvoice)
si.apply_discount_on = 'Net Total'
si.save()
einvoice = make_einvoice(si)
validate_totals(einvoice)
[d.set('included_in_print_rate', 1) for d in si.taxes]
si.save()
einvoice = make_einvoice(si)
validate_totals(einvoice)
def test_item_tax_net_range(self): def test_item_tax_net_range(self):
item = create_item("T Shirt") item = create_item("T Shirt")

View File

@@ -40,4 +40,3 @@ QUnit.test("test sales Invoice", function(assert) {
() => done() () => done()
]); ]);
}); });

View File

@@ -33,4 +33,3 @@ QUnit.test("test sales invoice with margin", function(assert) {
() => done() () => done()
]); ]);
}); });

View File

@@ -54,4 +54,3 @@ QUnit.test("test sales Invoice with payment", function(assert) {
() => done() () => done()
]); ]);
}); });

View File

@@ -49,4 +49,3 @@ QUnit.test("test sales Invoice with payment request", function(assert) {
() => done() () => done()
]); ]);
}); });

View File

@@ -42,4 +42,3 @@ QUnit.test("test sales Invoice with serialize item", function(assert) {
() => done() () => done()
]); ]);
}); });

View File

@@ -26,4 +26,3 @@ QUnit.test("test sales taxes and charges template", function(assert) {
() => done() () => done()
]); ]);
}); });

View File

@@ -34,4 +34,3 @@ QUnit.test("test Shipping Rule", function(assert) {
() => done() () => done()
]); ]);
}); });

View File

@@ -34,4 +34,3 @@ QUnit.test("test Shipping Rule", function(assert) {
() => done() () => done()
]); ]);
}); });

View File

@@ -367,21 +367,25 @@ class Subscription(Document):
) )
# Discounts # Discounts
if self.additional_discount_percentage: if self.is_trialling():
invoice.additional_discount_percentage = self.additional_discount_percentage invoice.additional_discount_percentage = 100
else:
if self.additional_discount_percentage:
invoice.additional_discount_percentage = self.additional_discount_percentage
if self.additional_discount_amount: if self.additional_discount_amount:
invoice.discount_amount = self.additional_discount_amount invoice.discount_amount = self.additional_discount_amount
if self.additional_discount_percentage or self.additional_discount_amount: if self.additional_discount_percentage or self.additional_discount_amount:
discount_on = self.apply_additional_discount discount_on = self.apply_additional_discount
invoice.apply_discount_on = discount_on if discount_on else 'Grand Total' invoice.apply_discount_on = discount_on if discount_on else 'Grand Total'
# Subscription period # Subscription period
invoice.from_date = self.current_invoice_start invoice.from_date = self.current_invoice_start
invoice.to_date = self.current_invoice_end invoice.to_date = self.current_invoice_end
invoice.flags.ignore_mandatory = True invoice.flags.ignore_mandatory = True
invoice.save() invoice.save()
if self.submit_invoice: if self.submit_invoice:

View File

@@ -630,5 +630,3 @@ class TestSubscription(unittest.TestCase):
subscription.process() subscription.process()
self.assertEqual(len(subscription.invoices), 1) self.assertEqual(len(subscription.invoices), 1)

View File

@@ -241,13 +241,14 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
tds_amount = 0 tds_amount = 0
invoice_filters = { invoice_filters = {
'name': ('in', vouchers), 'name': ('in', vouchers),
'docstatus': 1 'docstatus': 1,
'apply_tds': 1
} }
field = 'sum(net_total)' field = 'sum(net_total)'
if not cint(tax_details.consider_party_ledger_amount): if cint(tax_details.consider_party_ledger_amount):
invoice_filters.update({'apply_tds': 1}) invoice_filters.pop('apply_tds', None)
field = 'sum(grand_total)' field = 'sum(grand_total)'
supp_credit_amt = frappe.db.get_value('Purchase Invoice', invoice_filters, field) or 0.0 supp_credit_amt = frappe.db.get_value('Purchase Invoice', invoice_filters, field) or 0.0

View File

@@ -145,6 +145,36 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in invoices: for d in invoices:
d.cancel() 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(): def cancel_invoices():
purchase_invoices = frappe.get_all("Purchase Invoice", { purchase_invoices = frappe.get_all("Purchase Invoice", {
'supplier': ['in', ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']], 'supplier': ['in', ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']],
@@ -220,7 +250,7 @@ def create_sales_invoice(**args):
def create_records(): def create_records():
# create a new suppliers # 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): if frappe.db.exists('Supplier', name):
continue continue

View File

@@ -101,7 +101,7 @@ def merge_similar_entries(gl_map, precision=None):
def check_if_in_list(gle, gl_map, dimensions=None): def check_if_in_list(gle, gl_map, dimensions=None):
account_head_fieldnames = ['voucher_detail_no', 'party', 'against_voucher', account_head_fieldnames = ['voucher_detail_no', 'party', 'against_voucher',
'cost_center', 'against_voucher_type', 'party_type', 'project'] 'cost_center', 'against_voucher_type', 'party_type', 'project', 'finance_book']
if dimensions: if dimensions:
account_head_fieldnames = account_head_fieldnames + dimensions account_head_fieldnames = account_head_fieldnames + dimensions

View File

@@ -286,6 +286,7 @@ def validate_party_gle_currency(party_type, party, company, party_account_curren
.format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency) .format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency)
def validate_party_accounts(doc): def validate_party_accounts(doc):
companies = [] companies = []
for account in doc.get("accounts"): for account in doc.get("accounts"):
@@ -446,6 +447,10 @@ def get_payment_terms_template(party_name, party_type, company=None):
return template return template
def validate_party_frozen_disabled(party_type, party_name): def validate_party_frozen_disabled(party_type, party_name):
if frappe.flags.ignore_party_validation:
return
if party_type and party_name: if party_type and party_name:
if party_type in ("Customer", "Supplier"): if party_type in ("Customer", "Supplier"):
party = frappe.get_cached_value(party_type, party_name, ["is_frozen", "disabled"], as_dict=True) party = frappe.get_cached_value(party_type, party_name, ["is_frozen", "disabled"], as_dict=True)

View File

@@ -1,162 +0,0 @@
{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%}
{%- set einvoice = json.loads(doc.signed_einvoice) -%}
<div class="page-break">
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
{% if letter_head and not no_letterhead %}
<div class="letter-head">{{ letter_head }}</div>
{% endif %}
<div class="print-heading">
<h2>E Invoice<br><small>{{ doc.name }}</small></h2>
</div>
</div>
{% if print_settings.repeat_header_footer %}
<div id="footer-html" class="visible-pdf">
{% if not no_letterhead and footer %}
<div class="letter-head-footer">
{{ footer }}
</div>
{% endif %}
<p class="text-center small page-number visible-pdf">
{{ _("Page {0} of {1}").format('<span class="page"></span>', '<span class="topage"></span>') }}
</p>
</div>
{% endif %}
<h5 class="font-bold" style="margin-top: 0px;">1. Transaction Details</h5>
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
<div class="col-xs-8 column-break">
<div class="row data-field">
<div class="col-xs-4"><label>IRN</label></div>
<div class="col-xs-8 value">{{ einvoice.Irn }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Ack. No</label></div>
<div class="col-xs-8 value">{{ einvoice.AckNo }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Ack. Date</label></div>
<div class="col-xs-8 value">{{ frappe.utils.format_datetime(einvoice.AckDt, "dd/MM/yyyy hh:mm:ss") }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Category</label></div>
<div class="col-xs-8 value">{{ einvoice.TranDtls.SupTyp }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Document Type</label></div>
<div class="col-xs-8 value">{{ einvoice.DocDtls.Typ }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Document No</label></div>
<div class="col-xs-8 value">{{ einvoice.DocDtls.No }}</div>
</div>
</div>
<div class="col-xs-4 column-break">
<img src="{{ doc.qrcode_image }}" width="175px" style="float: right;">
</div>
</div>
<h5 class="font-bold" style="margin-top: 15px; margin-bottom: 10px;">2. Party Details</h5>
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
{%- set seller = einvoice.SellerDtls -%}
<div class="col-xs-6 column-break">
<h5 style="margin-bottom: 5px;">Seller</h5>
<p>{{ seller.Gstin }}</p>
<p>{{ seller.LglNm }}</p>
<p>{{ seller.Addr1 }}</p>
{%- if seller.Addr2 -%} <p>{{ seller.Addr2 }}</p> {% endif %}
<p>{{ seller.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.company_address, "gst_state") }} - {{ seller.Pin }}</p>
{%- if einvoice.ShipDtls -%}
{%- set shipping = einvoice.ShipDtls -%}
<h5 style="margin-bottom: 5px;">Shipping</h5>
<p>{{ shipping.Gstin }}</p>
<p>{{ shipping.LglNm }}</p>
<p>{{ shipping.Addr1 }}</p>
{%- if shipping.Addr2 -%} <p>{{ shipping.Addr2 }}</p> {% endif %}
<p>{{ shipping.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.shipping_address_name, "gst_state") }} - {{ shipping.Pin }}</p>
{% endif %}
</div>
{%- set buyer = einvoice.BuyerDtls -%}
<div class="col-xs-6 column-break">
<h5 style="margin-bottom: 5px;">Buyer</h5>
<p>{{ buyer.Gstin }}</p>
<p>{{ buyer.LglNm }}</p>
<p>{{ buyer.Addr1 }}</p>
{%- if buyer.Addr2 -%} <p>{{ buyer.Addr2 }}</p> {% endif %}
<p>{{ buyer.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.customer_address, "gst_state") }} - {{ buyer.Pin }}</p>
</div>
</div>
<div style="overflow-x: auto;">
<h5 class="font-bold" style="margin-top: 15px; margin-bottom: 10px;">3. Item Details</h5>
<table class="table table-bordered">
<thead>
<tr>
<th class="text-left" style="width: 3%;">Sr. No.</th>
<th class="text-left">Item</th>
<th class="text-left" style="width: 10%;">HSN Code</th>
<th class="text-left" style="width: 5%;">Qty</th>
<th class="text-left" style="width: 5%;">UOM</th>
<th class="text-left">Rate</th>
<th class="text-left" style="width: 5%;">Discount</th>
<th class="text-left">Taxable Amount</th>
<th class="text-left" style="width: 7%;">Tax Rate</th>
<th class="text-left" style="width: 5%;">Other Charges</th>
<th class="text-left">Total</th>
</tr>
</thead>
<tbody>
{% for item in einvoice.ItemList %}
<tr>
<td class="text-left" style="width: 3%;">{{ item.SlNo }}</td>
<td class="text-left">{{ item.PrdDesc }}</td>
<td class="text-left" style="width: 10%;">{{ item.HsnCd }}</td>
<td class="text-right" style="width: 5%;">{{ item.Qty }}</td>
<td class="text-left" style="width: 5%;">{{ item.Unit }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(item.UnitPrice, None, "INR") }}</td>
<td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(item.Discount, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(item.AssAmt, None, "INR") }}</td>
<td class="text-right" style="width: 7%;">{{ item.GstRt + item.CesRt }} %</td>
<td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(item.TotItemVal, None, "INR") }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div style="overflow-x: auto;">
<h5 class="font-bold" style="margin-bottom: 0px;">4. Value Details</h5>
<table class="table table-bordered">
<thead>
<tr>
<th class="text-left">Taxable Amount</th>
<th class="text-left">CGST</th>
<th class="text-left"">SGST</th>
<th class="text-left">IGST</th>
<th class="text-left">CESS</th>
<th class="text-left" style="width: 10%;">State CESS</th>
<th class="text-left">Discount</th>
<th class="text-left" style="width: 10%;">Other Charges</th>
<th class="text-left" style="width: 10%;">Round Off</th>
<th class="text-left">Total Value</th>
</tr>
</thead>
<tbody>
{%- set value_details = einvoice.ValDtls -%}
<tr>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.AssVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CgstVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.SgstVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.IgstVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.OthChrg, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }}</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@@ -1,24 +0,0 @@
{
"align_labels_right": 1,
"creation": "2020-10-10 18:01:21.032914",
"custom_format": 0,
"default_print_language": "en-US",
"disabled": 1,
"doc_type": "Sales Invoice",
"docstatus": 0,
"doctype": "Print Format",
"font": "Default",
"html": "",
"idx": 0,
"line_breaks": 1,
"modified": "2020-10-23 19:54:40.634936",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GST E-Invoice",
"owner": "Administrator",
"print_format_builder": 0,
"print_format_type": "Jinja",
"raw_printing": 0,
"show_section_headings": 1,
"standard": "Yes"
}

View File

@@ -27,4 +27,3 @@
{{ _("Authorized Signatory") }} {{ _("Authorized Signatory") }}
</p> </p>
</div> </div>

View File

@@ -62,8 +62,3 @@ def make_sales_invoice():
income_account = 'Sales - _TC2', income_account = 'Sales - _TC2',
expense_account = 'Cost of Goods Sold - _TC2', expense_account = 'Cost of Goods Sold - _TC2',
cost_center = 'Main - _TC2') cost_center = 'Main - _TC2')

View File

@@ -136,4 +136,3 @@ frappe.query_reports["Accounts Payable"] = {
} }
erpnext.utils.add_dimensions('Accounts Payable', 9); erpnext.utils.add_dimensions('Accounts Payable', 9);

View File

@@ -105,4 +105,3 @@ frappe.query_reports["Accounts Payable Summary"] = {
} }
erpnext.utils.add_dimensions('Accounts Payable Summary', 9); erpnext.utils.add_dimensions('Accounts Payable Summary', 9);

View File

@@ -12,4 +12,3 @@ def execute(filters=None):
"naming_by": ["Buying Settings", "supp_master_name"], "naming_by": ["Buying Settings", "supp_master_name"],
} }
return AccountsReceivableSummary(filters).run(args) return AccountsReceivableSummary(filters).run(args)

View File

@@ -200,4 +200,3 @@ frappe.query_reports["Accounts Receivable"] = {
} }
erpnext.utils.add_dimensions('Accounts Receivable', 9); erpnext.utils.add_dimensions('Accounts Receivable', 9);

View File

@@ -535,6 +535,8 @@ class ReceivablePayableReport(object):
if getdate(entry_date) > getdate(self.filters.report_date): if getdate(entry_date) > getdate(self.filters.report_date):
row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0 row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0
row.total_due = row.range1 + row.range2 + row.range3 + row.range4 + row.range5
def get_ageing_data(self, entry_date, row): def get_ageing_data(self, entry_date, row):
# [0-30, 30-60, 60-90, 90-120, 120-above] # [0-30, 30-60, 60-90, 90-120, 120-above]
row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0 row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0

View File

@@ -93,4 +93,3 @@ def make_credit_note(docname):
cost_center = 'Main - _TC2', cost_center = 'Main - _TC2',
is_return = 1, is_return = 1,
return_against = docname) return_against = docname)

View File

@@ -82,6 +82,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
"range3": 0.0, "range3": 0.0,
"range4": 0.0, "range4": 0.0,
"range5": 0.0, "range5": 0.0,
"total_due": 0.0,
"sales_person": [] "sales_person": []
})) }))
@@ -135,3 +136,6 @@ class AccountsReceivableSummary(ReceivablePayableReport):
"{range3}-{range4}".format(range3=cint(self.filters["range3"])+ 1, range4=self.filters["range4"]), "{range3}-{range4}".format(range3=cint(self.filters["range3"])+ 1, range4=self.filters["range4"]),
"{range4}-{above}".format(range4=cint(self.filters["range4"])+ 1, above=_("Above"))]): "{range4}-{above}".format(range4=cint(self.filters["range4"])+ 1, above=_("Above"))]):
self.add_column(label=label, fieldname='range' + str(i+1)) self.add_column(label=label, fieldname='range' + str(i+1))
# Add column for total due amount
self.add_column(label="Total Amount Due", fieldname='total_due')

View File

@@ -92,4 +92,3 @@ frappe.query_reports["Budget Variance Report"] = {
erpnext.dimension_filters.forEach((dimension) => { erpnext.dimension_filters.forEach((dimension) => {
frappe.query_reports["Budget Variance Report"].filters[4].options.push(dimension["document_type"]); frappe.query_reports["Budget Variance Report"].filters[4].options.push(dimension["document_type"]);
}); });

View File

@@ -399,4 +399,3 @@ def get_chart_data(filters, columns, data):
}, },
'type' : 'bar' 'type' : 'bar'
} }

View File

@@ -210,10 +210,10 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
company_currency = get_company_currency(filters) company_currency = get_company_currency(filters)
if filters.filter_based_on == 'Fiscal Year': if filters.filter_based_on == 'Fiscal Year':
start_date = fiscal_year.year_start_date start_date = fiscal_year.year_start_date if filters.report != 'Balance Sheet' else None
end_date = fiscal_year.year_end_date end_date = fiscal_year.year_end_date
else: else:
start_date = filters.period_start_date start_date = filters.period_start_date if filters.report != 'Balance Sheet' else None
end_date = filters.period_end_date end_date = filters.period_end_date
gl_entries_by_account = {} gl_entries_by_account = {}

View File

@@ -176,4 +176,3 @@ frappe.query_reports["General Ledger"] = {
} }
erpnext.utils.add_dimensions('General Ledger', 15) erpnext.utils.add_dimensions('General Ledger', 15)

View File

@@ -78,13 +78,10 @@ def validate_filters(filters, account_details):
def validate_party(filters): def validate_party(filters):
party_type, party = filters.get("party_type"), filters.get("party") party_type, party = filters.get("party_type"), filters.get("party")
if party: if party and party_type:
if not party_type: for d in party:
frappe.throw(_("To filter based on Party, select Party Type first")) if not frappe.db.exists(party_type, d):
else: frappe.throw(_("Invalid {0}: {1}").format(party_type, d))
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): def set_account_currency(filters):
if filters.get("account") or (filters.get('party') and len(filters.party) == 1): if filters.get("account") or (filters.get('party') and len(filters.party) == 1):

View File

@@ -1,16 +1,20 @@
{ {
"add_total_row": 1, "add_total_row": 0,
"columns": [],
"creation": "2013-02-25 17:03:34", "creation": "2013-02-25 17:03:34",
"disable_prepared_report": 0,
"disabled": 0, "disabled": 0,
"docstatus": 0, "docstatus": 0,
"doctype": "Report", "doctype": "Report",
"filters": [],
"idx": 3, "idx": 3,
"is_standard": "Yes", "is_standard": "Yes",
"modified": "2020-08-13 11:26:39.112352", "modified": "2021-08-19 18:57:07.468202",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Gross Profit", "name": "Gross Profit",
"owner": "Administrator", "owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Sales Invoice", "ref_doctype": "Sales Invoice",
"report_name": "Gross Profit", "report_name": "Gross Profit",
"report_type": "Script Report", "report_type": "Script Report",

View File

@@ -41,12 +41,14 @@ def execute(filters=None):
columns = get_columns(group_wise_columns, filters) columns = get_columns(group_wise_columns, filters)
for src in gross_profit_data.grouped_data: for idx, src in enumerate(gross_profit_data.grouped_data):
row = [] row = []
for col in group_wise_columns.get(scrub(filters.group_by)): for col in group_wise_columns.get(scrub(filters.group_by)):
row.append(src.get(col)) row.append(src.get(col))
row.append(filters.currency) row.append(filters.currency)
if idx == len(gross_profit_data.grouped_data)-1:
row[0] = frappe.bold("Total")
data.append(row) data.append(row)
return columns, data return columns, data
@@ -154,6 +156,15 @@ class GrossProfitGenerator(object):
def get_average_rate_based_on_group_by(self): def get_average_rate_based_on_group_by(self):
# sum buying / selling totals for group # sum buying / selling totals for group
self.totals = frappe._dict(
qty=0,
base_amount=0,
buying_amount=0,
gross_profit=0,
gross_profit_percent=0,
base_rate=0,
buying_rate=0
)
for key in list(self.grouped): for key in list(self.grouped):
if self.filters.get("group_by") != "Invoice": if self.filters.get("group_by") != "Invoice":
for i, row in enumerate(self.grouped[key]): for i, row in enumerate(self.grouped[key]):
@@ -165,6 +176,7 @@ class GrossProfitGenerator(object):
new_row.base_amount += flt(row.base_amount, self.currency_precision) new_row.base_amount += flt(row.base_amount, self.currency_precision)
new_row = self.set_average_rate(new_row) new_row = self.set_average_rate(new_row)
self.grouped_data.append(new_row) self.grouped_data.append(new_row)
self.add_to_totals(new_row)
else: else:
for i, row in enumerate(self.grouped[key]): for i, row in enumerate(self.grouped[key]):
if row.parent in self.returned_invoices \ if row.parent in self.returned_invoices \
@@ -177,15 +189,25 @@ class GrossProfitGenerator(object):
if row.qty or row.base_amount: if row.qty or row.base_amount:
row = self.set_average_rate(row) row = self.set_average_rate(row)
self.grouped_data.append(row) self.grouped_data.append(row)
self.add_to_totals(row)
self.set_average_gross_profit(self.totals)
self.grouped_data.append(self.totals)
def set_average_rate(self, new_row): def set_average_rate(self, new_row):
self.set_average_gross_profit(new_row)
new_row.buying_rate = flt(new_row.buying_amount / new_row.qty, self.float_precision) if new_row.qty else 0
new_row.base_rate = flt(new_row.base_amount / new_row.qty, self.float_precision) if new_row.qty else 0
return new_row
def set_average_gross_profit(self, new_row):
new_row.gross_profit = flt(new_row.base_amount - new_row.buying_amount, self.currency_precision) new_row.gross_profit = flt(new_row.base_amount - new_row.buying_amount, self.currency_precision)
new_row.gross_profit_percent = flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision) \ new_row.gross_profit_percent = flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision) \
if new_row.base_amount else 0 if new_row.base_amount else 0
new_row.buying_rate = flt(new_row.buying_amount / new_row.qty, self.float_precision) if new_row.qty else 0
new_row.base_rate = flt(new_row.base_amount / new_row.qty, self.float_precision) if new_row.qty else 0
return new_row def add_to_totals(self, new_row):
for key in self.totals:
if new_row.get(key):
self.totals[key] += new_row[key]
def get_returned_invoice_items(self): def get_returned_invoice_items(self):
returned_invoices = frappe.db.sql(""" returned_invoices = frappe.db.sql("""

View File

@@ -76,7 +76,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
'company': d.company, 'company': d.company,
'sales_order': d.sales_order, 'sales_order': d.sales_order,
'delivery_note': d.delivery_note, 'delivery_note': d.delivery_note,
'income_account': d.unrealized_profit_loss_account or d.income_account, 'income_account': d.unrealized_profit_loss_account if d.is_internal_customer == 1 else d.income_account,
'cost_center': d.cost_center, 'cost_center': d.cost_center,
'stock_qty': d.stock_qty, 'stock_qty': d.stock_qty,
'stock_uom': d.stock_uom 'stock_uom': d.stock_uom
@@ -380,6 +380,7 @@ def get_items(filters, additional_query_columns):
`tabSales Invoice Item`.name, `tabSales Invoice Item`.parent, `tabSales Invoice Item`.name, `tabSales Invoice Item`.parent,
`tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to, `tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to,
`tabSales Invoice`.unrealized_profit_loss_account, `tabSales Invoice`.unrealized_profit_loss_account,
`tabSales Invoice`.is_internal_customer,
`tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks, `tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks,
`tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total, `tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description, `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,
@@ -625,7 +626,3 @@ def add_sub_total_row(item, total_row_map, group_by_value, tax_columns):
for tax in tax_columns: for tax in tax_columns:
total_row.setdefault(frappe.scrub(tax + ' Amount'), 0.0) total_row.setdefault(frappe.scrub(tax + ' Amount'), 0.0)
total_row[frappe.scrub(tax + ' Amount')] += flt(item[frappe.scrub(tax + ' Amount')]) total_row[frappe.scrub(tax + ' Amount')] += flt(item[frappe.scrub(tax + ' Amount')])

View File

@@ -69,4 +69,3 @@ frappe.query_reports["Sales Register"] = {
} }
erpnext.utils.add_dimensions('Sales Register', 7); erpnext.utils.add_dimensions('Sales Register', 7);

View File

@@ -84,7 +84,7 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No
# Add amount in unrealized account # Add amount in unrealized account
for account in unrealized_profit_loss_accounts: for account in unrealized_profit_loss_accounts:
row.update({ row.update({
frappe.scrub(account): flt(internal_invoice_map.get((inv.name, account))) frappe.scrub(account+"_unrealized"): flt(internal_invoice_map.get((inv.name, account)))
}) })
# net total # net total
@@ -258,6 +258,7 @@ def get_columns(invoice_list, additional_table_columns):
unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account
from `tabSales Invoice` where docstatus = 1 and name in (%s) from `tabSales Invoice` where docstatus = 1 and name in (%s)
and is_internal_customer = 1
and ifnull(unrealized_profit_loss_account, '') != '' and ifnull(unrealized_profit_loss_account, '') != ''
order by unrealized_profit_loss_account""" % order by unrealized_profit_loss_account""" %
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list)) ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
@@ -284,7 +285,7 @@ def get_columns(invoice_list, additional_table_columns):
for account in unrealized_profit_loss_accounts: for account in unrealized_profit_loss_accounts:
unrealized_profit_loss_account_columns.append({ unrealized_profit_loss_account_columns.append({
"label": account, "label": account,
"fieldname": frappe.scrub(account), "fieldname": frappe.scrub(account+"_unrealized"),
"fieldtype": "Currency", "fieldtype": "Currency",
"options": "currency", "options": "currency",
"width": 120 "width": 120

View File

@@ -110,6 +110,3 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
erpnext.utils.add_dimensions('Trial Balance', 6); erpnext.utils.add_dimensions('Trial Balance', 6);
}); });

View File

@@ -19,6 +19,7 @@ from erpnext.stock import get_warehouse_account_map
class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError): pass class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError): pass
class FiscalYearError(frappe.ValidationError): pass class FiscalYearError(frappe.ValidationError): pass
class PaymentEntryUnlinkError(frappe.ValidationError): pass
@frappe.whitelist() @frappe.whitelist()
def get_fiscal_year(date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False): def get_fiscal_year(date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False):
@@ -350,6 +351,7 @@ def reconcile_against_document(args):
# cancel advance entry # cancel advance entry
doc = frappe.get_doc(d.voucher_type, d.voucher_no) doc = frappe.get_doc(d.voucher_type, d.voucher_no)
frappe.flags.ignore_party_validation = True
doc.make_gl_entries(cancel=1, adv_adj=1) doc.make_gl_entries(cancel=1, adv_adj=1)
# update ref in advance entry # update ref in advance entry
@@ -361,6 +363,7 @@ def reconcile_against_document(args):
# re-submit advance entry # re-submit advance entry
doc = frappe.get_doc(d.voucher_type, d.voucher_no) doc = frappe.get_doc(d.voucher_type, d.voucher_no)
doc.make_gl_entries(cancel = 0, adv_adj =1) doc.make_gl_entries(cancel = 0, adv_adj =1)
frappe.flags.ignore_party_validation = False
if d.voucher_type in ('Payment Entry', 'Journal Entry'): if d.voucher_type in ('Payment Entry', 'Journal Entry'):
doc.update_expense_claim() doc.update_expense_claim()
@@ -553,10 +556,16 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no):
and docstatus < 2""", (now(), frappe.session.user, ref_type, ref_no)) and docstatus < 2""", (now(), frappe.session.user, ref_type, ref_no))
for pe in linked_pe: for pe in linked_pe:
pe_doc = frappe.get_doc("Payment Entry", pe) try:
pe_doc.set_total_allocated_amount() pe_doc = frappe.get_doc("Payment Entry", pe)
pe_doc.set_unallocated_amount() pe_doc.set_amounts()
pe_doc.clear_unallocated_reference_document_rows() pe_doc.clear_unallocated_reference_document_rows()
pe_doc.validate_payment_type_with_outstanding()
except Exception as e:
msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name)
msg += '<br>'
msg += _("Please cancel payment entry manually first")
frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error"))
frappe.db.sql("""update `tabPayment Entry` set total_allocated_amount=%s, frappe.db.sql("""update `tabPayment Entry` set total_allocated_amount=%s,
base_total_allocated_amount=%s, unallocated_amount=%s, modified=%s, modified_by=%s base_total_allocated_amount=%s, unallocated_amount=%s, modified=%s, modified_by=%s

View File

@@ -36,4 +36,3 @@ QUnit.test("test: Disease", function (assert) {
]); ]);
}); });

View File

@@ -59,7 +59,7 @@ def make_depreciation_entry(asset_name, date=None):
"credit_in_account_currency": d.depreciation_amount, "credit_in_account_currency": d.depreciation_amount,
"reference_type": "Asset", "reference_type": "Asset",
"reference_name": asset.name, "reference_name": asset.name,
"cost_center": "" "cost_center": depreciation_cost_center
} }
debit_entry = { debit_entry = {

View File

@@ -10,4 +10,3 @@ frappe.listview_settings['Asset Repair'] = {
} }
} }
}; };

View File

@@ -565,6 +565,7 @@
"fieldname": "scan_barcode", "fieldname": "scan_barcode",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Scan Barcode", "label": "Scan Barcode",
"options": "Barcode",
"show_days": 1, "show_days": 1,
"show_seconds": 1 "show_seconds": 1
}, },
@@ -1378,7 +1379,7 @@
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-05-30 15:17:53.663648", "modified": "2021-08-17 20:16:12.737743",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",

View File

@@ -93,5 +93,3 @@ var loadAllStandings = function(frm) {
} }
}); });
}; };

View File

@@ -128,4 +128,3 @@ valid_scorecard = [
"weighting_function":"{total_score} * max( 0, min ( 1 , (12 - {period_number}) / 12) )" "weighting_function":"{total_score} * max( 0, min ( 1 , (12 - {period_number}) / 12) )"
} }
] ]

View File

@@ -109,4 +109,3 @@ def make_supplier_scorecard(source_name, target_doc=None):
}, target_doc, post_process, ignore_permissions=True) }, target_doc, post_process, ignore_permissions=True)
return doc return doc

View File

@@ -268,4 +268,3 @@ def get_columns(filters):
]) ])
return columns return columns

View File

@@ -102,4 +102,3 @@ def get_linked_material_requests(items):
mr_list.append(material_request) mr_list.append(material_request)
return mr_list return mr_list

View File

@@ -135,14 +135,9 @@ class AccountsController(TransactionBase):
validate_regional(self) validate_regional(self)
validate_einvoice_fields(self)
if self.doctype != 'Material Request': if self.doctype != 'Material Request':
apply_pricing_rule_on_transaction(self) apply_pricing_rule_on_transaction(self)
def before_cancel(self):
validate_einvoice_fields(self)
def on_trash(self): def on_trash(self):
# delete sl and gl entries on deletion of transaction # delete sl and gl entries on deletion of transaction
if frappe.db.get_single_value('Accounts Settings', 'delete_linked_ledger_entries'): if frappe.db.get_single_value('Accounts Settings', 'delete_linked_ledger_entries'):
@@ -164,7 +159,8 @@ class AccountsController(TransactionBase):
self.set_due_date() self.set_due_date()
self.set_payment_schedule() self.set_payment_schedule()
self.validate_payment_schedule_amount() 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() self.validate_advance_entries()
def validate_non_invoice_documents_schedule(self): def validate_non_invoice_documents_schedule(self):
@@ -1841,6 +1837,11 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
for d in data: for d in data:
new_child_flag = False new_child_flag = False
if not d.get("item_code"):
# ignore empty rows
continue
if not d.get("docname"): if not d.get("docname"):
new_child_flag = True new_child_flag = True
check_doc_permissions(parent, 'create') check_doc_permissions(parent, 'create')
@@ -1975,7 +1976,3 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
@erpnext.allow_regional @erpnext.allow_regional
def validate_regional(doc): def validate_regional(doc):
pass pass
@erpnext.allow_regional
def validate_einvoice_fields(doc):
pass

View File

@@ -344,4 +344,3 @@ def create_variant_doc_for_quick_entry(template, args):
variant.name = variant.item_code variant.name = variant.item_code
validate_item_variant_attributes(variant, args) validate_item_variant_attributes(variant, args)
return variant.as_dict() return variant.as_dict()

View File

@@ -329,7 +329,6 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.po_detail = source_doc.po_detail target_doc.po_detail = source_doc.po_detail
target_doc.pr_detail = source_doc.pr_detail target_doc.pr_detail = source_doc.pr_detail
target_doc.purchase_invoice_item = source_doc.name target_doc.purchase_invoice_item = source_doc.name
target_doc.price_list_rate = 0
elif doctype == "Delivery Note": elif doctype == "Delivery Note":
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
@@ -360,7 +359,6 @@ def make_return_doc(doctype, source_name, target_doc=None):
else: else:
target_doc.pos_invoice_item = source_doc.name target_doc.pos_invoice_item = source_doc.name
target_doc.price_list_rate = 0
if default_warehouse_for_sales_return: if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return target_doc.warehouse = default_warehouse_for_sales_return

View File

@@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import cint, flt, cstr, get_link_to_form, nowtime 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.get_item_details import get_bin_details
from erpnext.stock.utils import get_incoming_rate from erpnext.stock.utils import get_incoming_rate
from erpnext.stock.get_item_details import get_conversion_factor 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 from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
class SellingController(StockController): class SellingController(StockController):
def get_feed(self): def get_feed(self):
return _("To {0} | {1} {2}").format(self.customer_name, self.currency, return _("To {0} | {1} {2}").format(self.customer_name, self.currency,
self.grand_total) self.grand_total)
@@ -169,39 +168,96 @@ class SellingController(StockController):
def validate_selling_price(self): def validate_selling_price(self):
def throw_message(idx, item_name, rate, ref_rate_field): def throw_message(idx, item_name, rate, ref_rate_field):
bold_net_rate = frappe.bold("net rate") throw(_("""Row #{0}: Selling rate for item {1} is lower than its {2}.
msg = (_("""Row #{}: Selling rate for item {} is lower than its {}. Selling {} should be atleast {}""") Selling {3} should be atleast {4}.<br><br>Alternatively,
.format(idx, frappe.bold(item_name), frappe.bold(ref_rate_field), bold_net_rate, frappe.bold(rate))) you can disable selling price validation in {5} to bypass
msg += "<br><br>" this validation.""").format(
msg += (_("""You can alternatively disable selling price validation in {} to bypass this validation.""") idx,
.format(get_link_to_form("Selling Settings", "Selling Settings"))) bold(item_name),
frappe.throw(msg, title=_("Invalid Selling Price")) 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"): if (
return self.get("is_return")
if hasattr(self, "is_return") and self.is_return: or not frappe.db.get_single_value("Selling Settings", "validate_selling_price")
):
return return
for it in self.get("items"): is_internal_customer = self.get('is_internal_customer')
if not it.item_code: valuation_rate_map = {}
for item in self.items:
if not item.item_code:
continue continue
last_purchase_rate, is_stock_item = frappe.get_cached_value("Item", it.item_code, ["last_purchase_rate", "is_stock_item"]) last_purchase_rate, is_stock_item = frappe.get_cached_value(
last_purchase_rate_in_sales_uom = last_purchase_rate * (it.conversion_factor or 1) "Item", item.item_code, ("last_purchase_rate", "is_stock_item")
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_valuation_rate = frappe.db.sql(""" last_purchase_rate_in_sales_uom = (
SELECT valuation_rate FROM `tabStock Ledger Entry` WHERE item_code = %s last_purchase_rate * (item.conversion_factor or 1)
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")
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): def get_item_list(self):
il = [] il = []

View File

@@ -86,7 +86,8 @@ status_map = {
], ],
"Bank Transaction": [ "Bank Transaction": [
["Unreconciled", "eval:self.docstatus == 1 and self.unallocated_amount>0"], ["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": [ "POS Opening Entry": [
["Draft", None], ["Draft", None],

View File

@@ -595,7 +595,8 @@ class calculate_taxes_and_totals(object):
self.doc.precision("outstanding_amount")) self.doc.precision("outstanding_amount"))
if self.doc.doctype == 'Sales Invoice' and self.doc.get('is_pos') and self.doc.get('is_return'): if self.doc.doctype == 'Sales Invoice' and self.doc.get('is_pos') and self.doc.get('is_return'):
self.update_paid_amount_for_return(total_amount_to_pay) self.set_total_amount_to_default_mop(total_amount_to_pay)
self.calculate_paid_amount()
def calculate_paid_amount(self): def calculate_paid_amount(self):
@@ -675,7 +676,7 @@ class calculate_taxes_and_totals(object):
def set_item_wise_tax_breakup(self): def set_item_wise_tax_breakup(self):
self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc) self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc)
def update_paid_amount_for_return(self, total_amount_to_pay): def set_total_amount_to_default_mop(self, total_amount_to_pay):
default_mode_of_payment = frappe.db.get_value('POS Payment Method', default_mode_of_payment = frappe.db.get_value('POS Payment Method',
{'parent': self.doc.pos_profile, 'default': 1}, ['mode_of_payment'], as_dict=1) {'parent': self.doc.pos_profile, 'default': 1}, ['mode_of_payment'], as_dict=1)
@@ -687,8 +688,6 @@ class calculate_taxes_and_totals(object):
'default': 1 'default': 1
}) })
self.calculate_paid_amount()
def get_itemised_tax_breakup_html(doc): def get_itemised_tax_breakup_html(doc):
if not doc.taxes: if not doc.taxes:
return return

View File

@@ -235,4 +235,3 @@ def _get_employee_from_user(user):
# frappe.db.exists returns a tuple of a tuple # frappe.db.exists returns a tuple of a tuple
return frappe.get_doc('Employee', employee_docname[0][0]) return frappe.get_doc('Employee', employee_docname[0][0])
return None return None

View File

@@ -36,7 +36,8 @@ class Lead(SellingController):
}) })
def set_full_name(self): def set_full_name(self):
self.lead_name = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name])) if self.first_name:
self.lead_name = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name]))
def validate_email_id(self): def validate_email_id(self):
if self.email_id: if self.email_id:

View File

@@ -2,8 +2,8 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('LinkedIn Settings', { frappe.ui.form.on('LinkedIn Settings', {
onload: function(frm){ onload: function(frm) {
if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret) {
frappe.confirm( frappe.confirm(
__('Session not valid, Do you want to login?'), __('Session not valid, Do you want to login?'),
function(){ function(){
@@ -14,8 +14,9 @@ frappe.ui.form.on('LinkedIn Settings', {
} }
); );
} }
frm.dashboard.set_headline(__("For more information, {0}.", [`<a target='_blank' href='https://docs.erpnext.com/docs/user/manual/en/CRM/linkedin-settings'>${__('Click here')}</a>`]));
}, },
refresh: function(frm){ refresh: function(frm) {
if (frm.doc.session_status=="Expired"){ if (frm.doc.session_status=="Expired"){
let msg = __("Session Not Active. Save doc to login."); let msg = __("Session Not Active. Save doc to login.");
frm.dashboard.set_headline_alert( 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){ if (frm.doc.consumer_key && frm.doc.consumer_secret){
frappe.dom.freeze(); frappe.dom.freeze();
frappe.call({ frappe.call({
@@ -67,7 +68,7 @@ frappe.ui.form.on('LinkedIn Settings', {
}); });
} }
}, },
after_save: function(frm){ after_save: function(frm) {
frm.trigger("login"); frm.trigger("login");
} }
}); });

View File

@@ -2,6 +2,7 @@
"actions": [], "actions": [],
"creation": "2020-01-30 13:36:39.492931", "creation": "2020-01-30 13:36:39.492931",
"doctype": "DocType", "doctype": "DocType",
"documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/linkedin-settings",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
@@ -87,7 +88,7 @@
], ],
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2020-04-16 23:22:51.966397", "modified": "2021-02-18 15:19:21.920725",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "LinkedIn Settings", "name": "LinkedIn Settings",

View File

@@ -3,11 +3,12 @@
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, requests, json import frappe
import requests
from frappe import _ 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.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 from six.moves.urllib.parse import urlencode
class LinkedInSettings(Document): class LinkedInSettings(Document):
@@ -42,11 +43,7 @@ class LinkedInSettings(Document):
self.db_set("access_token", response["access_token"]) self.db_set("access_token", response["access_token"])
def get_member_profile(self): def get_member_profile(self):
headers = { response = requests.get(url="https://api.linkedin.com/v2/me", headers=self.get_headers())
"Authorization": "Bearer {}".format(self.access_token)
}
url = "https://api.linkedin.com/v2/me"
response = requests.get(url=url, headers=headers)
response = frappe.parse_json(response.content.decode()) response = frappe.parse_json(response.content.decode())
frappe.db.set_value(self.doctype, self.name, { frappe.db.set_value(self.doctype, self.name, {
@@ -55,16 +52,16 @@ class LinkedInSettings(Document):
"session_status": "Active" "session_status": "Active"
}) })
frappe.local.response["type"] = "redirect" 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: if not media:
return self.post_text(text) return self.post_text(text, title)
else: else:
media_id = self.upload_image(media) media_id = self.upload_image(media)
if media_id: if media_id:
return self.post_text(text, media_id=media_id) return self.post_text(text, title, media_id=media_id)
else: else:
frappe.log_error("Failed to upload media.","LinkedIn Upload Error") frappe.log_error("Failed to upload media.","LinkedIn Upload Error")
@@ -82,9 +79,7 @@ class LinkedInSettings(Document):
}] }]
} }
} }
headers = { headers = self.get_headers()
"Authorization": "Bearer {}".format(self.access_token)
}
response = self.http_post(url=register_url, body=body, headers=headers) response = self.http_post(url=register_url, body=body, headers=headers)
if response.status_code == 200: if response.status_code == 200:
@@ -100,24 +95,33 @@ class LinkedInSettings(Document):
return None 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" url = "https://api.linkedin.com/v2/shares"
headers = { headers = self.get_headers()
"X-Restli-Protocol-Version": "2.0.0", headers["X-Restli-Protocol-Version"] = "2.0.0"
"Authorization": "Bearer {}".format(self.access_token), headers["Content-Type"] = "application/json; charset=UTF-8"
"Content-Type": "application/json; charset=UTF-8"
}
body = { body = {
"distribution": { "distribution": {
"linkedInDistributionTarget": {} "linkedInDistributionTarget": {}
}, },
"owner":"urn:li:organization:{0}".format(self.company_id), "owner":"urn:li:organization:{0}".format(self.company_id),
"subject": "Test Share Subject", "subject": title,
"text": { "text": {
"text": text "text": text
} }
} }
reference_url = self.get_reference_url(text)
if reference_url:
body["content"] = {
"contentEntities": [
{
"entityLocation": reference_url
}
]
}
if media_id: if media_id:
body["content"]= { body["content"]= {
"contentEntities": [{ "contentEntities": [{
@@ -141,20 +145,60 @@ class LinkedInSettings(Document):
raise raise
except Exception as e: except Exception as e:
content = json.loads(response.content) self.api_error(response)
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)
return 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) @frappe.whitelist(allow_guest=True)
def callback(code=None, error=None, error_description=None): def callback(code=None, error=None, error_description=None):
if not error: if not error:

View File

@@ -95,11 +95,18 @@ frappe.ui.form.on("Opportunity", {
}, __('Create')); }, __('Create'));
} }
if (frm.doc.opportunity_from != "Customer") {
frm.add_custom_button(__('Customer'),
function() {
frm.trigger("make_customer")
}, __('Create'));
}
frm.add_custom_button(__('Quotation'), frm.add_custom_button(__('Quotation'),
function() { function() {
frm.trigger("create_quotation") frm.trigger("create_quotation")
}, __('Create')); }, __('Create'));
} }
if(!frm.doc.__islocal && frm.perm[0].write && frm.doc.docstatus==0) { if(!frm.doc.__islocal && frm.perm[0].write && frm.doc.docstatus==0) {
if(frm.doc.status==="Open") { if(frm.doc.status==="Open") {
@@ -196,6 +203,13 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
frm: cur_frm 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})); extend_cscript(cur_frm.cscript, new erpnext.crm.Opportunity({frm: cur_frm}));

View File

@@ -287,6 +287,24 @@ def make_request_for_quotation(source_name, target_doc=None):
return doclist 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() @frappe.whitelist()
def make_supplier_quotation(source_name, target_doc=None): def make_supplier_quotation(source_name, target_doc=None):
doclist = get_mapped_doc("Opportunity", source_name, { doclist = get_mapped_doc("Opportunity", source_name, {

View File

@@ -1,67 +1,139 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Social Media Post', { frappe.ui.form.on('Social Media Post', {
validate: function(frm){ validate: function(frm) {
if (frm.doc.twitter === 0 && frm.doc.linkedin === 0){ if (frm.doc.twitter === 0 && frm.doc.linkedin === 0) {
frappe.throw(__("Select atleast one Social Media from Share on.")) frappe.throw(__("Select atleast one Social Media Platform to Share on."));
} }
if (frm.doc.scheduled_time) { if (frm.doc.scheduled_time) {
let scheduled_time = new Date(frm.doc.scheduled_time); let scheduled_time = new Date(frm.doc.scheduled_time);
let date_time = new Date(); let date_time = new Date();
if (scheduled_time.getTime() < date_time.getTime()){ if (scheduled_time.getTime() < date_time.getTime()) {
frappe.throw(__("Invalid Scheduled Time")); frappe.throw(__("Scheduled Time must be a future time."));
} }
} }
if (frm.doc.text?.length > 280){ frm.trigger('validate_tweet_length');
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);
}
let html=''; text: function(frm) {
if (frm.doc.twitter){ if (frm.doc.text) {
let color = frm.doc.twitter_post_id ? "green" : "red"; frm.set_df_property('text', 'description', `${frm.doc.text.length}/280`);
let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted"; frm.refresh_field('text');
html += `<div class="col-xs-6"> frm.trigger('validate_tweet_length');
<span class="indicator whitespace-nowrap ${color}"><span>Twitter : ${status} </span></span> }
</div>` ; },
}
if (frm.doc.linkedin){ validate_tweet_length: function(frm) {
let color = frm.doc.linkedin_post_id ? "green" : "red"; if (frm.doc.text && frm.doc.text.length > 280) {
let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted"; frappe.throw(__("Tweet length Must be less than 280."));
html += `<div class="col-xs-6"> }
<span class="indicator whitespace-nowrap ${color}"><span>LinkedIn : ${status} </span></span> },
</div>` ;
} onload: function(frm) {
html = `<div class="row">${html}</div>`; frm.trigger('make_dashboard');
frm.dashboard.set_headline_alert(html); },
}
} make_dashboard: function(frm) {
if (frm.doc.post_status == "Posted") {
frappe.call({
doc: frm.doc,
method: 'get_post',
freeze: true,
callback: (r) => {
if (!r.message) {
return;
}
let datasets = [], colors = [];
if (r.message && r.message.twitter) {
colors.push('#1DA1F2');
datasets.push({
name: 'Twitter',
values: [r.message.twitter.favorite_count, r.message.twitter.retweet_count]
});
}
if (r.message && r.message.linkedin) {
colors.push('#0077b5');
datasets.push({
name: 'LinkedIn',
values: [r.message.linkedin.totalShareStatistics.likeCount, r.message.linkedin.totalShareStatistics.shareCount]
});
}
if (datasets.length) {
frm.dashboard.render_graph({
data: {
labels: ['Likes', 'Retweets/Shares'],
datasets: datasets
},
title: __("Post Metrics"),
type: 'bar',
height: 300,
colors: colors
});
}
}
});
}
},
refresh: function(frm) {
frm.trigger('text');
if (frm.doc.docstatus === 1) {
if (!['Posted', 'Deleted'].includes(frm.doc.post_status)) {
frm.trigger('add_post_btn');
}
if (frm.doc.post_status !='Deleted') {
frm.add_custom_button(('Delete Post'), function() {
frappe.confirm(__('Are you sure want to delete the Post from Social Media platforms?'),
function() {
frappe.call({
doc: frm.doc,
method: 'delete_post',
freeze: true,
callback: () => {
frm.reload_doc();
}
});
}
);
});
}
if (frm.doc.post_status !='Deleted') {
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 += `<div class="col-xs-6">
<span class="indicator whitespace-nowrap ${color}"><span>Twitter : ${status} </span></span>
</div>` ;
}
if (frm.doc.linkedin) {
let color = frm.doc.linkedin_post_id ? "green" : "red";
let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted";
html += `<div class="col-xs-6">
<span class="indicator whitespace-nowrap ${color}"><span>LinkedIn : ${status} </span></span>
</div>` ;
}
html = `<div class="row">${html}</div>`;
frm.dashboard.set_headline_alert(html);
}
}
},
add_post_btn: function(frm) {
frm.add_custom_button(__('Post Now'), function() {
frappe.call({
doc: frm.doc,
method: 'post',
freeze: true,
callback: function() {
frm.reload_doc();
}
});
});
}
}); });
var add_post_btn = function(frm){
frm.add_custom_button(('Post Now'), function(){
post(frm);
});
}
var post = function(frm){
frappe.dom.freeze();
frappe.call({
method: "erpnext.crm.doctype.social_media_post.social_media_post.publish",
args: {
doctype: frm.doc.doctype,
name: frm.doc.name
},
callback: function(r) {
frm.reload_doc();
frappe.dom.unfreeze();
}
})
}

View File

@@ -3,9 +3,11 @@
"autoname": "format: CRM-SMP-{YYYY}-{MM}-{DD}-{###}", "autoname": "format: CRM-SMP-{YYYY}-{MM}-{DD}-{###}",
"creation": "2020-01-30 11:53:13.872864", "creation": "2020-01-30 11:53:13.872864",
"doctype": "DocType", "doctype": "DocType",
"documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/social-media-post",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"title",
"campaign_name", "campaign_name",
"scheduled_time", "scheduled_time",
"post_status", "post_status",
@@ -30,32 +32,24 @@
"fieldname": "text", "fieldname": "text",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Tweet", "label": "Tweet",
"mandatory_depends_on": "eval:doc.twitter ==1", "mandatory_depends_on": "eval:doc.twitter ==1"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "image", "fieldname": "image",
"fieldtype": "Attach Image", "fieldtype": "Attach Image",
"label": "Image", "label": "Image"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "1",
"fieldname": "twitter", "fieldname": "twitter",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Twitter", "label": "Twitter"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "1",
"fieldname": "linkedin", "fieldname": "linkedin",
"fieldtype": "Check", "fieldtype": "Check",
"label": "LinkedIn", "label": "LinkedIn"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "amended_from", "fieldname": "amended_from",
@@ -64,27 +58,22 @@
"no_copy": 1, "no_copy": 1,
"options": "Social Media Post", "options": "Social Media Post",
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"depends_on": "eval:doc.twitter ==1", "depends_on": "eval:doc.twitter ==1",
"fieldname": "content", "fieldname": "content",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Twitter", "label": "Twitter"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "post_status", "fieldname": "post_status",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Post Status", "label": "Post Status",
"options": "\nScheduled\nPosted\nError", "no_copy": 1,
"read_only": 1, "options": "\nScheduled\nPosted\nCancelled\nDeleted\nError",
"show_days": 1, "read_only": 1
"show_seconds": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
@@ -92,9 +81,8 @@
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Twitter Post Id", "label": "Twitter Post Id",
"read_only": 1, "no_copy": 1,
"show_days": 1, "read_only": 1
"show_seconds": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
@@ -102,82 +90,69 @@
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "LinkedIn Post Id", "label": "LinkedIn Post Id",
"read_only": 1, "no_copy": 1,
"show_days": 1, "read_only": 1
"show_seconds": 1
}, },
{ {
"fieldname": "campaign_name", "fieldname": "campaign_name",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Campaign", "label": "Campaign",
"options": "Campaign", "options": "Campaign"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_6", "fieldname": "column_break_6",
"fieldtype": "Column Break", "fieldtype": "Column Break",
"label": "Share On", "label": "Share On"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_14", "fieldname": "column_break_14",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "tweet_preview", "fieldname": "tweet_preview",
"fieldtype": "HTML", "fieldtype": "HTML"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
"depends_on": "eval:doc.linkedin==1", "depends_on": "eval:doc.linkedin==1",
"fieldname": "linkedin_section", "fieldname": "linkedin_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "LinkedIn", "label": "LinkedIn"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "attachments_section", "fieldname": "attachments_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Attachments", "label": "Attachments"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "linkedin_post", "fieldname": "linkedin_post",
"fieldtype": "Text", "fieldtype": "Text",
"label": "Post", "label": "Post",
"mandatory_depends_on": "eval:doc.linkedin ==1", "mandatory_depends_on": "eval:doc.linkedin ==1"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_15", "fieldname": "column_break_15",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "scheduled_time", "fieldname": "scheduled_time",
"fieldtype": "Datetime", "fieldtype": "Datetime",
"label": "Scheduled Time", "label": "Scheduled Time",
"read_only_depends_on": "eval:doc.post_status == \"Posted\"", "read_only_depends_on": "eval:doc.post_status == \"Posted\""
"show_days": 1, },
"show_seconds": 1 {
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"reqd": 1
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-06-14 10:31:33.961381", "modified": "2021-04-14 14:24:59.821223",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Social Media Post", "name": "Social Media Post",
@@ -228,5 +203,6 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"title_field": "title",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -10,17 +10,51 @@ import datetime
class SocialMediaPost(Document): class SocialMediaPost(Document):
def validate(self): def validate(self):
if (not self.twitter and not self.linkedin):
frappe.throw(_("Select atleast one Social Media Platform to Share on."))
if self.scheduled_time: if self.scheduled_time:
current_time = frappe.utils.now_datetime() current_time = frappe.utils.now_datetime()
scheduled_time = frappe.utils.get_datetime(self.scheduled_time) scheduled_time = frappe.utils.get_datetime(self.scheduled_time)
if scheduled_time < current_time: if scheduled_time < current_time:
frappe.throw(_("Invalid Scheduled Time")) frappe.throw(_("Scheduled Time must be a future time."))
if self.text and len(self.text) > 280:
frappe.throw(_("Tweet length must be less than 280."))
def submit(self): def submit(self):
if self.scheduled_time: if self.scheduled_time:
self.post_status = "Scheduled" self.post_status = "Scheduled"
super(SocialMediaPost, self).submit() super(SocialMediaPost, self).submit()
def on_cancel(self):
self.db_set('post_status', 'Cancelled')
@frappe.whitelist()
def delete_post(self):
if self.twitter and self.twitter_post_id:
twitter = frappe.get_doc("Twitter Settings")
twitter.delete_tweet(self.twitter_post_id)
if self.linkedin and self.linkedin_post_id:
linkedin = frappe.get_doc("LinkedIn Settings")
linkedin.delete_post(self.linkedin_post_id)
self.db_set('post_status', 'Deleted')
@frappe.whitelist()
def get_post(self):
response = {}
if self.linkedin and self.linkedin_post_id:
linkedin = frappe.get_doc("LinkedIn Settings")
response['linkedin'] = linkedin.get_post(self.linkedin_post_id)
if self.twitter and self.twitter_post_id:
twitter = frappe.get_doc("Twitter Settings")
response['twitter'] = twitter.get_tweet(self.twitter_post_id)
return response
@frappe.whitelist()
def post(self): def post(self):
try: try:
if self.twitter and not self.twitter_post_id: if self.twitter and not self.twitter_post_id:
@@ -29,28 +63,22 @@ class SocialMediaPost(Document):
self.db_set("twitter_post_id", twitter_post.id) self.db_set("twitter_post_id", twitter_post.id)
if self.linkedin and not self.linkedin_post_id: if self.linkedin and not self.linkedin_post_id:
linkedin = frappe.get_doc("LinkedIn Settings") linkedin = frappe.get_doc("LinkedIn Settings")
linkedin_post = linkedin.post(self.linkedin_post, self.image) linkedin_post = linkedin.post(self.linkedin_post, self.title, self.image)
self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id'].split(":")[-1]) self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id'])
self.db_set("post_status", "Posted") self.db_set("post_status", "Posted")
except: except:
self.db_set("post_status", "Error") self.db_set("post_status", "Error")
title = _("Error while POSTING {0}").format(self.name) title = _("Error while POSTING {0}").format(self.name)
traceback = frappe.get_traceback() frappe.log_error(message=frappe.get_traceback(), title=title)
frappe.log_error(message=traceback , title=title)
def process_scheduled_social_media_posts(): def process_scheduled_social_media_posts():
posts = frappe.get_list("Social Media Post", filters={"post_status": "Scheduled", "docstatus":1}, fields= ["name", "scheduled_time","post_status"]) posts = frappe.get_list("Social Media Post", filters={"post_status": "Scheduled", "docstatus":1}, fields= ["name", "scheduled_time"])
start = frappe.utils.now_datetime() start = frappe.utils.now_datetime()
end = start + datetime.timedelta(minutes=10) end = start + datetime.timedelta(minutes=10)
for post in posts: for post in posts:
if post.scheduled_time: if post.scheduled_time:
post_time = frappe.utils.get_datetime(post.scheduled_time) post_time = frappe.utils.get_datetime(post.scheduled_time)
if post_time > start and post_time <= end: if post_time > start and post_time <= end:
publish('Social Media Post', post.name) sm_post = frappe.get_doc('Social Media Post', post.name)
sm_post.post()
@frappe.whitelist()
def publish(doctype, name):
sm_post = frappe.get_doc(doctype, name)
sm_post.post()
frappe.db.commit()

View File

@@ -1,10 +1,11 @@
frappe.listview_settings['Social Media Post'] = { frappe.listview_settings['Social Media Post'] = {
add_fields: ["status","post_status"], add_fields: ["status", "post_status"],
get_indicator: function(doc) { get_indicator: function(doc) {
return [__(doc.post_status), { return [__(doc.post_status), {
"Scheduled": "orange", "Scheduled": "orange",
"Posted": "green", "Posted": "green",
"Error": "red" "Error": "red",
}[doc.post_status]]; "Deleted": "red"
} }[doc.post_status]];
}
} }

View File

@@ -2,7 +2,7 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Twitter Settings', { frappe.ui.form.on('Twitter Settings', {
onload: function(frm){ onload: function(frm) {
if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){
frappe.confirm( frappe.confirm(
__('Session not valid, Do you want to login?'), __('Session not valid, Do you want to login?'),
@@ -14,10 +14,11 @@ frappe.ui.form.on('Twitter Settings', {
} }
); );
} }
frm.dashboard.set_headline(__("For more information, {0}.", [`<a target='_blank' href='https://docs.erpnext.com/docs/user/manual/en/CRM/twitter-settings'>${__('Click here')}</a>`]));
}, },
refresh: function(frm){ refresh: function(frm) {
let msg, color, flag=false; let msg, color, flag=false;
if (frm.doc.session_status == "Active"){ if (frm.doc.session_status == "Active") {
msg = __("Session Active"); msg = __("Session Active");
color = 'green'; color = 'green';
flag = true; flag = true;
@@ -28,7 +29,7 @@ frappe.ui.form.on('Twitter Settings', {
flag = true; flag = true;
} }
if (flag){ if (flag) {
frm.dashboard.set_headline_alert( frm.dashboard.set_headline_alert(
`<div class="row"> `<div class="row">
<div class="col-xs-12"> <div class="col-xs-12">
@@ -38,7 +39,7 @@ frappe.ui.form.on('Twitter Settings', {
); );
} }
}, },
login: function(frm){ login: function(frm) {
if (frm.doc.consumer_key && frm.doc.consumer_secret){ if (frm.doc.consumer_key && frm.doc.consumer_secret){
frappe.dom.freeze(); frappe.dom.freeze();
frappe.call({ frappe.call({
@@ -52,7 +53,7 @@ frappe.ui.form.on('Twitter Settings', {
}); });
} }
}, },
after_save: function(frm){ after_save: function(frm) {
frm.trigger("login"); frm.trigger("login");
} }
}); });

View File

@@ -2,6 +2,7 @@
"actions": [], "actions": [],
"creation": "2020-01-30 10:29:08.562108", "creation": "2020-01-30 10:29:08.562108",
"doctype": "DocType", "doctype": "DocType",
"documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/twitter-settings",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
@@ -77,7 +78,7 @@
"image_field": "profile_pic", "image_field": "profile_pic",
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2020-05-13 17:50:47.934776", "modified": "2021-02-18 15:18:07.900031",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Twitter Settings", "name": "Twitter Settings",

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