Merge branch 'develop' of https://github.com/frappe/erpnext into party_account_currency_check

This commit is contained in:
Deepesh Garg
2021-08-25 20:47:16 +05:30
117 changed files with 2346 additions and 774 deletions

View File

@@ -21,6 +21,10 @@ class BankTransaction(StatusUpdater):
self.update_allocations() self.update_allocations()
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:
@@ -41,21 +45,46 @@ class BankTransaction(StatusUpdater):
frappe.db.set_value(self.doctype, self.name, "status", "Reconciled") frappe.db.set_value(self.doctype, self.name, "status", "Reconciled")
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("""

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

@@ -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

@@ -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 = self.get_gl_entries()
if gl_entries:
from erpnext.accounts.general_ledger import make_gl_entries
make_gl_entries(gl_entries)
def get_gl_entries(self):
gl_entries = [] gl_entries = []
net_pl_balance = 0
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) def get_pnl_gl_entry(self, 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
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):
@@ -120,6 +121,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",
@@ -1183,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",
@@ -1549,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-08-17 20:13:44.255437", "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

@@ -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

@@ -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,
@@ -692,10 +692,11 @@
{ {
"fieldname": "scan_barcode", "fieldname": "scan_barcode",
"fieldtype": "Data", "fieldtype": "Data",
"options": "Barcode",
"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,
@@ -1059,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
}, },
@@ -1145,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)",
@@ -1205,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",
@@ -1444,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",
@@ -1557,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
}, },
@@ -1644,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,
@@ -1703,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",
@@ -1714,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
@@ -2014,7 +2021,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2021-08-17 20:16:12.737743", "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

@@ -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')

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

@@ -240,14 +240,15 @@ def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details):
def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers): def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers):
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

@@ -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

@@ -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

@@ -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

@@ -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):
@@ -555,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

@@ -160,7 +160,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):
@@ -1853,6 +1854,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')

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)
@@ -685,9 +686,7 @@ class calculate_taxes_and_totals(object):
'mode_of_payment': default_mode_of_payment.mode_of_payment, 'mode_of_payment': default_mode_of_payment.mode_of_payment,
'amount': total_amount_to_pay, 'amount': total_amount_to_pay,
'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:

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

View File

@@ -32,7 +32,9 @@ class TwitterSettings(Document):
try: try:
auth.get_access_token(oauth_verifier) auth.get_access_token(oauth_verifier)
api = self.get_api(auth.access_token, auth.access_token_secret) self.access_token = auth.access_token
self.access_token_secret = auth.access_token_secret
api = self.get_api()
user = api.me() user = api.me()
profile_pic = (user._json["profile_image_url"]).replace("_normal","") profile_pic = (user._json["profile_image_url"]).replace("_normal","")
@@ -50,11 +52,11 @@ class TwitterSettings(Document):
frappe.msgprint(_("Error! Failed to get access token.")) frappe.msgprint(_("Error! Failed to get access token."))
frappe.throw(_('Invalid Consumer Key or Consumer Secret Key')) frappe.throw(_('Invalid Consumer Key or Consumer Secret Key'))
def get_api(self, access_token, access_token_secret): def get_api(self):
# authentication of consumer key and secret # authentication of consumer key and secret
auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
# authentication of access token and secret # authentication of access token and secret
auth.set_access_token(access_token, access_token_secret) auth.set_access_token(self.access_token, self.access_token_secret)
return tweepy.API(auth) return tweepy.API(auth)
@@ -68,13 +70,13 @@ class TwitterSettings(Document):
def upload_image(self, media): def upload_image(self, media):
media = get_file_path(media) media = get_file_path(media)
api = self.get_api(self.access_token, self.access_token_secret) api = self.get_api()
media = api.media_upload(media) media = api.media_upload(media)
return media.media_id return media.media_id
def send_tweet(self, text, media_id=None): def send_tweet(self, text, media_id=None):
api = self.get_api(self.access_token, self.access_token_secret) api = self.get_api()
try: try:
if media_id: if media_id:
response = api.update_status(status = text, media_ids = [media_id]) response = api.update_status(status = text, media_ids = [media_id])
@@ -84,12 +86,32 @@ class TwitterSettings(Document):
return response return response
except TweepError as e: except TweepError as e:
content = json.loads(e.response.content) self.api_error(e)
content = content["errors"][0]
if e.response.status_code == 401: def delete_tweet(self, tweet_id):
self.db_set("session_status", "Expired") api = self.get_api()
frappe.db.commit() try:
frappe.throw(content["message"],title="Twitter Error {0} {1}".format(e.response.status_code, e.response.reason)) api.destroy_status(tweet_id)
except TweepError as e:
self.api_error(e)
def get_tweet(self, tweet_id):
api = self.get_api()
try:
response = api.get_status(tweet_id, trim_user=True, include_entities=True)
except TweepError as e:
self.api_error(e)
return response._json
def api_error(self, e):
content = json.loads(e.response.content)
content = content["errors"][0]
if e.response.status_code == 401:
self.db_set("session_status", "Expired")
frappe.db.commit()
frappe.throw(content["message"],title=_("Twitter Error {0} : {1}").format(e.response.status_code, e.response.reason))
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def callback(oauth_token = None, oauth_verifier = None): def callback(oauth_token = None, oauth_verifier = None):

View File

@@ -29,10 +29,10 @@ class TestFeeValidity(unittest.TestCase):
healthcare_settings.save(ignore_permissions=True) healthcare_settings.save(ignore_permissions=True)
patient, medical_department, practitioner = create_healthcare_docs() patient, medical_department, practitioner = create_healthcare_docs()
# appointment should not be invoiced. Check Fee Validity created for new patient # For first appointment, invoice is generated
appointment = create_appointment(patient, practitioner, nowdate()) appointment = create_appointment(patient, practitioner, nowdate())
invoiced = frappe.db.get_value("Patient Appointment", appointment.name, "invoiced") invoiced = frappe.db.get_value("Patient Appointment", appointment.name, "invoiced")
self.assertEqual(invoiced, 0) self.assertEqual(invoiced, 1)
# appointment should not be invoiced as it is within fee validity # appointment should not be invoiced as it is within fee validity
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4)) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4))

View File

@@ -282,7 +282,7 @@
], ],
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2021-01-22 10:14:43.187675", "modified": "2021-08-24 10:42:08.513054",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Healthcare Practitioner", "name": "Healthcare Practitioner",
@@ -295,6 +295,7 @@
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Laboratory User", "role": "Laboratory User",
"select": 1,
"share": 1, "share": 1,
"write": 1 "write": 1
}, },
@@ -307,6 +308,7 @@
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Physician", "role": "Physician",
"select": 1,
"share": 1, "share": 1,
"write": 1 "write": 1
}, },
@@ -319,6 +321,7 @@
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Nursing User", "role": "Nursing User",
"select": 1,
"share": 1, "share": 1,
"write": 1 "write": 1
} }

View File

@@ -241,6 +241,13 @@ frappe.ui.form.on('Patient Appointment', {
frm.toggle_reqd('mode_of_payment', 0); frm.toggle_reqd('mode_of_payment', 0);
frm.toggle_reqd('paid_amount', 0); frm.toggle_reqd('paid_amount', 0);
frm.toggle_reqd('billing_item', 0); frm.toggle_reqd('billing_item', 0);
} else if (data.message) {
frm.toggle_display('mode_of_payment', 1);
frm.toggle_display('paid_amount', 1);
frm.toggle_display('billing_item', 1);
frm.toggle_reqd('mode_of_payment', 1);
frm.toggle_reqd('paid_amount', 1);
frm.toggle_reqd('billing_item', 1);
} else { } else {
// if automated appointment invoicing is disabled, hide fields // if automated appointment invoicing is disabled, hide fields
frm.toggle_display('mode_of_payment', data.message ? 1 : 0); frm.toggle_display('mode_of_payment', data.message ? 1 : 0);

View File

@@ -134,6 +134,7 @@
"set_only_once": 1 "set_only_once": 1
}, },
{ {
"depends_on": "eval:doc.practitioner;",
"fieldname": "section_break_12", "fieldname": "section_break_12",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Appointment Details" "label": "Appointment Details"
@@ -141,7 +142,6 @@
{ {
"fieldname": "practitioner", "fieldname": "practitioner",
"fieldtype": "Link", "fieldtype": "Link",
"ignore_user_permissions": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Healthcare Practitioner", "label": "Healthcare Practitioner",
"options": "Healthcare Practitioner", "options": "Healthcare Practitioner",
@@ -349,7 +349,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2021-02-08 13:13:15.116833", "modified": "2021-06-16 00:40:26.841794",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Patient Appointment", "name": "Patient Appointment",
@@ -400,4 +400,4 @@
"title_field": "title", "title_field": "title",
"track_changes": 1, "track_changes": 1,
"track_seen": 1 "track_seen": 1
} }

View File

@@ -136,8 +136,6 @@ def check_payment_fields_reqd(patient):
fee_validity = frappe.db.exists('Fee Validity', {'patient': patient, 'status': 'Pending'}) fee_validity = frappe.db.exists('Fee Validity', {'patient': patient, 'status': 'Pending'})
if fee_validity: if fee_validity:
return {'fee_validity': fee_validity} return {'fee_validity': fee_validity}
if check_is_new_patient(patient):
return False
return True return True
return False return False
@@ -152,8 +150,6 @@ def invoice_appointment(appointment_doc):
elif not fee_validity: elif not fee_validity:
if frappe.db.exists('Fee Validity Reference', {'appointment': appointment_doc.name}): if frappe.db.exists('Fee Validity Reference', {'appointment': appointment_doc.name}):
return return
if check_is_new_patient(appointment_doc.patient, appointment_doc.name):
return
else: else:
fee_validity = None fee_validity = None
@@ -199,9 +195,7 @@ def check_is_new_patient(patient, name=None):
filters['name'] = ('!=', name) filters['name'] = ('!=', name)
has_previous_appointment = frappe.db.exists('Patient Appointment', filters) has_previous_appointment = frappe.db.exists('Patient Appointment', filters)
if has_previous_appointment: return not has_previous_appointment
return False
return True
def get_appointment_item(appointment_doc, item): def get_appointment_item(appointment_doc, item):

View File

@@ -4,11 +4,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest import unittest
import frappe import frappe
from erpnext.healthcare.doctype.patient_appointment.patient_appointment import update_status, make_encounter from erpnext.healthcare.doctype.patient_appointment.patient_appointment import update_status, make_encounter, check_payment_fields_reqd, check_is_new_patient
from frappe.utils import nowdate, add_days, now_datetime from frappe.utils import nowdate, add_days, now_datetime
from frappe.utils.make_random import get_random from frappe.utils.make_random import get_random
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
class TestPatientAppointment(unittest.TestCase): class TestPatientAppointment(unittest.TestCase):
def setUp(self): def setUp(self):
frappe.db.sql("""delete from `tabPatient Appointment`""") frappe.db.sql("""delete from `tabPatient Appointment`""")
@@ -176,10 +177,55 @@ class TestPatientAppointment(unittest.TestCase):
mark_invoiced_inpatient_occupancy(ip_record1) mark_invoiced_inpatient_occupancy(ip_record1)
discharge_patient(ip_record1) discharge_patient(ip_record1)
def test_payment_should_be_mandatory_for_new_patient_appointment(self):
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
frappe.db.set_value('Healthcare Settings', None, 'max_visits', 3)
frappe.db.set_value('Healthcare Settings', None, 'valid_days', 30)
patient = create_patient()
assert check_is_new_patient(patient)
payment_required = check_payment_fields_reqd(patient)
assert payment_required is True
def test_sales_invoice_should_be_generated_for_new_patient_appointment(self):
patient, medical_department, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
invoice_count = frappe.db.count('Sales Invoice')
assert check_is_new_patient(patient)
create_appointment(patient, practitioner, nowdate())
new_invoice_count = frappe.db.count('Sales Invoice')
assert new_invoice_count == invoice_count + 1
def test_patient_appointment_should_consider_permissions_while_fetching_appointments(self):
patient, medical_department, practitioner = create_healthcare_docs()
create_appointment(patient, practitioner, nowdate())
patient, medical_department, new_practitioner = create_healthcare_docs(practitioner_name='Dr. John')
create_appointment(patient, new_practitioner, nowdate())
roles = [{"doctype": "Has Role", "role": "Physician"}]
user = create_user(roles=roles)
new_practitioner = frappe.get_doc('Healthcare Practitioner', new_practitioner)
new_practitioner.user_id = user.email
new_practitioner.save()
frappe.set_user(user.name)
appointments = frappe.get_list('Patient Appointment')
assert len(appointments) == 1
frappe.set_user("Administrator")
appointments = frappe.get_list('Patient Appointment')
assert len(appointments) == 2
def create_healthcare_docs(practitioner_name=None):
if not practitioner_name:
practitioner_name = '_Test Healthcare Practitioner'
def create_healthcare_docs():
patient = create_patient() patient = create_patient()
practitioner = frappe.db.exists('Healthcare Practitioner', '_Test Healthcare Practitioner') practitioner = frappe.db.exists('Healthcare Practitioner', practitioner_name)
medical_department = frappe.db.exists('Medical Department', '_Test Medical Department') medical_department = frappe.db.exists('Medical Department', '_Test Medical Department')
if not medical_department: if not medical_department:
@@ -190,7 +236,7 @@ def create_healthcare_docs():
if not practitioner: if not practitioner:
practitioner = frappe.new_doc('Healthcare Practitioner') practitioner = frappe.new_doc('Healthcare Practitioner')
practitioner.first_name = '_Test Healthcare Practitioner' practitioner.first_name = practitioner_name
practitioner.gender = 'Female' practitioner.gender = 'Female'
practitioner.department = medical_department practitioner.department = medical_department
practitioner.op_consulting_charge = 500 practitioner.op_consulting_charge = 500
@@ -297,3 +343,17 @@ def create_appointment_type(args=None):
'price_list': args.get('price_list') or frappe.db.get_value("Price List", {"selling": 1}), 'price_list': args.get('price_list') or frappe.db.get_value("Price List", {"selling": 1}),
'items': args.get('items') or items 'items': args.get('items') or items
}).insert() }).insert()
def create_user(email=None, roles=None):
if not email:
email = '{}@frappe.com'.format(frappe.utils.random_string(10))
user = frappe.db.exists('User', email)
if not user:
user = frappe.get_doc({
"doctype": "User",
"email": email,
"first_name": "test_user",
"password": "password",
"roles": roles,
}).insert()
return user

View File

@@ -355,7 +355,8 @@ scheduler_events = {
"erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity", "erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity",
"erpnext.controllers.accounts_controller.update_invoice_status", "erpnext.controllers.accounts_controller.update_invoice_status",
"erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year", "erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year",
"erpnext.hr.doctype.employee.employee.send_birthday_reminders", "erpnext.hr.doctype.employee.employee_reminders.send_work_anniversary_reminders",
"erpnext.hr.doctype.employee.employee_reminders.send_birthday_reminders",
"erpnext.projects.doctype.task.task.set_tasks_as_overdue", "erpnext.projects.doctype.task.task.set_tasks_as_overdue",
"erpnext.assets.doctype.asset.depreciation.post_depreciation_entries", "erpnext.assets.doctype.asset.depreciation.post_depreciation_entries",
"erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.send_summary", "erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.send_summary",
@@ -387,6 +388,12 @@ scheduler_events = {
"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans", "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
"erpnext.crm.doctype.lead.lead.daily_open_lead" "erpnext.crm.doctype.lead.lead.daily_open_lead"
], ],
"weekly": [
"erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_weekly"
],
"monthly": [
"erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_monthly"
],
"monthly_long": [ "monthly_long": [
"erpnext.accounts.deferred_revenue.process_deferred_accounting", "erpnext.accounts.deferred_revenue.process_deferred_accounting",
"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_demand_loans" "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_demand_loans"

View File

@@ -8,7 +8,7 @@ from frappe import _
from frappe.utils import date_diff, add_days, getdate, cint, format_date from frappe.utils import date_diff, add_days, getdate, cint, format_date
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, validate_active_employee, \ from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, validate_active_employee, \
get_holidays_for_employee, create_additional_leave_ledger_entry create_additional_leave_ledger_entry, get_holiday_dates_for_employee
class CompensatoryLeaveRequest(Document): class CompensatoryLeaveRequest(Document):
@@ -39,7 +39,7 @@ class CompensatoryLeaveRequest(Document):
frappe.throw(_("You are not present all day(s) between compensatory leave request days")) frappe.throw(_("You are not present all day(s) between compensatory leave request days"))
def validate_holidays(self): def validate_holidays(self):
holidays = get_holidays_for_employee(self.employee, self.work_from_date, self.work_end_date) holidays = get_holiday_dates_for_employee(self.employee, self.work_from_date, self.work_end_date)
if len(holidays) < date_diff(self.work_end_date, self.work_from_date) + 1: if len(holidays) < date_diff(self.work_end_date, self.work_from_date) + 1:
if date_diff(self.work_end_date, self.work_from_date): if date_diff(self.work_end_date, self.work_from_date):
msg = _("The days between {0} to {1} are not valid holidays.").format(frappe.bold(format_date(self.work_from_date)), frappe.bold(format_date(self.work_end_date))) msg = _("The days between {0} to {1} are not valid holidays.").format(frappe.bold(format_date(self.work_from_date)), frappe.bold(format_date(self.work_end_date)))

View File

@@ -1,7 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import getdate, validate_email_address, today, add_years, cstr from frappe.utils import getdate, validate_email_address, today, add_years, cstr
@@ -9,7 +7,6 @@ from frappe.model.naming import set_name_by_naming_series
from frappe import throw, _, scrub from frappe import throw, _, scrub
from frappe.permissions import add_user_permission, remove_user_permission, \ from frappe.permissions import add_user_permission, remove_user_permission, \
set_user_permission_if_allowed, has_permission, get_doc_permissions set_user_permission_if_allowed, has_permission, get_doc_permissions
from frappe.model.document import Document
from erpnext.utilities.transaction_base import delete_events from erpnext.utilities.transaction_base import delete_events
from frappe.utils.nestedset import NestedSet from frappe.utils.nestedset import NestedSet
@@ -286,94 +283,8 @@ def update_user_permissions(doc, method):
employee = frappe.get_doc("Employee", {"user_id": doc.name}) employee = frappe.get_doc("Employee", {"user_id": doc.name})
employee.update_user_permissions() employee.update_user_permissions()
def send_birthday_reminders():
"""Send Employee birthday reminders if no 'Stop Birthday Reminders' is not set."""
if int(frappe.db.get_single_value("HR Settings", "stop_birthday_reminders") or 0):
return
employees_born_today = get_employees_who_are_born_today()
for company, birthday_persons in employees_born_today.items():
employee_emails = get_all_employee_emails(company)
birthday_person_emails = [get_employee_email(doc) for doc in birthday_persons]
recipients = list(set(employee_emails) - set(birthday_person_emails))
reminder_text, message = get_birthday_reminder_text_and_message(birthday_persons)
send_birthday_reminder(recipients, reminder_text, birthday_persons, message)
if len(birthday_persons) > 1:
# special email for people sharing birthdays
for person in birthday_persons:
person_email = person["user_id"] or person["personal_email"] or person["company_email"]
others = [d for d in birthday_persons if d != person]
reminder_text, message = get_birthday_reminder_text_and_message(others)
send_birthday_reminder(person_email, reminder_text, others, message)
def get_employee_email(employee_doc): def get_employee_email(employee_doc):
return employee_doc["user_id"] or employee_doc["personal_email"] or employee_doc["company_email"] return employee_doc.get("user_id") or employee_doc.get("personal_email") or employee_doc.get("company_email")
def get_birthday_reminder_text_and_message(birthday_persons):
if len(birthday_persons) == 1:
birthday_person_text = birthday_persons[0]['name']
else:
# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
person_names = [d['name'] for d in birthday_persons]
last_person = person_names[-1]
birthday_person_text = ", ".join(person_names[:-1])
birthday_person_text = _("{} & {}").format(birthday_person_text, last_person)
reminder_text = _("Today is {0}'s birthday 🎉").format(birthday_person_text)
message = _("A friendly reminder of an important date for our team.")
message += "<br>"
message += _("Everyone, lets congratulate {0} on their birthday.").format(birthday_person_text)
return reminder_text, message
def send_birthday_reminder(recipients, reminder_text, birthday_persons, message):
frappe.sendmail(
recipients=recipients,
subject=_("Birthday Reminder"),
template="birthday_reminder",
args=dict(
reminder_text=reminder_text,
birthday_persons=birthday_persons,
message=message,
),
header=_("Birthday Reminder 🎂")
)
def get_employees_who_are_born_today():
"""Get all employee born today & group them based on their company"""
from collections import defaultdict
employees_born_today = frappe.db.multisql({
"mariadb": """
SELECT `personal_email`, `company`, `company_email`, `user_id`, `employee_name` AS 'name', `image`
FROM `tabEmployee`
WHERE
DAY(date_of_birth) = DAY(%(today)s)
AND
MONTH(date_of_birth) = MONTH(%(today)s)
AND
`status` = 'Active'
""",
"postgres": """
SELECT "personal_email", "company", "company_email", "user_id", "employee_name" AS 'name', "image"
FROM "tabEmployee"
WHERE
DATE_PART('day', "date_of_birth") = date_part('day', %(today)s)
AND
DATE_PART('month', "date_of_birth") = date_part('month', %(today)s)
AND
"status" = 'Active'
""",
}, dict(today=today()), as_dict=1)
grouped_employees = defaultdict(lambda: [])
for employee_doc in employees_born_today:
grouped_employees[employee_doc.get('company')].append(employee_doc)
return grouped_employees
def get_holiday_list_for_employee(employee, raise_exception=True): def get_holiday_list_for_employee(employee, raise_exception=True):
if employee: if employee:
@@ -390,17 +301,40 @@ def get_holiday_list_for_employee(employee, raise_exception=True):
return holiday_list return holiday_list
def is_holiday(employee, date=None, raise_exception=True): def is_holiday(employee, date=None, raise_exception=True, only_non_weekly=False, with_description=False):
'''Returns True if given Employee has an holiday on the given date '''
:param employee: Employee `name` Returns True if given Employee has an holiday on the given date
:param date: Date to check. Will check for today if None''' :param employee: Employee `name`
:param date: Date to check. Will check for today if None
:param raise_exception: Raise an exception if no holiday list found, default is True
:param only_non_weekly: Check only non-weekly holidays, default is False
'''
holiday_list = get_holiday_list_for_employee(employee, raise_exception) holiday_list = get_holiday_list_for_employee(employee, raise_exception)
if not date: if not date:
date = today() date = today()
if holiday_list: if not holiday_list:
return frappe.get_all('Holiday List', dict(name=holiday_list, holiday_date=date)) and True or False return False
filters = {
'parent': holiday_list,
'holiday_date': date
}
if only_non_weekly:
filters['weekly_off'] = False
holidays = frappe.get_all(
'Holiday',
fields=['description'],
filters=filters,
pluck='description'
)
if with_description:
return len(holidays) > 0, holidays
return len(holidays) > 0
@frappe.whitelist() @frappe.whitelist()
def deactivate_sales_person(status = None, employee = None): def deactivate_sales_person(status = None, employee = None):
@@ -503,7 +437,6 @@ def get_children(doctype, parent=None, company=None, is_root=False, is_tree=Fals
return employees return employees
def on_doctype_update(): def on_doctype_update():
frappe.db.add_index("Employee", ["lft", "rgt"]) frappe.db.add_index("Employee", ["lft", "rgt"])

View File

@@ -0,0 +1,247 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe import _
from frappe.utils import comma_sep, getdate, today, add_months, add_days
from erpnext.hr.doctype.employee.employee import get_all_employee_emails, get_employee_email
from erpnext.hr.utils import get_holidays_for_employee
# -----------------
# HOLIDAY REMINDERS
# -----------------
def send_reminders_in_advance_weekly():
to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders") or 1)
frequency = frappe.db.get_single_value("HR Settings", "frequency")
if not (to_send_in_advance and frequency == "Weekly"):
return
send_advance_holiday_reminders("Weekly")
def send_reminders_in_advance_monthly():
to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders") or 1)
frequency = frappe.db.get_single_value("HR Settings", "frequency")
if not (to_send_in_advance and frequency == "Monthly"):
return
send_advance_holiday_reminders("Monthly")
def send_advance_holiday_reminders(frequency):
"""Send Holiday Reminders in Advance to Employees
`frequency` (str): 'Weekly' or 'Monthly'
"""
if frequency == "Weekly":
start_date = getdate()
end_date = add_days(getdate(), 7)
elif frequency == "Monthly":
# Sent on 1st of every month
start_date = getdate()
end_date = add_months(getdate(), 1)
else:
return
employees = frappe.db.get_all('Employee', pluck='name')
for employee in employees:
holidays = get_holidays_for_employee(
employee,
start_date, end_date,
only_non_weekly=True,
raise_exception=False
)
if not (holidays is None):
send_holidays_reminder_in_advance(employee, holidays)
def send_holidays_reminder_in_advance(employee, holidays):
employee_doc = frappe.get_doc('Employee', employee)
employee_email = get_employee_email(employee_doc)
frequency = frappe.db.get_single_value("HR Settings", "frequency")
email_header = _("Holidays this Month.") if frequency == "Monthly" else _("Holidays this Week.")
frappe.sendmail(
recipients=[employee_email],
subject=_("Upcoming Holidays Reminder"),
template="holiday_reminder",
args=dict(
reminder_text=_("Hey {}! This email is to remind you about the upcoming holidays.").format(employee_doc.get('first_name')),
message=_("Below is the list of upcoming holidays for you:"),
advance_holiday_reminder=True,
holidays=holidays,
frequency=frequency[:-2]
),
header=email_header
)
# ------------------
# BIRTHDAY REMINDERS
# ------------------
def send_birthday_reminders():
"""Send Employee birthday reminders if no 'Stop Birthday Reminders' is not set."""
to_send = int(frappe.db.get_single_value("HR Settings", "send_birthday_reminders") or 1)
if not to_send:
return
employees_born_today = get_employees_who_are_born_today()
for company, birthday_persons in employees_born_today.items():
employee_emails = get_all_employee_emails(company)
birthday_person_emails = [get_employee_email(doc) for doc in birthday_persons]
recipients = list(set(employee_emails) - set(birthday_person_emails))
reminder_text, message = get_birthday_reminder_text_and_message(birthday_persons)
send_birthday_reminder(recipients, reminder_text, birthday_persons, message)
if len(birthday_persons) > 1:
# special email for people sharing birthdays
for person in birthday_persons:
person_email = person["user_id"] or person["personal_email"] or person["company_email"]
others = [d for d in birthday_persons if d != person]
reminder_text, message = get_birthday_reminder_text_and_message(others)
send_birthday_reminder(person_email, reminder_text, others, message)
def get_birthday_reminder_text_and_message(birthday_persons):
if len(birthday_persons) == 1:
birthday_person_text = birthday_persons[0]['name']
else:
# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
person_names = [d['name'] for d in birthday_persons]
birthday_person_text = comma_sep(person_names, frappe._("{0} & {1}"), False)
reminder_text = _("Today is {0}'s birthday 🎉").format(birthday_person_text)
message = _("A friendly reminder of an important date for our team.")
message += "<br>"
message += _("Everyone, lets congratulate {0} on their birthday.").format(birthday_person_text)
return reminder_text, message
def send_birthday_reminder(recipients, reminder_text, birthday_persons, message):
frappe.sendmail(
recipients=recipients,
subject=_("Birthday Reminder"),
template="birthday_reminder",
args=dict(
reminder_text=reminder_text,
birthday_persons=birthday_persons,
message=message,
),
header=_("Birthday Reminder 🎂")
)
def get_employees_who_are_born_today():
"""Get all employee born today & group them based on their company"""
return get_employees_having_an_event_today("birthday")
def get_employees_having_an_event_today(event_type):
"""Get all employee who have `event_type` today
& group them based on their company. `event_type`
can be `birthday` or `work_anniversary`"""
from collections import defaultdict
# Set column based on event type
if event_type == 'birthday':
condition_column = 'date_of_birth'
elif event_type == 'work_anniversary':
condition_column = 'date_of_joining'
else:
return
employees_born_today = frappe.db.multisql({
"mariadb": f"""
SELECT `personal_email`, `company`, `company_email`, `user_id`, `employee_name` AS 'name', `image`, `date_of_joining`
FROM `tabEmployee`
WHERE
DAY({condition_column}) = DAY(%(today)s)
AND
MONTH({condition_column}) = MONTH(%(today)s)
AND
`status` = 'Active'
""",
"postgres": f"""
SELECT "personal_email", "company", "company_email", "user_id", "employee_name" AS 'name', "image"
FROM "tabEmployee"
WHERE
DATE_PART('day', {condition_column}) = date_part('day', %(today)s)
AND
DATE_PART('month', {condition_column}) = date_part('month', %(today)s)
AND
"status" = 'Active'
""",
}, dict(today=today(), condition_column=condition_column), as_dict=1)
grouped_employees = defaultdict(lambda: [])
for employee_doc in employees_born_today:
grouped_employees[employee_doc.get('company')].append(employee_doc)
return grouped_employees
# --------------------------
# WORK ANNIVERSARY REMINDERS
# --------------------------
def send_work_anniversary_reminders():
"""Send Employee Work Anniversary Reminders if 'Send Work Anniversary Reminders' is checked"""
to_send = int(frappe.db.get_single_value("HR Settings", "send_work_anniversary_reminders") or 1)
if not to_send:
return
employees_joined_today = get_employees_having_an_event_today("work_anniversary")
for company, anniversary_persons in employees_joined_today.items():
employee_emails = get_all_employee_emails(company)
anniversary_person_emails = [get_employee_email(doc) for doc in anniversary_persons]
recipients = list(set(employee_emails) - set(anniversary_person_emails))
reminder_text, message = get_work_anniversary_reminder_text_and_message(anniversary_persons)
send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message)
if len(anniversary_persons) > 1:
# email for people sharing work anniversaries
for person in anniversary_persons:
person_email = person["user_id"] or person["personal_email"] or person["company_email"]
others = [d for d in anniversary_persons if d != person]
reminder_text, message = get_work_anniversary_reminder_text_and_message(others)
send_work_anniversary_reminder(person_email, reminder_text, others, message)
def get_work_anniversary_reminder_text_and_message(anniversary_persons):
if len(anniversary_persons) == 1:
anniversary_person = anniversary_persons[0]['name']
persons_name = anniversary_person
# Number of years completed at the company
completed_years = getdate().year - anniversary_persons[0]['date_of_joining'].year
anniversary_person += f" completed {completed_years} years"
else:
person_names_with_years = []
names = []
for person in anniversary_persons:
person_text = person['name']
names.append(person_text)
# Number of years completed at the company
completed_years = getdate().year - person['date_of_joining'].year
person_text += f" completed {completed_years} years"
person_names_with_years.append(person_text)
# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
anniversary_person = comma_sep(person_names_with_years, frappe._("{0} & {1}"), False)
persons_name = comma_sep(names, frappe._("{0} & {1}"), False)
reminder_text = _("Today {0} at our Company! 🎉").format(anniversary_person)
message = _("A friendly reminder of an important date for our team.")
message += "<br>"
message += _("Everyone, lets congratulate {0} on their work anniversary!").format(persons_name)
return reminder_text, message
def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message):
frappe.sendmail(
recipients=recipients,
subject=_("Work Anniversary Reminder"),
template="anniversary_reminder",
args=dict(
reminder_text=reminder_text,
anniversary_persons=anniversary_persons,
message=message,
),
header=_("🎊️🎊️ Work Anniversary Reminder 🎊️🎊️")
)

View File

@@ -1,7 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe import frappe
import erpnext import erpnext
@@ -12,29 +10,6 @@ from erpnext.hr.doctype.employee.employee import InactiveEmployeeStatusError
test_records = frappe.get_test_records('Employee') test_records = frappe.get_test_records('Employee')
class TestEmployee(unittest.TestCase): class TestEmployee(unittest.TestCase):
def test_birthday_reminders(self):
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
employee.date_of_birth = "1992" + frappe.utils.nowdate()[4:]
employee.company_email = "test@example.com"
employee.company = "_Test Company"
employee.save()
from erpnext.hr.doctype.employee.employee import get_employees_who_are_born_today, send_birthday_reminders
employees_born_today = get_employees_who_are_born_today()
self.assertTrue(employees_born_today.get("_Test Company"))
frappe.db.sql("delete from `tabEmail Queue`")
hr_settings = frappe.get_doc("HR Settings", "HR Settings")
hr_settings.stop_birthday_reminders = 0
hr_settings.save()
send_birthday_reminders()
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertTrue("Subject: Birthday Reminder" in email_queue[0].message)
def test_employee_status_left(self): def test_employee_status_left(self):
employee1 = make_employee("test_employee_1@company.com") employee1 = make_employee("test_employee_1@company.com")
employee2 = make_employee("test_employee_2@company.com") employee2 = make_employee("test_employee_2@company.com")

View File

@@ -0,0 +1,173 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
import unittest
from frappe.utils import getdate
from datetime import timedelta
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.hr_settings.hr_settings import set_proceed_with_frequency_change
class TestEmployeeReminders(unittest.TestCase):
@classmethod
def setUpClass(cls):
from erpnext.hr.doctype.holiday_list.test_holiday_list import make_holiday_list
# Create a test holiday list
test_holiday_dates = cls.get_test_holiday_dates()
test_holiday_list = make_holiday_list(
'TestHolidayRemindersList',
holiday_dates=[
{'holiday_date': test_holiday_dates[0], 'description': 'test holiday1'},
{'holiday_date': test_holiday_dates[1], 'description': 'test holiday2'},
{'holiday_date': test_holiday_dates[2], 'description': 'test holiday3', 'weekly_off': 1},
{'holiday_date': test_holiday_dates[3], 'description': 'test holiday4'},
{'holiday_date': test_holiday_dates[4], 'description': 'test holiday5'},
{'holiday_date': test_holiday_dates[5], 'description': 'test holiday6'},
],
from_date=getdate()-timedelta(days=10),
to_date=getdate()+timedelta(weeks=5)
)
# Create a test employee
test_employee = frappe.get_doc(
'Employee',
make_employee('test@gopher.io', company="_Test Company")
)
# Attach the holiday list to employee
test_employee.holiday_list = test_holiday_list.name
test_employee.save()
# Attach to class
cls.test_employee = test_employee
cls.test_holiday_dates = test_holiday_dates
@classmethod
def get_test_holiday_dates(cls):
today_date = getdate()
return [
today_date,
today_date-timedelta(days=4),
today_date-timedelta(days=3),
today_date+timedelta(days=1),
today_date+timedelta(days=3),
today_date+timedelta(weeks=3)
]
def setUp(self):
# Clear Email Queue
frappe.db.sql("delete from `tabEmail Queue`")
def test_is_holiday(self):
from erpnext.hr.doctype.employee.employee import is_holiday
self.assertTrue(is_holiday(self.test_employee.name))
self.assertTrue(is_holiday(self.test_employee.name, date=self.test_holiday_dates[1]))
self.assertFalse(is_holiday(self.test_employee.name, date=getdate()-timedelta(days=1)))
# Test weekly_off holidays
self.assertTrue(is_holiday(self.test_employee.name, date=self.test_holiday_dates[2]))
self.assertFalse(is_holiday(self.test_employee.name, date=self.test_holiday_dates[2], only_non_weekly=True))
# Test with descriptions
has_holiday, descriptions = is_holiday(self.test_employee.name, with_description=True)
self.assertTrue(has_holiday)
self.assertTrue('test holiday1' in descriptions)
def test_birthday_reminders(self):
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
employee.date_of_birth = "1992" + frappe.utils.nowdate()[4:]
employee.company_email = "test@example.com"
employee.company = "_Test Company"
employee.save()
from erpnext.hr.doctype.employee.employee_reminders import get_employees_who_are_born_today, send_birthday_reminders
employees_born_today = get_employees_who_are_born_today()
self.assertTrue(employees_born_today.get("_Test Company"))
hr_settings = frappe.get_doc("HR Settings", "HR Settings")
hr_settings.send_birthday_reminders = 1
hr_settings.save()
send_birthday_reminders()
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertTrue("Subject: Birthday Reminder" in email_queue[0].message)
def test_work_anniversary_reminders(self):
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
employee.date_of_joining = "1998" + frappe.utils.nowdate()[4:]
employee.company_email = "test@example.com"
employee.company = "_Test Company"
employee.save()
from erpnext.hr.doctype.employee.employee_reminders import get_employees_having_an_event_today, send_work_anniversary_reminders
employees_having_work_anniversary = get_employees_having_an_event_today('work_anniversary')
self.assertTrue(employees_having_work_anniversary.get("_Test Company"))
hr_settings = frappe.get_doc("HR Settings", "HR Settings")
hr_settings.send_work_anniversary_reminders = 1
hr_settings.save()
send_work_anniversary_reminders()
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertTrue("Subject: Work Anniversary Reminder" in email_queue[0].message)
def test_send_holidays_reminder_in_advance(self):
from erpnext.hr.utils import get_holidays_for_employee
from erpnext.hr.doctype.employee.employee_reminders import send_holidays_reminder_in_advance
# Get HR settings and enable advance holiday reminders
hr_settings = frappe.get_doc("HR Settings", "HR Settings")
hr_settings.send_holiday_reminders = 1
set_proceed_with_frequency_change()
hr_settings.frequency = 'Weekly'
hr_settings.save()
holidays = get_holidays_for_employee(
self.test_employee.get('name'),
getdate(), getdate() + timedelta(days=3),
only_non_weekly=True,
raise_exception=False
)
send_holidays_reminder_in_advance(
self.test_employee.get('name'),
holidays
)
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertEqual(len(email_queue), 1)
def test_advance_holiday_reminders_monthly(self):
from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_monthly
# Get HR settings and enable advance holiday reminders
hr_settings = frappe.get_doc("HR Settings", "HR Settings")
hr_settings.send_holiday_reminders = 1
set_proceed_with_frequency_change()
hr_settings.frequency = 'Monthly'
hr_settings.save()
send_reminders_in_advance_monthly()
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertTrue(len(email_queue) > 0)
def test_advance_holiday_reminders_weekly(self):
from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_weekly
# Get HR settings and enable advance holiday reminders
hr_settings = frappe.get_doc("HR Settings", "HR Settings")
hr_settings.send_holiday_reminders = 1
hr_settings.frequency = 'Weekly'
hr_settings.save()
send_reminders_in_advance_weekly()
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertTrue(len(email_queue) > 0)

View File

@@ -11,8 +11,14 @@
"emp_created_by", "emp_created_by",
"column_break_4", "column_break_4",
"standard_working_hours", "standard_working_hours",
"stop_birthday_reminders",
"expense_approver_mandatory_in_expense_claim", "expense_approver_mandatory_in_expense_claim",
"reminders_section",
"send_birthday_reminders",
"column_break_9",
"send_work_anniversary_reminders",
"column_break_11",
"send_holiday_reminders",
"frequency",
"leave_settings", "leave_settings",
"send_leave_notification", "send_leave_notification",
"leave_approval_notification_template", "leave_approval_notification_template",
@@ -50,13 +56,6 @@
"fieldname": "column_break_4", "fieldname": "column_break_4",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"default": "0",
"description": "Don't send employee birthday reminders",
"fieldname": "stop_birthday_reminders",
"fieldtype": "Check",
"label": "Stop Birthday Reminders"
},
{ {
"default": "1", "default": "1",
"fieldname": "expense_approver_mandatory_in_expense_claim", "fieldname": "expense_approver_mandatory_in_expense_claim",
@@ -142,13 +141,53 @@
"fieldname": "standard_working_hours", "fieldname": "standard_working_hours",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Standard Working Hours" "label": "Standard Working Hours"
},
{
"collapsible": 1,
"fieldname": "reminders_section",
"fieldtype": "Section Break",
"label": "Reminders"
},
{
"default": "1",
"fieldname": "send_holiday_reminders",
"fieldtype": "Check",
"label": "Holidays"
},
{
"default": "1",
"fieldname": "send_work_anniversary_reminders",
"fieldtype": "Check",
"label": "Work Anniversaries "
},
{
"default": "Weekly",
"depends_on": "eval:doc.send_holiday_reminders",
"fieldname": "frequency",
"fieldtype": "Select",
"label": "Set the frequency for holiday reminders",
"options": "Weekly\nMonthly"
},
{
"default": "1",
"fieldname": "send_birthday_reminders",
"fieldtype": "Check",
"label": "Birthdays"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
"idx": 1, "idx": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-05-11 10:52:56.192773", "modified": "2021-08-24 14:54:12.834162",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "HR Settings", "name": "HR Settings",

View File

@@ -1,17 +1,79 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import format_date
# Wether to proceed with frequency change
PROCEED_WITH_FREQUENCY_CHANGE = False
class HRSettings(Document): class HRSettings(Document):
def validate(self): def validate(self):
self.set_naming_series() self.set_naming_series()
# Based on proceed flag
global PROCEED_WITH_FREQUENCY_CHANGE
if not PROCEED_WITH_FREQUENCY_CHANGE:
self.validate_frequency_change()
PROCEED_WITH_FREQUENCY_CHANGE = False
def set_naming_series(self): def set_naming_series(self):
from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
set_by_naming_series("Employee", "employee_number", set_by_naming_series("Employee", "employee_number",
self.get("emp_created_by")=="Naming Series", hide_name_field=True) self.get("emp_created_by")=="Naming Series", hide_name_field=True)
def validate_frequency_change(self):
weekly_job, monthly_job = None, None
try:
weekly_job = frappe.get_doc(
'Scheduled Job Type',
'employee_reminders.send_reminders_in_advance_weekly'
)
monthly_job = frappe.get_doc(
'Scheduled Job Type',
'employee_reminders.send_reminders_in_advance_monthly'
)
except frappe.DoesNotExistError:
return
next_weekly_trigger = weekly_job.get_next_execution()
next_monthly_trigger = monthly_job.get_next_execution()
if self.freq_changed_from_monthly_to_weekly():
if next_monthly_trigger < next_weekly_trigger:
self.show_freq_change_warning(next_monthly_trigger, next_weekly_trigger)
elif self.freq_changed_from_weekly_to_monthly():
if next_monthly_trigger > next_weekly_trigger:
self.show_freq_change_warning(next_weekly_trigger, next_monthly_trigger)
def freq_changed_from_weekly_to_monthly(self):
return self.has_value_changed("frequency") and self.frequency == "Monthly"
def freq_changed_from_monthly_to_weekly(self):
return self.has_value_changed("frequency") and self.frequency == "Weekly"
def show_freq_change_warning(self, from_date, to_date):
from_date = frappe.bold(format_date(from_date))
to_date = frappe.bold(format_date(to_date))
frappe.msgprint(
msg=frappe._('Employees will miss holiday reminders from {} until {}. <br> Do you want to proceed with this change?').format(from_date, to_date),
title='Confirm change in Frequency',
primary_action={
'label': frappe._('Yes, Proceed'),
'client_action': 'erpnext.proceed_save_with_reminders_frequency_change'
},
raise_exception=frappe.ValidationError
)
@frappe.whitelist()
def set_proceed_with_frequency_change():
'''Enables proceed with frequency change'''
global PROCEED_WITH_FREQUENCY_CHANGE
PROCEED_WITH_FREQUENCY_CHANGE = True

View File

@@ -10,7 +10,7 @@ from frappe import _
from frappe.utils.csvutils import UnicodeWriter from frappe.utils.csvutils import UnicodeWriter
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.hr.utils import get_holidays_for_employee from erpnext.hr.utils import get_holiday_dates_for_employee
class UploadAttendance(Document): class UploadAttendance(Document):
pass pass
@@ -94,7 +94,7 @@ def get_holidays_for_employees(employees, from_date, to_date):
holidays = {} holidays = {}
for employee in employees: for employee in employees:
holiday_list = get_holiday_list_for_employee(employee) holiday_list = get_holiday_list_for_employee(employee)
holiday = get_holidays_for_employee(employee, getdate(from_date), getdate(to_date)) holiday = get_holiday_dates_for_employee(employee, getdate(from_date), getdate(to_date))
if holiday_list not in holidays: if holiday_list not in holidays:
holidays[holiday_list] = holiday holidays[holiday_list] = holiday

View File

@@ -335,21 +335,44 @@ def get_sal_slip_total_benefit_given(employee, payroll_period, component=False):
total_given_benefit_amount = sum_of_given_benefit[0].total_amount total_given_benefit_amount = sum_of_given_benefit[0].total_amount
return total_given_benefit_amount return total_given_benefit_amount
def get_holidays_for_employee(employee, start_date, end_date): def get_holiday_dates_for_employee(employee, start_date, end_date):
holiday_list = get_holiday_list_for_employee(employee) """return a list of holiday dates for the given employee between start_date and end_date"""
# return only date
holidays = get_holidays_for_employee(employee, start_date, end_date)
return [cstr(h.holiday_date) for h in holidays]
holidays = frappe.db.sql_list('''select holiday_date from `tabHoliday`
where
parent=%(holiday_list)s
and holiday_date >= %(start_date)s
and holiday_date <= %(end_date)s''', {
"holiday_list": holiday_list,
"start_date": start_date,
"end_date": end_date
})
holidays = [cstr(i) for i in holidays] def get_holidays_for_employee(employee, start_date, end_date, raise_exception=True, only_non_weekly=False):
"""Get Holidays for a given employee
`employee` (str)
`start_date` (str or datetime)
`end_date` (str or datetime)
`raise_exception` (bool)
`only_non_weekly` (bool)
return: list of dicts with `holiday_date` and `description`
"""
holiday_list = get_holiday_list_for_employee(employee, raise_exception=raise_exception)
if not holiday_list:
return []
filters = {
'parent': holiday_list,
'holiday_date': ('between', [start_date, end_date])
}
if only_non_weekly:
filters['weekly_off'] = False
holidays = frappe.get_all(
'Holiday',
fields=['description', 'holiday_date'],
filters=filters
)
return holidays return holidays
@erpnext.allow_regional @erpnext.allow_regional

View File

@@ -148,6 +148,7 @@ class BOM(WebsiteGenerator):
self.set_plc_conversion_rate() self.set_plc_conversion_rate()
self.validate_uom_is_interger() self.validate_uom_is_interger()
self.set_bom_material_details() self.set_bom_material_details()
self.set_bom_scrap_items_detail()
self.validate_materials() self.validate_materials()
self.set_routing_operations() self.set_routing_operations()
self.validate_operations() self.validate_operations()
@@ -200,7 +201,7 @@ class BOM(WebsiteGenerator):
def set_bom_material_details(self): def set_bom_material_details(self):
for item in self.get("items"): for item in self.get("items"):
self.validate_bom_currecny(item) self.validate_bom_currency(item)
ret = self.get_bom_material_detail({ ret = self.get_bom_material_detail({
"company": self.company, "company": self.company,
@@ -219,6 +220,19 @@ class BOM(WebsiteGenerator):
if not item.get(r): if not item.get(r):
item.set(r, ret[r]) item.set(r, ret[r])
def set_bom_scrap_items_detail(self):
for item in self.get("scrap_items"):
args = {
"item_code": item.item_code,
"company": self.company,
"scrap_items": True,
"bom_no": '',
}
ret = self.get_bom_material_detail(args)
for key, value in ret.items():
if not item.get(key):
item.set(key, value)
@frappe.whitelist() @frappe.whitelist()
def get_bom_material_detail(self, args=None): def get_bom_material_detail(self, args=None):
""" Get raw material details like uom, desc and rate""" """ Get raw material details like uom, desc and rate"""
@@ -255,7 +269,7 @@ class BOM(WebsiteGenerator):
return ret_item return ret_item
def validate_bom_currecny(self, item): def validate_bom_currency(self, item):
if item.get('bom_no') and frappe.db.get_value('BOM', item.get('bom_no'), 'currency') != self.currency: if item.get('bom_no') and frappe.db.get_value('BOM', item.get('bom_no'), 'currency') != self.currency:
frappe.throw(_("Row {0}: Currency of the BOM #{1} should be equal to the selected currency {2}") frappe.throw(_("Row {0}: Currency of the BOM #{1} should be equal to the selected currency {2}")
.format(item.idx, item.bom_no, self.currency)) .format(item.idx, item.bom_no, self.currency))

View File

@@ -26,17 +26,17 @@ class JobCard(Document):
self.set_status() self.set_status()
self.validate_operation_id() self.validate_operation_id()
self.validate_sequence_id() self.validate_sequence_id()
self.get_sub_operations() self.set_sub_operations()
self.update_sub_operation_status() self.update_sub_operation_status()
def get_sub_operations(self): def set_sub_operations(self):
if self.operation: if self.operation:
self.sub_operations = [] self.sub_operations = []
for row in frappe.get_all("Sub Operation", for row in frappe.get_all('Sub Operation',
filters = {"parent": self.operation}, fields=["operation", "idx"]): filters = {'parent': self.operation}, fields=['operation', 'idx'], order_by='idx'):
row.status = "Pending" row.status = 'Pending'
row.sub_operation = row.operation row.sub_operation = row.operation
self.append("sub_operations", row) self.append('sub_operations', row)
def validate_time_logs(self): def validate_time_logs(self):
self.total_time_in_mins = 0.0 self.total_time_in_mins = 0.0
@@ -690,7 +690,7 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta
target.set('time_logs', []) target.set('time_logs', [])
target.set('employee', []) target.set('employee', [])
target.set('items', []) target.set('items', [])
target.get_sub_operations() target.set_sub_operations()
target.get_required_items() target.get_required_items()
target.validate_time_logs() target.validate_time_logs()

View File

@@ -275,6 +275,7 @@ erpnext.patches.v13_0.remove_attribute_field_from_item_variant_setting
erpnext.patches.v13_0.germany_make_custom_fields erpnext.patches.v13_0.germany_make_custom_fields
erpnext.patches.v13_0.germany_fill_debtor_creditor_number erpnext.patches.v13_0.germany_fill_debtor_creditor_number
erpnext.patches.v13_0.set_pos_closing_as_failed erpnext.patches.v13_0.set_pos_closing_as_failed
erpnext.patches.v13_0.rename_stop_to_send_birthday_reminders
execute:frappe.rename_doc("Workspace", "Loan Management", "Loans", force=True) execute:frappe.rename_doc("Workspace", "Loan Management", "Loans", force=True)
erpnext.patches.v13_0.update_timesheet_changes erpnext.patches.v13_0.update_timesheet_changes
erpnext.patches.v13_0.add_doctype_to_sla #14-06-2021 erpnext.patches.v13_0.add_doctype_to_sla #14-06-2021
@@ -294,5 +295,6 @@ erpnext.patches.v13_0.update_tds_check_field #3
erpnext.patches.v13_0.add_custom_field_for_south_africa #2 erpnext.patches.v13_0.add_custom_field_for_south_africa #2
erpnext.patches.v13_0.update_recipient_email_digest erpnext.patches.v13_0.update_recipient_email_digest
erpnext.patches.v13_0.shopify_deprecation_warning erpnext.patches.v13_0.shopify_deprecation_warning
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
erpnext.patches.v13_0.einvoicing_deprecation_warning erpnext.patches.v13_0.einvoicing_deprecation_warning
erpnext.patches.v14_0.delete_einvoicing_doctypes erpnext.patches.v14_0.delete_einvoicing_doctypes

View File

@@ -0,0 +1,23 @@
import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
frappe.reload_doc('hr', 'doctype', 'hr_settings')
try:
# Rename the field
rename_field('HR Settings', 'stop_birthday_reminders', 'send_birthday_reminders')
# Reverse the value
old_value = frappe.db.get_single_value('HR Settings', 'send_birthday_reminders')
frappe.db.set_value(
'HR Settings',
'HR Settings',
'send_birthday_reminders',
1 if old_value == 0 else 0
)
except Exception as e:
if e.args[0] != 1054:
raise

View File

@@ -0,0 +1,45 @@
# Copyright (c) 2019, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
"""
Reset Clearance Date for Payment Entries of type Internal Transfer that have only been reconciled with one Bank Transaction.
This will allow the Payment Entries to be reconciled with the second Bank Transaction using the Bank Reconciliation Tool.
"""
intra_company_pe = get_intra_company_payment_entries_with_clearance_dates()
reconciled_bank_transactions = get_reconciled_bank_transactions(intra_company_pe)
for payment_entry in reconciled_bank_transactions:
if len(reconciled_bank_transactions[payment_entry]) == 1:
frappe.db.set_value('Payment Entry', payment_entry, 'clearance_date', None)
def get_intra_company_payment_entries_with_clearance_dates():
return frappe.get_all(
'Payment Entry',
filters = {
'payment_type': 'Internal Transfer',
'clearance_date': ["not in", None]
},
pluck = 'name'
)
def get_reconciled_bank_transactions(intra_company_pe):
"""Returns dictionary where each key:value pair is Payment Entry : List of Bank Transactions reconciled with Payment Entry"""
reconciled_bank_transactions = {}
for payment_entry in intra_company_pe:
reconciled_bank_transactions[payment_entry] = frappe.get_all(
'Bank Transaction Payments',
filters = {
'payment_entry': payment_entry
},
pluck='parent'
)
return reconciled_bank_transactions

View File

@@ -9,7 +9,7 @@ from frappe.utils import date_diff, getdate, rounded, add_days, cstr, cint, flt
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period_days, get_period_factor from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period_days, get_period_factor
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure
from erpnext.hr.utils import get_sal_slip_total_benefit_given, get_holidays_for_employee, get_previous_claimed_amount, validate_active_employee from erpnext.hr.utils import get_sal_slip_total_benefit_given, get_holiday_dates_for_employee, get_previous_claimed_amount, validate_active_employee
class EmployeeBenefitApplication(Document): class EmployeeBenefitApplication(Document):
def validate(self): def validate(self):
@@ -139,7 +139,7 @@ def get_max_benefits_remaining(employee, on_date, payroll_period):
# Then the sum multiply with the no of lwp in that period # Then the sum multiply with the no of lwp in that period
# Include that amount to the prev_sal_slip_flexi_total to get the actual # Include that amount to the prev_sal_slip_flexi_total to get the actual
if have_depends_on_payment_days and per_day_amount_total > 0: if have_depends_on_payment_days and per_day_amount_total > 0:
holidays = get_holidays_for_employee(employee, payroll_period_obj.start_date, on_date) holidays = get_holiday_dates_for_employee(employee, payroll_period_obj.start_date, on_date)
working_days = date_diff(on_date, payroll_period_obj.start_date) + 1 working_days = date_diff(on_date, payroll_period_obj.start_date) + 1
leave_days = calculate_lwp(employee, payroll_period_obj.start_date, holidays, working_days) leave_days = calculate_lwp(employee, payroll_period_obj.start_date, holidays, working_days)
leave_days_amount = leave_days * per_day_amount_total leave_days_amount = leave_days * per_day_amount_total

View File

@@ -7,7 +7,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.utils import date_diff, getdate, formatdate, cint, month_diff, flt, add_months from frappe.utils import date_diff, getdate, formatdate, cint, month_diff, flt, add_months
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import get_holidays_for_employee from erpnext.hr.utils import get_holiday_dates_for_employee
class PayrollPeriod(Document): class PayrollPeriod(Document):
def validate(self): def validate(self):
@@ -65,7 +65,7 @@ def get_payroll_period_days(start_date, end_date, employee, company=None):
actual_no_of_days = date_diff(getdate(payroll_period[0][2]), getdate(payroll_period[0][1])) + 1 actual_no_of_days = date_diff(getdate(payroll_period[0][2]), getdate(payroll_period[0][1])) + 1
working_days = actual_no_of_days working_days = actual_no_of_days
if not cint(frappe.db.get_value("Payroll Settings", None, "include_holidays_in_total_working_days")): if not cint(frappe.db.get_value("Payroll Settings", None, "include_holidays_in_total_working_days")):
holidays = get_holidays_for_employee(employee, getdate(payroll_period[0][1]), getdate(payroll_period[0][2])) holidays = get_holiday_dates_for_employee(employee, getdate(payroll_period[0][1]), getdate(payroll_period[0][2]))
working_days -= len(holidays) working_days -= len(holidays)
return payroll_period[0][0], working_days, actual_no_of_days return payroll_period[0][0], working_days, actual_no_of_days
return False, False, False return False, False, False

View File

@@ -4,18 +4,11 @@
frappe.ui.form.on('Salary Component', { frappe.ui.form.on('Salary Component', {
setup: function(frm) { setup: function(frm) {
frm.set_query("account", "accounts", function(doc, cdt, cdn) { frm.set_query("account", "accounts", function(doc, cdt, cdn) {
let d = frappe.get_doc(cdt, cdn); var d = locals[cdt][cdn];
let root_type = "Liability";
if (frm.doc.type == "Deduction") {
root_type = "Expense";
}
return { return {
filters: { filters: {
"is_group": 0, "is_group": 0,
"company": d.company, "company": d.company
"root_type": root_type
} }
}; };
}); });

View File

@@ -11,6 +11,7 @@ from frappe.model.naming import make_autoname
from frappe import msgprint, _ from frappe import msgprint, _
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.hr.utils import get_holiday_dates_for_employee
from erpnext.utilities.transaction_base import TransactionBase from erpnext.utilities.transaction_base import TransactionBase
from frappe.utils.background_jobs import enqueue from frappe.utils.background_jobs import enqueue
from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries
@@ -337,20 +338,7 @@ class SalarySlip(TransactionBase):
return payment_days return payment_days
def get_holidays_for_employee(self, start_date, end_date): def get_holidays_for_employee(self, start_date, end_date):
holiday_list = get_holiday_list_for_employee(self.employee) return get_holiday_dates_for_employee(self.employee, start_date, end_date)
holidays = frappe.db.sql_list('''select holiday_date from `tabHoliday`
where
parent=%(holiday_list)s
and holiday_date >= %(start_date)s
and holiday_date <= %(end_date)s''', {
"holiday_list": holiday_list,
"start_date": start_date,
"end_date": end_date
})
holidays = [cstr(i) for i in holidays]
return holidays
def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days): def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days):
lwp = 0 lwp = 0

View File

@@ -754,8 +754,6 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
} }
}); });
this.frm.refresh_fields(); this.frm.refresh_fields();
this.calculate_paid_amount();
} }
set_default_payment(total_amount_to_pay, update_paid_amount) { set_default_payment(total_amount_to_pay, update_paid_amount) {

View File

@@ -2242,12 +2242,19 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
coupon_code() { coupon_code() {
var me = this; var me = this;
frappe.run_serially([ if (this.frm.doc.coupon_code) {
() => this.frm.doc.ignore_pricing_rule=1, frappe.run_serially([
() => me.ignore_pricing_rule(), () => this.frm.doc.ignore_pricing_rule=1,
() => this.frm.doc.ignore_pricing_rule=0, () => me.ignore_pricing_rule(),
() => me.apply_pricing_rule() () => this.frm.doc.ignore_pricing_rule=0,
]); () => me.apply_pricing_rule()
]);
} else {
frappe.run_serially([
() => this.frm.doc.ignore_pricing_rule=1,
() => me.ignore_pricing_rule()
]);
}
} }
}; };

View File

@@ -82,6 +82,17 @@ $.extend(erpnext, {
}); });
frappe.set_route('Form','Journal Entry', journal_entry.name); frappe.set_route('Form','Journal Entry', journal_entry.name);
}); });
},
proceed_save_with_reminders_frequency_change: () => {
frappe.ui.hide_open_dialog();
frappe.call({
method: 'erpnext.hr.doctype.hr_settings.hr_settings.set_proceed_with_frequency_change',
callback: () => {
cur_frm.save();
}
});
} }
}); });
@@ -563,7 +574,7 @@ erpnext.utils.update_child_items = function(opts) {
}, },
], ],
primary_action: function() { primary_action: function() {
const trans_items = this.get_values()["trans_items"]; const trans_items = this.get_values()["trans_items"].filter((item) => !!item.item_code);
frappe.call({ frappe.call({
method: 'erpnext.controllers.accounts_controller.update_child_qty_rate', method: 'erpnext.controllers.accounts_controller.update_child_qty_rate',
freeze: true, freeze: true,

View File

@@ -860,6 +860,8 @@
.invoice-fields { .invoice-fields {
overflow-y: scroll; overflow-y: scroll;
height: 100%;
padding-right: var(--padding-sm);
} }
} }

View File

@@ -531,6 +531,7 @@ def make_custom_fields(update=True):
fieldtype='Link', options='Salary Component', insert_after='hra_section'), fieldtype='Link', options='Salary Component', insert_after='hra_section'),
dict(fieldname='hra_component', label='HRA Component', dict(fieldname='hra_component', label='HRA Component',
fieldtype='Link', options='Salary Component', insert_after='basic_component'), fieldtype='Link', options='Salary Component', insert_after='basic_component'),
dict(fieldname='hra_column_break', fieldtype='Column Break', insert_after='hra_component'),
dict(fieldname='arrear_component', label='Arrear Component', dict(fieldname='arrear_component', label='Arrear Component',
fieldtype='Link', options='Salary Component', insert_after='hra_component'), fieldtype='Link', options='Salary Component', insert_after='hra_component'),
dict(fieldname='non_profit_section', label='Non Profit Settings', dict(fieldname='non_profit_section', label='Non Profit Settings',
@@ -539,6 +540,7 @@ def make_custom_fields(update=True):
fieldtype='Data', insert_after='non_profit_section'), fieldtype='Data', insert_after='non_profit_section'),
dict(fieldname='with_effect_from', label='80G With Effect From', dict(fieldname='with_effect_from', label='80G With Effect From',
fieldtype='Date', insert_after='company_80g_number'), fieldtype='Date', insert_after='company_80g_number'),
dict(fieldname='non_profit_column_break', fieldtype='Column Break', insert_after='with_effect_from'),
dict(fieldname='pan_details', label='PAN Number', dict(fieldname='pan_details', label='PAN Number',
fieldtype='Data', insert_after='with_effect_from') fieldtype='Data', insert_after='with_effect_from')
], ],

View File

@@ -475,7 +475,7 @@ def get_ewb_data(dt, dn):
ewaybills.append(data) ewaybills.append(data)
data = { data = {
'version': '1.0.1118', 'version': '1.0.0421',
'billLists': ewaybills 'billLists': ewaybills
} }

View File

@@ -6,9 +6,8 @@ import frappe
from frappe.utils import flt, cstr from frappe.utils import flt, cstr
from erpnext.controllers.taxes_and_totals import get_itemised_tax from erpnext.controllers.taxes_and_totals import get_itemised_tax
from frappe import _ from frappe import _
from frappe.core.doctype.file.file import remove_file from frappe.utils.file_manager import remove_file
from six import string_types from six import string_types
from frappe.desk.form.load import get_attachments
from erpnext.regional.italy import state_codes from erpnext.regional.italy import state_codes

View File

@@ -588,7 +588,7 @@ def get_json(filters, report_name, data):
fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year)
gst_json = {"version": "GST2.2.9", gst_json = {"version": "GST3.0.4",
"hash": "hash", "gstin": gstin, "fp": fp} "hash": "hash", "gstin": gstin, "fp": fp}
res = {} res = {}
@@ -765,7 +765,7 @@ def get_cdnr_reg_json(res, gstin):
"ntty": invoice[0]["document_type"], "ntty": invoice[0]["document_type"],
"pos": "%02d" % int(invoice[0]["place_of_supply"].split('-')[0]), "pos": "%02d" % int(invoice[0]["place_of_supply"].split('-')[0]),
"rchrg": invoice[0]["reverse_charge"], "rchrg": invoice[0]["reverse_charge"],
"inv_type": get_invoice_type_for_cdnr(invoice[0]) "inv_typ": get_invoice_type_for_cdnr(invoice[0])
} }
inv_item["itms"] = [] inv_item["itms"] = []

View File

@@ -20,6 +20,7 @@
"tax_withholding_category", "tax_withholding_category",
"default_bank_account", "default_bank_account",
"lead_name", "lead_name",
"opportunity_name",
"image", "image",
"column_break0", "column_break0",
"account_manager", "account_manager",
@@ -493,6 +494,14 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Tax Withholding Category", "label": "Tax Withholding Category",
"options": "Tax Withholding Category" "options": "Tax Withholding Category"
},
{
"fieldname": "opportunity_name",
"fieldtype": "Link",
"label": "From Opportunity",
"no_copy": 1,
"options": "Opportunity",
"print_hide": 1
} }
], ],
"icon": "fa fa-user", "icon": "fa fa-user",
@@ -500,7 +509,7 @@
"image_field": "image", "image_field": "image",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-01-28 12:54:57.258959", "modified": "2021-08-25 18:56:09.929905",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Customer", "name": "Customer",

View File

@@ -394,6 +394,10 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
} }
_set_batch_number(doc) { _set_batch_number(doc) {
if (doc.batch_no) {
return
}
let args = {'item_code': doc.item_code, 'warehouse': doc.warehouse, 'qty': flt(doc.qty) * flt(doc.conversion_factor)}; let args = {'item_code': doc.item_code, 'warehouse': doc.warehouse, 'qty': flt(doc.qty) * flt(doc.conversion_factor)};
if (doc.has_serial_no && doc.serial_no) { if (doc.has_serial_no && doc.serial_no) {
args['serial_no'] = doc.serial_no args['serial_no'] = doc.serial_no

View File

@@ -46,6 +46,43 @@ frappe.ui.form.on("Company", {
}); });
}, },
change_abbreviation(frm) {
var dialog = new frappe.ui.Dialog({
title: "Replace Abbr",
fields: [
{"fieldtype": "Data", "label": "New Abbreviation", "fieldname": "new_abbr",
"reqd": 1 },
{"fieldtype": "Button", "label": "Update", "fieldname": "update"},
]
});
dialog.fields_dict.update.$input.click(function() {
var args = dialog.get_values();
if (!args) return;
frappe.show_alert(__("Update in progress. It might take a while."));
return frappe.call({
method: "erpnext.setup.doctype.company.company.enqueue_replace_abbr",
args: {
"company": frm.doc.name,
"old": frm.doc.abbr,
"new": args.new_abbr
},
callback: function(r) {
if (r.exc) {
frappe.msgprint(__("There were errors."));
return;
} else {
frm.set_value("abbr", args.new_abbr);
}
dialog.hide();
frm.refresh();
},
btn: this
});
});
dialog.show();
},
company_name: function(frm) { company_name: function(frm) {
if(frm.doc.__islocal) { if(frm.doc.__islocal) {
// add missing " " arg in split method // add missing " " arg in split method
@@ -127,6 +164,10 @@ frappe.ui.form.on("Company", {
}, __('Manage')); }, __('Manage'));
} }
} }
frm.add_custom_button(__('Change Abbreviation'), () => {
frm.trigger('change_abbreviation');
}, __('Manage'));
} }
erpnext.company.set_chart_of_accounts_options(frm.doc); erpnext.company.set_chart_of_accounts_options(frm.doc);
@@ -204,43 +245,6 @@ erpnext.company.set_chart_of_accounts_options = function(doc) {
} }
} }
cur_frm.cscript.change_abbr = function() {
var dialog = new frappe.ui.Dialog({
title: "Replace Abbr",
fields: [
{"fieldtype": "Data", "label": "New Abbreviation", "fieldname": "new_abbr",
"reqd": 1 },
{"fieldtype": "Button", "label": "Update", "fieldname": "update"},
]
});
dialog.fields_dict.update.$input.click(function() {
var args = dialog.get_values();
if(!args) return;
frappe.show_alert(__("Update in progress. It might take a while."));
return frappe.call({
method: "erpnext.setup.doctype.company.company.enqueue_replace_abbr",
args: {
"company": cur_frm.doc.name,
"old": cur_frm.doc.abbr,
"new": args.new_abbr
},
callback: function(r) {
if(r.exc) {
frappe.msgprint(__("There were errors."));
return;
} else {
cur_frm.set_value("abbr", args.new_abbr);
}
dialog.hide();
cur_frm.refresh();
},
btn: this
})
});
dialog.show();
}
erpnext.company.setup_queries = function(frm) { erpnext.company.setup_queries = function(frm) {
$.each([ $.each([
["default_bank_account", {"account_type": "Bank"}], ["default_bank_account", {"account_type": "Bank"}],

View File

@@ -12,33 +12,48 @@
"details", "details",
"company_name", "company_name",
"abbr", "abbr",
"change_abbr", "default_currency",
"country",
"is_group", "is_group",
"cb0", "cb0",
"domain",
"parent_company",
"charts_section",
"default_currency",
"default_letter_head", "default_letter_head",
"default_holiday_list",
"default_finance_book",
"default_selling_terms",
"default_buying_terms",
"default_warehouse_for_sales_return",
"default_in_transit_warehouse",
"column_break_10",
"country",
"create_chart_of_accounts_based_on",
"chart_of_accounts",
"existing_company",
"tax_id", "tax_id",
"domain",
"date_of_establishment", "date_of_establishment",
"parent_company",
"company_info",
"company_logo",
"date_of_incorporation",
"phone_no",
"email",
"company_description",
"column_break1",
"date_of_commencement",
"fax",
"website",
"address_html",
"section_break_28",
"create_chart_of_accounts_based_on",
"existing_company",
"column_break_26",
"chart_of_accounts",
"charts_section",
"sales_settings", "sales_settings",
"monthly_sales_target", "default_buying_terms",
"sales_monthly_history", "sales_monthly_history",
"column_break_goals", "monthly_sales_target",
"transactions_annual_history",
"total_monthly_sales", "total_monthly_sales",
"column_break_goals",
"default_selling_terms",
"default_warehouse_for_sales_return",
"credit_limit",
"transactions_annual_history",
"hr_settings_section",
"default_holiday_list",
"default_expense_claim_payable_account",
"column_break_10",
"default_employee_advance_account",
"default_payroll_payable_account",
"default_settings", "default_settings",
"default_bank_account", "default_bank_account",
"default_cash_account", "default_cash_account",
@@ -52,24 +67,20 @@
"column_break0", "column_break0",
"allow_account_creation_against_child_company", "allow_account_creation_against_child_company",
"default_payable_account", "default_payable_account",
"default_employee_advance_account",
"default_expense_account", "default_expense_account",
"default_income_account", "default_income_account",
"default_deferred_revenue_account", "default_deferred_revenue_account",
"default_deferred_expense_account", "default_deferred_expense_account",
"default_payroll_payable_account",
"default_expense_claim_payable_account",
"default_discount_account", "default_discount_account",
"section_break_22",
"cost_center",
"column_break_26",
"credit_limit",
"payment_terms", "payment_terms",
"cost_center",
"default_finance_book",
"auto_accounting_for_stock_settings", "auto_accounting_for_stock_settings",
"enable_perpetual_inventory", "enable_perpetual_inventory",
"enable_perpetual_inventory_for_non_stock_items", "enable_perpetual_inventory_for_non_stock_items",
"default_inventory_account", "default_inventory_account",
"stock_adjustment_account", "stock_adjustment_account",
"default_in_transit_warehouse",
"column_break_32", "column_break_32",
"stock_received_but_not_billed", "stock_received_but_not_billed",
"service_received_but_not_billed", "service_received_but_not_billed",
@@ -79,25 +90,14 @@
"depreciation_expense_account", "depreciation_expense_account",
"series_for_depreciation_entry", "series_for_depreciation_entry",
"expenses_included_in_asset_valuation", "expenses_included_in_asset_valuation",
"repair_and_maintenance_account",
"column_break_40", "column_break_40",
"disposal_account", "disposal_account",
"depreciation_cost_center", "depreciation_cost_center",
"capital_work_in_progress_account", "capital_work_in_progress_account",
"repair_and_maintenance_account",
"asset_received_but_not_billed", "asset_received_but_not_billed",
"budget_detail", "budget_detail",
"exception_budget_approver_role", "exception_budget_approver_role",
"company_info",
"company_logo",
"date_of_incorporation",
"address_html",
"date_of_commencement",
"phone_no",
"fax",
"email",
"website",
"column_break1",
"company_description",
"registration_info", "registration_info",
"registration_details", "registration_details",
"lft", "lft",
@@ -127,12 +127,6 @@
"oldfieldtype": "Data", "oldfieldtype": "Data",
"reqd": 1 "reqd": 1
}, },
{
"depends_on": "eval:!doc.__islocal && in_list(frappe.user_roles, \"System Manager\")",
"fieldname": "change_abbr",
"fieldtype": "Button",
"label": "Change Abbreviation"
},
{ {
"bold": 1, "bold": 1,
"default": "0", "default": "0",
@@ -176,10 +170,9 @@
"label": "Company Description" "label": "Company Description"
}, },
{ {
"collapsible": 1,
"fieldname": "sales_settings", "fieldname": "sales_settings",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Sales Settings" "label": "Buying & Selling Settings"
}, },
{ {
"fieldname": "sales_monthly_history", "fieldname": "sales_monthly_history",
@@ -442,10 +435,6 @@
"no_copy": 1, "no_copy": 1,
"options": "Account" "options": "Account"
}, },
{
"fieldname": "section_break_22",
"fieldtype": "Section Break"
},
{ {
"depends_on": "eval:!doc.__islocal", "depends_on": "eval:!doc.__islocal",
"fieldname": "cost_center", "fieldname": "cost_center",
@@ -455,10 +444,6 @@
"no_copy": 1, "no_copy": 1,
"options": "Cost Center" "options": "Cost Center"
}, },
{
"fieldname": "column_break_26",
"fieldtype": "Column Break"
},
{ {
"depends_on": "eval:!doc.__islocal", "depends_on": "eval:!doc.__islocal",
"fieldname": "credit_limit", "fieldname": "credit_limit",
@@ -589,10 +574,10 @@
}, },
{ {
"collapsible": 1, "collapsible": 1,
"description": "For reference only.", "depends_on": "eval: doc.docstatus == 0 && doc.__islocal != 1",
"fieldname": "company_info", "fieldname": "company_info",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Company Info" "label": "Address & Contact"
}, },
{ {
"fieldname": "date_of_incorporation", "fieldname": "date_of_incorporation",
@@ -741,6 +726,20 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Repair and Maintenance Account", "label": "Repair and Maintenance Account",
"options": "Account" "options": "Account"
},
{
"fieldname": "section_break_28",
"fieldtype": "Section Break",
"label": "Chart of Accounts"
},
{
"fieldname": "hr_settings_section",
"fieldtype": "Section Break",
"label": "HR & Payroll Settings"
},
{
"fieldname": "column_break_26",
"fieldtype": "Column Break"
} }
], ],
"icon": "fa fa-building", "icon": "fa fa-building",
@@ -748,7 +747,7 @@
"image_field": "company_logo", "image_field": "company_logo",
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2021-05-12 16:51:08.187233", "modified": "2021-07-12 11:27:06.353860",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Company", "name": "Company",

View File

@@ -423,11 +423,11 @@ def replace_abbr(company, old, new):
_rename_record(d) _rename_record(d)
try: try:
frappe.db.auto_commit_on_many_writes = 1 frappe.db.auto_commit_on_many_writes = 1
frappe.db.set_value("Company", company, "abbr", new)
for dt in ["Warehouse", "Account", "Cost Center", "Department", for dt in ["Warehouse", "Account", "Cost Center", "Department",
"Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]: "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]:
_rename_records(dt) _rename_records(dt)
frappe.db.commit() frappe.db.commit()
frappe.db.set_value("Company", company, "abbr", new)
except Exception: except Exception:
frappe.log_error(title=_('Abbreviation Rename Error')) frappe.log_error(title=_('Abbreviation Rename Error'))

View File

@@ -356,3 +356,23 @@ erpnext.stock.delivery_note.set_print_hide = function(doc, cdt, cdn){
dn_fields['taxes'].print_hide = 0; dn_fields['taxes'].print_hide = 0;
} }
} }
frappe.tour['Delivery Note'] = [
{
fieldname: "customer",
title: __("Customer"),
description: __("This field is used to set the 'Customer'.")
},
{
fieldname: "items",
title: __("Items"),
description: __("This table is used to set details about the 'Item', 'Qty', 'Basic Rate', etc.") + " " +
__("Different 'Source Warehouse' and 'Target Warehouse' can be set for each row.")
},
{
fieldname: "set_posting_time",
title: __("Edit Posting Date and Time"),
description: __("This option can be checked to edit the 'Posting Date' and 'Posting Time' fields.")
}
]

View File

@@ -792,4 +792,4 @@ frappe.ui.form.on("UOM Conversion Detail", {
}); });
} }
} }
}) });

View File

@@ -1101,3 +1101,4 @@ function check_should_not_attach_bom_items(bom_no) {
} }
$.extend(cur_frm.cscript, new erpnext.stock.StockEntry({frm: cur_frm})); $.extend(cur_frm.cscript, new erpnext.stock.StockEntry({frm: cur_frm}));

View File

@@ -84,8 +84,6 @@
"oldfieldtype": "Section Break" "oldfieldtype": "Section Break"
}, },
{ {
"allow_on_submit": 1,
"default": "{purpose}",
"fieldname": "title", "fieldname": "title",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
@@ -630,7 +628,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-08-17 20:16:12.737743", "modified": "2021-08-20 19:19:31.514846",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Entry", "name": "Stock Entry",

View File

@@ -58,6 +58,7 @@ class StockEntry(StockController):
self.validate_posting_time() self.validate_posting_time()
self.validate_purpose() self.validate_purpose()
self.set_title()
self.validate_item() self.validate_item()
self.validate_customer_provided_item() self.validate_customer_provided_item()
self.validate_qty() self.validate_qty()
@@ -1608,6 +1609,14 @@ class StockEntry(StockController):
return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos))) return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
def set_title(self):
if frappe.flags.in_import and self.title:
# Allow updating title during data import/update
return
self.title = self.purpose
@frappe.whitelist() @frappe.whitelist()
def move_sample_to_retention_warehouse(company, items): def move_sample_to_retention_warehouse(company, items):
if isinstance(items, string_types): if isinstance(items, string_types):

View File

@@ -302,3 +302,4 @@ erpnext.stock.StockReconciliation = class StockReconciliation extends erpnext.st
}; };
cur_frm.cscript = new erpnext.stock.StockReconciliation({frm: cur_frm}); cur_frm.cscript = new erpnext.stock.StockReconciliation({frm: cur_frm});

View File

@@ -16,36 +16,3 @@ frappe.ui.form.on('Stock Settings', {
} }
}); });
frappe.tour['Stock Settings'] = [
{
fieldname: "item_naming_by",
title: __("Item Naming By"),
description: __("By default, the Item Name is set as per the Item Code entered. If you want Items to be named by a ") + "<a href='https://docs.erpnext.com/docs/user/manual/en/setting-up/settings/naming-series' target='_blank'>Naming Series</a>" + __(" choose the 'Naming Series' option."),
},
{
fieldname: "default_warehouse",
title: __("Default Warehouse"),
description: __("Set a Default Warehouse for Inventory Transactions. This will be fetched into the Default Warehouse in the Item master.")
},
{
fieldname: "allow_negative_stock",
title: __("Allow Negative Stock"),
description: __("This will allow stock items to be displayed in negative values. Using this option depends on your use case. With this option unchecked, the system warns before obstructing a transaction that is causing negative stock.")
},
{
fieldname: "valuation_method",
title: __("Valuation Method"),
description: __("Choose between FIFO and Moving Average Valuation Methods. Click ") + "<a href='https://docs.erpnext.com/docs/user/manual/en/stock/articles/item-valuation-fifo-and-moving-average' target='_blank'>here</a>" + __(" to know more about them.")
},
{
fieldname: "show_barcode_field",
title: __("Show Barcode Field"),
description: __("Show 'Scan Barcode' field above every child table to insert Items with ease.")
},
{
fieldname: "automatically_set_serial_nos_based_on_fifo",
title: __("Automatically Set Serial Nos based on FIFO"),
description: __("Serial numbers for stock will be set automatically based on the Items entered based on first in first out in transactions like Purchase/Sales Invoices, Delivery Notes, etc.")
}
];

View File

@@ -86,3 +86,4 @@ function convert_to_group_or_ledger(frm){
}) })
} }

View File

@@ -0,0 +1,56 @@
{
"creation": "2021-08-24 14:44:22.292652",
"docstatus": 0,
"doctype": "Form Tour",
"idx": 0,
"is_standard": 1,
"modified": "2021-08-25 16:31:31.441194",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry",
"owner": "Administrator",
"reference_doctype": "Stock Entry",
"save_on_complete": 1,
"steps": [
{
"description": "Select the type of Stock Entry to be made. For now, to receive stock into a warehouses select <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/stock/articles/stock-entry-purpose#2purpose-material-receipt\" target=\"_blank\">Material Receipt.</a>",
"field": "",
"fieldname": "stock_entry_type",
"fieldtype": "Link",
"has_next_condition": 1,
"is_table_field": 0,
"label": "Stock Entry Type",
"next_step_condition": "eval: doc.stock_entry_type === \"Material Receipt\"",
"parent_field": "",
"position": "Top",
"title": "Stock Entry Type"
},
{
"description": "Select a target warehouse where the stock will be received.",
"field": "",
"fieldname": "to_warehouse",
"fieldtype": "Link",
"has_next_condition": 1,
"is_table_field": 0,
"label": "Default Target Warehouse",
"next_step_condition": "eval: doc.to_warehouse",
"parent_field": "",
"position": "Top",
"title": "Default Target Warehouse"
},
{
"description": "Select an item and entry quantity to be delivered.",
"field": "",
"fieldname": "items",
"fieldtype": "Table",
"has_next_condition": 1,
"is_table_field": 0,
"label": "Items",
"next_step_condition": "eval: doc.items[0]?.item_code",
"parent_field": "",
"position": "Top",
"title": "Items"
}
],
"title": "Stock Entry"
}

View File

@@ -0,0 +1,55 @@
{
"creation": "2021-08-24 14:44:46.770952",
"docstatus": 0,
"doctype": "Form Tour",
"idx": 0,
"is_standard": 1,
"modified": "2021-08-25 16:26:11.718664",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation",
"owner": "Administrator",
"reference_doctype": "Stock Reconciliation",
"save_on_complete": 1,
"steps": [
{
"description": "Set Purpose to Opening Stock to set the stock opening balance.",
"field": "",
"fieldname": "purpose",
"fieldtype": "Select",
"has_next_condition": 1,
"is_table_field": 0,
"label": "Purpose",
"next_step_condition": "eval: doc.purpose === \"Opening Stock\"",
"parent_field": "",
"position": "Top",
"title": "Purpose"
},
{
"description": "Select the items for which the opening stock has to be set.",
"field": "",
"fieldname": "items",
"fieldtype": "Table",
"has_next_condition": 1,
"is_table_field": 0,
"label": "Items",
"next_step_condition": "eval: doc.items[0]?.item_code",
"parent_field": "",
"position": "Top",
"title": "Items"
},
{
"description": "Edit the Posting Date by clicking on the Edit Posting Date and Time checkbox below.",
"field": "",
"fieldname": "posting_date",
"fieldtype": "Date",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Posting Date",
"parent_field": "",
"position": "Bottom",
"title": "Posting Date"
}
],
"title": "Stock Reconciliation"
}

View File

@@ -0,0 +1,89 @@
{
"creation": "2021-08-20 15:20:59.336585",
"docstatus": 0,
"doctype": "Form Tour",
"idx": 0,
"is_standard": 1,
"modified": "2021-08-25 16:19:37.699528",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",
"owner": "Administrator",
"reference_doctype": "Stock Settings",
"save_on_complete": 1,
"steps": [
{
"description": "By default, the Item Name is set as per the Item Code entered. If you want Items to be named by a Naming Series choose the 'Naming Series' option.",
"field": "",
"fieldname": "item_naming_by",
"fieldtype": "Select",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Item Naming By",
"parent_field": "",
"position": "Bottom",
"title": "Item Naming By"
},
{
"description": "Set a Default Warehouse for Inventory Transactions. This will be fetched into the Default Warehouse in the Item master.",
"field": "",
"fieldname": "default_warehouse",
"fieldtype": "Link",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Default Warehouse",
"parent_field": "",
"position": "Bottom",
"title": "Default Warehouse"
},
{
"description": "Quality inspection is performed on the inward and outward movement of goods. Receipt and delivery transactions will be stopped or the user will be warned if the quality inspection is not performed.",
"field": "",
"fieldname": "action_if_quality_inspection_is_not_submitted",
"fieldtype": "Select",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Action If Quality Inspection Is Not Submitted",
"parent_field": "",
"position": "Bottom",
"title": "Action if Quality Inspection Is Not Submitted"
},
{
"description": "Serial numbers for stock will be set automatically based on the Items entered based on first in first out in transactions like Purchase/Sales Invoices, Delivery Notes, etc.",
"field": "",
"fieldname": "automatically_set_serial_nos_based_on_fifo",
"fieldtype": "Check",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Automatically Set Serial Nos Based on FIFO",
"parent_field": "",
"position": "Bottom",
"title": "Automatically Set Serial Nos based on FIFO"
},
{
"description": "Show 'Scan Barcode' field above every child table to insert Items with ease.",
"field": "",
"fieldname": "show_barcode_field",
"fieldtype": "Check",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Show Barcode Field in Stock Transactions",
"parent_field": "",
"position": "Bottom",
"title": "Show Barcode Field"
},
{
"description": "Choose between FIFO and Moving Average Valuation Methods. Click <a href=\"https://docs.erpnext.com/docs/user/manual/en/stock/articles/item-valuation-fifo-and-moving-average\" target=\"_blank\">here</a> to know more about them.",
"field": "",
"fieldname": "valuation_method",
"fieldtype": "Select",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Default Valuation Method",
"parent_field": "",
"position": "Bottom",
"title": "Default Valuation Method"
}
],
"title": "Stock Settings"
}

View File

@@ -0,0 +1,54 @@
{
"creation": "2021-08-24 14:43:44.465237",
"docstatus": 0,
"doctype": "Form Tour",
"idx": 0,
"is_standard": 1,
"modified": "2021-08-24 14:50:31.988256",
"modified_by": "Administrator",
"module": "Stock",
"name": "Warehouse",
"owner": "Administrator",
"reference_doctype": "Warehouse",
"save_on_complete": 1,
"steps": [
{
"description": "Select a name for the warehouse. This should reflect its location or purpose.",
"field": "",
"fieldname": "warehouse_name",
"fieldtype": "Data",
"has_next_condition": 1,
"is_table_field": 0,
"label": "Warehouse Name",
"next_step_condition": "eval: doc.warehouse_name",
"parent_field": "",
"position": "Bottom",
"title": "Warehouse Name"
},
{
"description": "Select a warehouse type to categorize the warehouse into a sub-group.",
"field": "",
"fieldname": "warehouse_type",
"fieldtype": "Link",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Warehouse Type",
"parent_field": "",
"position": "Top",
"title": "Warehouse Type"
},
{
"description": "Select an account to set a default account for all transactions with this warehouse.",
"field": "",
"fieldname": "account",
"fieldtype": "Link",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Account",
"parent_field": "",
"position": "Top",
"title": "Account"
}
],
"title": "Warehouse"
}

View File

@@ -278,6 +278,10 @@ def get_basic_details(args, item, overwrite_warehouse=True):
else: else:
args.uom = item.stock_uom args.uom = item.stock_uom
if (args.get("batch_no") and
item.name != frappe.get_cached_value('Batch', args.get("batch_no"), 'item')):
args['batch_no'] = ''
out = frappe._dict({ out = frappe._dict({
"item_code": item.name, "item_code": item.name,
"item_name": item.item_name, "item_name": item.item_name,

View File

@@ -19,32 +19,26 @@
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/stock", "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/stock",
"idx": 0, "idx": 0,
"is_complete": 0, "is_complete": 0,
"modified": "2020-10-14 14:54:42.741971", "modified": "2021-08-20 14:38:55.570067",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock", "name": "Stock",
"owner": "Administrator", "owner": "Administrator",
"steps": [ "steps": [
{ {
"step": "Setup your Warehouse" "step": "Stock Settings"
}, },
{ {
"step": "Create a Product" "step": "Create a Warehouse"
},
{
"step": "Create a Supplier"
},
{
"step": "Introduction to Stock Entry"
}, },
{ {
"step": "Create a Stock Entry" "step": "Create a Stock Entry"
}, },
{ {
"step": "Create a Purchase Receipt" "step": "Stock Opening Balance"
}, },
{ {
"step": "Stock Settings" "step": "View Stock Projected Qty"
} }
], ],
"subtitle": "Inventory, Warehouses, Analysis, and more.", "subtitle": "Inventory, Warehouses, Analysis, and more.",

View File

@@ -1,19 +0,0 @@
{
"action": "Create Entry",
"creation": "2020-05-19 18:59:13.266713",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-10-14 14:53:25.618434",
"modified_by": "Administrator",
"name": "Create a Purchase Receipt",
"owner": "Administrator",
"reference_document": "Purchase Receipt",
"show_full_form": 1,
"title": "Create a Purchase Receipt",
"validate_action": 1
}

View File

@@ -1,19 +1,21 @@
{ {
"action": "Create Entry", "action": "Create Entry",
"action_label": "Create a Material Transfer Entry",
"creation": "2020-05-15 03:20:16.277043", "creation": "2020-05-15 03:20:16.277043",
"description": "# Manage Stock Movements\nStock entry allows you to register the movement of stock for various purposes like transfer, received, issues, repacked, etc. To address issues related to theft and pilferages, you can always ensure that the movement of goods happens against a document reference Stock Entry in ERPNext.\n\nLet\u2019s get a quick walk-through on the various scenarios covered in Stock Entry by watching [*this video*](https://www.youtube.com/watch?v=Njt107hlY3I).",
"docstatus": 0, "docstatus": 0,
"doctype": "Onboarding Step", "doctype": "Onboarding Step",
"idx": 0, "idx": 0,
"is_complete": 0, "is_complete": 0,
"is_mandatory": 0,
"is_single": 0, "is_single": 0,
"is_skipped": 0, "is_skipped": 0,
"modified": "2020-10-14 14:53:00.105905", "modified": "2021-06-18 13:57:11.434063",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Create a Stock Entry", "name": "Create a Stock Entry",
"owner": "Administrator", "owner": "Administrator",
"reference_document": "Stock Entry", "reference_document": "Stock Entry",
"show_form_tour": 1,
"show_full_form": 1, "show_full_form": 1,
"title": "Create a Stock Entry", "title": "Manage Stock Movements",
"validate_action": 1 "validate_action": 1
} }

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