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

This commit is contained in:
Deepesh Garg
2021-05-02 22:37:04 +05:30
107 changed files with 5163 additions and 1556 deletions

View File

@@ -80,14 +80,29 @@ jobs:
env: env:
TYPE: ${{ matrix.TYPE }} TYPE: ${{ matrix.TYPE }}
- name: Coverage - name: Coverage - Pull Request
if: matrix.TYPE == 'server' if: matrix.TYPE == 'server' && github.event_name == 'pull_request'
run: | run: |
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE} cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
cd ${GITHUB_WORKSPACE} cd ${GITHUB_WORKSPACE}
pip install coveralls==3.0.1 pip install coveralls==2.2.0
pip install coverage==5.5 pip install coverage==4.5.4
coveralls --service=github coveralls --service=github
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }} COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
COVERALLS_SERVICE_NAME: github
- name: Coverage - Push
if: matrix.TYPE == 'server' && github.event_name == 'push'
run: |
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
cd ${GITHUB_WORKSPACE}
pip install coveralls==2.2.0
pip install coverage==4.5.4
coveralls --service=github-actions
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
COVERALLS_SERVICE_NAME: github-actions

View File

@@ -39,6 +39,10 @@ ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a
--- ---
### Containerized Installation
Use docker to deploy ERPNext in production or for development of [Frappe](https://github.com/frappe/frappe) apps. See https://github.com/frappe/frappe_docker for more details.
### Full Install ### Full Install
The Easy Way: our install script for bench will install all dependencies (e.g. MariaDB). See https://github.com/frappe/bench for more details. The Easy Way: our install script for bench will install all dependencies (e.g. MariaDB). See https://github.com/frappe/bench for more details.

View File

@@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides from erpnext.hooks import regional_overrides
from frappe.utils import getdate from frappe.utils import getdate
__version__ = '13.0.0-dev' __version__ = '13.2.0'
def get_default_company(user=None): def get_default_company(user=None):
'''Get default company for user''' '''Get default company for user'''

View File

@@ -78,8 +78,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
if ( if (
frm.doc.bank_account && frm.doc.bank_account &&
frm.doc.bank_statement_from_date && frm.doc.bank_statement_from_date &&
frm.doc.bank_statement_to_date && frm.doc.bank_statement_to_date
frm.doc.bank_statement_closing_balance
) { ) {
frm.trigger("render_chart"); frm.trigger("render_chart");
frm.trigger("render"); frm.trigger("render");

View File

@@ -39,13 +39,13 @@
"depends_on": "eval: doc.bank_account", "depends_on": "eval: doc.bank_account",
"fieldname": "bank_statement_from_date", "fieldname": "bank_statement_from_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Bank Statement From Date" "label": "From Date"
}, },
{ {
"depends_on": "eval: doc.bank_statement_from_date", "depends_on": "eval: doc.bank_statement_from_date",
"fieldname": "bank_statement_to_date", "fieldname": "bank_statement_to_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Bank Statement To Date" "label": "To Date"
}, },
{ {
"fieldname": "column_break_2", "fieldname": "column_break_2",
@@ -63,11 +63,10 @@
"depends_on": "eval: doc.bank_statement_to_date", "depends_on": "eval: doc.bank_statement_to_date",
"fieldname": "bank_statement_closing_balance", "fieldname": "bank_statement_closing_balance",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Bank Statement Closing Balance", "label": "Closing Balance",
"options": "Currency" "options": "Currency"
}, },
{ {
"depends_on": "eval: doc.bank_statement_closing_balance",
"fieldname": "section_break_1", "fieldname": "section_break_1",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Reconcile" "label": "Reconcile"
@@ -90,7 +89,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-02-02 01:35:53.043578", "modified": "2021-04-21 11:13:49.831769",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Bank Reconciliation Tool", "name": "Bank Reconciliation Tool",

View File

@@ -21,21 +21,17 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
refresh: function(frm) { refresh: function(frm) {
if(frm.doc.docstatus==1) { if(frm.doc.docstatus==1) {
frappe.db.get_value("Journal Entry Account", { frappe.call({
'reference_type': 'Exchange Rate Revaluation', method: 'check_journal_entry_condition',
'reference_name': frm.doc.name, doc: frm.doc,
'docstatus': 1 callback: function(r) {
}, "sum(debit) as sum", (r) =>{ if (r.message) {
let total_amt = 0;
frm.doc.accounts.forEach(d=> {
total_amt = total_amt + d['new_balance_in_base_currency'];
});
if(total_amt !== r.sum) {
frm.add_custom_button(__('Journal Entry'), function() { frm.add_custom_button(__('Journal Entry'), function() {
return frm.events.make_jv(frm); return frm.events.make_jv(frm);
}, __('Create')); }, __('Create'));
} }
}, 'Journal Entry'); }
});
} }
}, },

View File

@@ -27,6 +27,23 @@ class ExchangeRateRevaluation(Document):
if not (self.company and self.posting_date): if not (self.company and self.posting_date):
frappe.throw(_("Please select Company and Posting Date to getting entries")) frappe.throw(_("Please select Company and Posting Date to getting entries"))
@frappe.whitelist()
def check_journal_entry_condition(self):
total_debit = frappe.db.get_value("Journal Entry Account", {
'reference_type': 'Exchange Rate Revaluation',
'reference_name': self.name,
'docstatus': 1
}, "sum(debit) as sum")
total_amt = 0
for d in self.accounts:
total_amt = total_amt + d.new_balance_in_base_currency
if total_amt != total_debit:
return True
return False
@frappe.whitelist() @frappe.whitelist()
def get_accounts_data(self, account=None): def get_accounts_data(self, account=None):
accounts = [] accounts = []

View File

@@ -592,6 +592,7 @@ class JournalEntry(AccountsController):
self.validate_total_debit_and_credit() self.validate_total_debit_and_credit()
@frappe.whitelist()
def get_outstanding_invoices(self): def get_outstanding_invoices(self):
self.set('accounts', []) self.set('accounts', [])
total = 0 total = 0

View File

@@ -561,7 +561,7 @@ frappe.ui.form.on('Payment Entry', {
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate)); flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate));
if(frm.doc.payment_type == "Pay") if(frm.doc.payment_type == "Pay")
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount); frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, 1);
else else
frm.events.set_unallocated_amount(frm); frm.events.set_unallocated_amount(frm);
@@ -582,7 +582,7 @@ frappe.ui.form.on('Payment Entry', {
} }
if(frm.doc.payment_type == "Receive") if(frm.doc.payment_type == "Receive")
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount); frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, 1);
else else
frm.events.set_unallocated_amount(frm); frm.events.set_unallocated_amount(frm);
}, },
@@ -743,7 +743,7 @@ frappe.ui.form.on('Payment Entry', {
}); });
}, },
allocate_party_amount_against_ref_docs: function(frm, paid_amount) { allocate_party_amount_against_ref_docs: function(frm, paid_amount, paid_amount_change) {
var total_positive_outstanding_including_order = 0; var total_positive_outstanding_including_order = 0;
var total_negative_outstanding = 0; var total_negative_outstanding = 0;
var total_deductions = frappe.utils.sum($.map(frm.doc.deductions || [], var total_deductions = frappe.utils.sum($.map(frm.doc.deductions || [],
@@ -800,22 +800,15 @@ frappe.ui.form.on('Payment Entry', {
//If allocate payment amount checkbox is unchecked, set zero to allocate amount //If allocate payment amount checkbox is unchecked, set zero to allocate amount
row.allocated_amount = 0; row.allocated_amount = 0;
} else if (frappe.flags.allocate_payment_amount != 0 && !row.allocated_amount) { } else if (frappe.flags.allocate_payment_amount != 0 && (!row.allocated_amount || paid_amount_change)) {
if (row.outstanding_amount > 0 && allocated_positive_outstanding > 0) { if (row.outstanding_amount > 0 && allocated_positive_outstanding >= 0) {
if (row.outstanding_amount >= allocated_positive_outstanding) { row.allocated_amount = (row.outstanding_amount >= allocated_positive_outstanding) ?
row.allocated_amount = allocated_positive_outstanding; allocated_positive_outstanding : row.outstanding_amount;
} else {
row.allocated_amount = row.outstanding_amount;
}
allocated_positive_outstanding -= flt(row.allocated_amount); allocated_positive_outstanding -= flt(row.allocated_amount);
} else if (row.outstanding_amount < 0 && allocated_negative_outstanding) {
if (Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) {
row.allocated_amount = -1*allocated_negative_outstanding;
} else {
row.allocated_amount = row.outstanding_amount;
};
} else if (row.outstanding_amount < 0 && allocated_negative_outstanding) {
row.allocated_amount = (Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) ?
-1*allocated_negative_outstanding : row.outstanding_amount;
allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount)); allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount));
} }
} }

View File

@@ -234,7 +234,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
}); });
if (invoices) { if (invoices) {
this.frm.fields_dict.payment.grid.update_docfield_property( this.frm.fields_dict.payments.grid.update_docfield_property(
'invoice_number', 'options', "\n" + invoices.join("\n") 'invoice_number', 'options', "\n" + invoices.join("\n")
); );

View File

@@ -20,10 +20,11 @@
"discount", "discount",
"section_break_9", "section_break_9",
"payment_amount", "payment_amount",
"outstanding",
"paid_amount",
"discounted_amount", "discounted_amount",
"column_break_3", "column_break_3",
"outstanding", "base_payment_amount"
"paid_amount"
], ],
"fields": [ "fields": [
{ {
@@ -78,7 +79,8 @@
"depends_on": "paid_amount", "depends_on": "paid_amount",
"fieldname": "paid_amount", "fieldname": "paid_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Paid Amount" "label": "Paid Amount",
"options": "currency"
}, },
{ {
"fieldname": "column_break_3", "fieldname": "column_break_3",
@@ -97,6 +99,7 @@
"fieldname": "outstanding", "fieldname": "outstanding",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Outstanding", "label": "Outstanding",
"options": "currency",
"read_only": 1 "read_only": 1
}, },
{ {
@@ -145,12 +148,18 @@
{ {
"fieldname": "section_break_4", "fieldname": "section_break_4",
"fieldtype": "Section Break" "fieldtype": "Section Break"
},
{
"fieldname": "base_payment_amount",
"fieldtype": "Currency",
"label": "Payment Amount (Company Currency)",
"options": "Company:company:default_currency"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-02-15 21:03:12.540546", "modified": "2021-04-28 05:41:35.084233",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Schedule", "name": "Payment Schedule",

View File

@@ -16,28 +16,8 @@ class POSClosingEntry(StatusUpdater):
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open": if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry")) frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
self.validate_pos_closing()
self.validate_pos_invoices() self.validate_pos_invoices()
def validate_pos_closing(self):
user = frappe.db.sql("""
SELECT name FROM `tabPOS Closing Entry`
WHERE
user = %(user)s AND docstatus = 1 AND pos_profile = %(profile)s AND
(period_start_date between %(start)s and %(end)s OR period_end_date between %(start)s and %(end)s)
""", {
'user': self.user,
'profile': self.pos_profile,
'start': self.period_start_date,
'end': self.period_end_date
})
if user:
bold_already_exists = frappe.bold(_("already exists"))
bold_user = frappe.bold(self.user)
frappe.throw(_("POS Closing Entry {} against {} between selected period")
.format(bold_already_exists, bold_user), title=_("Invalid Period"))
def validate_pos_invoices(self): def validate_pos_invoices(self):
invalid_rows = [] invalid_rows = []
for d in self.pos_transactions: for d in self.pos_transactions:

View File

@@ -96,30 +96,45 @@ class POSInvoice(SalesInvoice):
if paid_amt and pay.amount != paid_amt: if paid_amt and pay.amount != paid_amt:
return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment)) return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment))
def validate_stock_availablility(self): def validate_pos_reserved_serial_nos(self, item):
if self.is_return: serial_nos = get_serial_nos(item.serial_no)
return filters = {"item_code": item.item_code, "warehouse": item.warehouse}
if item.batch_no:
filters["batch_no"] = item.batch_no
allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')
error_msg = []
for d in self.get('items'):
msg = ""
if d.serial_no:
filters = { "item_code": d.item_code, "warehouse": d.warehouse }
if d.batch_no:
filters["batch_no"] = d.batch_no
reserved_serial_nos = get_pos_reserved_serial_nos(filters) reserved_serial_nos = get_pos_reserved_serial_nos(filters)
serial_nos = get_serial_nos(d.serial_no)
invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos] invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos]
bold_invalid_serial_nos = frappe.bold(', '.join(invalid_serial_nos)) bold_invalid_serial_nos = frappe.bold(', '.join(invalid_serial_nos))
if len(invalid_serial_nos) == 1: if len(invalid_serial_nos) == 1:
msg = (_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.") frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
.format(d.idx, bold_invalid_serial_nos)) .format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
elif invalid_serial_nos: elif invalid_serial_nos:
msg = (_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.") frappe.throw(_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.")
.format(d.idx, bold_invalid_serial_nos)) .format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
def validate_delivered_serial_nos(self, item):
serial_nos = get_serial_nos(item.serial_no)
delivered_serial_nos = frappe.db.get_list('Serial No', {
'item_code': item.item_code,
'name': ['in', serial_nos],
'sales_invoice': ['is', 'set']
}, pluck='name')
if delivered_serial_nos:
bold_delivered_serial_nos = frappe.bold(', '.join(delivered_serial_nos))
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no.")
.format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable"))
def validate_stock_availablility(self):
if self.is_return:
return
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
for d in self.get('items'):
if d.serial_no:
self.validate_pos_reserved_serial_nos(d)
self.validate_delivered_serial_nos(d)
else: else:
if allow_negative_stock: if allow_negative_stock:
return return
@@ -127,15 +142,11 @@ class POSInvoice(SalesInvoice):
available_stock = get_stock_availability(d.item_code, d.warehouse) available_stock = get_stock_availability(d.item_code, d.warehouse)
item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty) item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
if flt(available_stock) <= 0: if flt(available_stock) <= 0:
msg = (_('Row #{}: Item Code: {} is not available under warehouse {}.').format(d.idx, item_code, warehouse)) frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.')
.format(d.idx, item_code, warehouse), title=_("Item Unavailable"))
elif flt(available_stock) < flt(d.qty): elif flt(available_stock) < flt(d.qty):
msg = (_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.') frappe.throw(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.')
.format(d.idx, item_code, warehouse, qty)) .format(d.idx, item_code, warehouse, available_stock), title=_("Item Unavailable"))
if msg:
error_msg.append(msg)
if error_msg:
frappe.throw(error_msg, title=_("Item Unavailable"), as_list=True)
def validate_serialised_or_batched_item(self): def validate_serialised_or_batched_item(self):
error_msg = [] error_msg = []
@@ -202,9 +213,8 @@ class POSInvoice(SalesInvoice):
for d in self.get("items"): for d in self.get("items"):
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item") is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
if not is_stock_item: if not is_stock_item:
frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice. ").format( frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice. ")
d.idx, frappe.bold(d.item_code) .format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
), title=_("Invalid Item"))
def validate_mode_of_payment(self): def validate_mode_of_payment(self):
if len(self.payments) == 0: if len(self.payments) == 0:

View File

@@ -10,10 +10,12 @@ from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry 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.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
class TestPOSInvoice(unittest.TestCase): class TestPOSInvoice(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=800, basic_rate=100)
frappe.db.sql("delete from `tabTax Rule`") frappe.db.sql("delete from `tabTax Rule`")
def tearDown(self): def tearDown(self):
@@ -320,6 +322,34 @@ class TestPOSInvoice(unittest.TestCase):
self.assertRaises(frappe.ValidationError, pos2.insert) self.assertRaises(frappe.ValidationError, pos2.insert)
def test_delivered_serialized_item_transaction(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
se = make_serialized_item(company='_Test Company',
target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC')
serial_nos = get_serial_nos(se.get("items")[0].serial_no)
si = create_sales_invoice(company='_Test Company', debit_to='Debtors - _TC',
account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
si.get("items")[0].serial_no = serial_nos[0]
si.insert()
si.submit()
pos2 = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
pos2.get("items")[0].serial_no = serial_nos[0]
pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
self.assertRaises(frappe.ValidationError, pos2.insert)
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
from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points

View File

@@ -235,11 +235,11 @@ def get_invoice_customer_map(pos_invoices):
return pos_invoice_customer_map return pos_invoice_customer_map
def consolidate_pos_invoices(pos_invoices=[], closing_entry={}): def consolidate_pos_invoices(pos_invoices=None, closing_entry=None):
invoices = pos_invoices or closing_entry.get('pos_transactions') or get_all_unconsolidated_invoices() invoices = pos_invoices or (closing_entry and closing_entry.get('pos_transactions')) or get_all_unconsolidated_invoices()
invoice_by_customer = get_invoice_customer_map(invoices) invoice_by_customer = get_invoice_customer_map(invoices)
if len(invoices) >= 5 and closing_entry: if len(invoices) >= 1 and closing_entry:
closing_entry.set_status(update=True, status='Queued') closing_entry.set_status(update=True, status='Queued')
enqueue_job(create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry) enqueue_job(create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry)
else: else:
@@ -252,18 +252,18 @@ def unconsolidate_pos_invoices(closing_entry):
pluck='name' pluck='name'
) )
if len(merge_logs) >= 5: if len(merge_logs) >= 1:
closing_entry.set_status(update=True, status='Queued') closing_entry.set_status(update=True, status='Queued')
enqueue_job(cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry) enqueue_job(cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry)
else: else:
cancel_merge_logs(merge_logs, closing_entry) cancel_merge_logs(merge_logs, closing_entry)
def create_merge_logs(invoice_by_customer, closing_entry={}): def create_merge_logs(invoice_by_customer, closing_entry=None):
for customer, invoices in iteritems(invoice_by_customer): for customer, invoices in iteritems(invoice_by_customer):
merge_log = frappe.new_doc('POS Invoice Merge Log') merge_log = frappe.new_doc('POS Invoice Merge Log')
merge_log.posting_date = getdate(closing_entry.get('posting_date')) merge_log.posting_date = getdate(closing_entry.get('posting_date')) if closing_entry else nowdate()
merge_log.customer = customer merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get('name', None) merge_log.pos_closing_entry = closing_entry.get('name') if closing_entry else None
merge_log.set('pos_invoices', invoices) merge_log.set('pos_invoices', invoices)
merge_log.save(ignore_permissions=True) merge_log.save(ignore_permissions=True)
@@ -273,7 +273,7 @@ def create_merge_logs(invoice_by_customer, closing_entry={}):
closing_entry.set_status(update=True, status='Submitted') closing_entry.set_status(update=True, status='Submitted')
closing_entry.update_opening_entry() closing_entry.update_opening_entry()
def cancel_merge_logs(merge_logs, closing_entry={}): def cancel_merge_logs(merge_logs, closing_entry=None):
for log in merge_logs: for log in merge_logs:
merge_log = frappe.get_doc('POS Invoice Merge Log', log) merge_log = frappe.get_doc('POS Invoice Merge Log', log)
merge_log.flags.ignore_permissions = True merge_log.flags.ignore_permissions = True
@@ -283,20 +283,20 @@ def cancel_merge_logs(merge_logs, closing_entry={}):
closing_entry.set_status(update=True, status='Cancelled') closing_entry.set_status(update=True, status='Cancelled')
closing_entry.update_opening_entry(for_cancel=True) closing_entry.update_opening_entry(for_cancel=True)
def enqueue_job(job, merge_logs=None, invoice_by_customer=None, closing_entry=None): def enqueue_job(job, **kwargs):
check_scheduler_status() check_scheduler_status()
closing_entry = kwargs.get('closing_entry') or {}
job_name = closing_entry.get("name") job_name = closing_entry.get("name")
if not job_already_enqueued(job_name): if not job_already_enqueued(job_name):
enqueue( enqueue(
job, job,
**kwargs,
queue="long", queue="long",
timeout=10000, timeout=10000,
event="processing_merge_logs", event="processing_merge_logs",
job_name=job_name, job_name=job_name,
closing_entry=closing_entry,
invoice_by_customer=invoice_by_customer,
merge_logs=merge_logs,
now=frappe.conf.developer_mode or frappe.flags.in_test now=frappe.conf.developer_mode or frappe.flags.in_test
) )

View File

@@ -0,0 +1,37 @@
{
"actions": [],
"creation": "2021-04-19 14:56:06.652327",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"field",
"fieldname"
],
"fields": [
{
"fieldname": "fieldname",
"fieldtype": "Data",
"hidden": 1,
"label": "Fieldname"
},
{
"fieldname": "field",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Field"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-04-21 11:12:54.632093",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Search Fields",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class POSSearchFields(Document):
pass

View File

@@ -1,9 +1,17 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
let search_fields_datatypes = ['Data', 'Link', 'Dynamic Link', 'Long Text', 'Select', 'Small Text', 'Text', 'Text Editor'];
let do_not_include_fields = ["naming_series", "item_code", "item_name", "stock_uom", "hub_sync_id", "asset_naming_series",
"default_material_request_type", "valuation_method", "warranty_period", "weight_uom", "batch_number_series",
"serial_no_series", "purchase_uom", "customs_tariff_number", "sales_uom", "deferred_revenue_account",
"deferred_expense_account", "quality_inspection_template", "route", "slideshow", "website_image_alt", "thumbnail",
"web_long_description", "hub_sync_id"]
frappe.ui.form.on('POS Settings', { frappe.ui.form.on('POS Settings', {
onload: function(frm) { onload: function(frm) {
frm.trigger("get_invoice_fields"); frm.trigger("get_invoice_fields");
frm.trigger("add_search_options");
}, },
get_invoice_fields: function(frm) { get_invoice_fields: function(frm) {
@@ -21,6 +29,38 @@ frappe.ui.form.on('POS Settings', {
); );
}); });
},
add_search_options: function(frm) {
frappe.model.with_doctype("Item", () => {
var fields = $.map(frappe.get_doc("DocType", "Item").fields, function(d) {
if (search_fields_datatypes.includes(d.fieldtype) && !(do_not_include_fields.includes(d.fieldname))) {
return [d.label];
} else {
return null;
}
});
fields.unshift('');
frm.fields_dict.pos_search_fields.grid.update_docfield_property('field', 'options', fields);
});
}
});
frappe.ui.form.on("POS Search Fields", {
field: function(frm, doctype, name) {
var doc = frappe.get_doc(doctype, name);
var df = $.map(frappe.get_doc("DocType", "Item").fields, function(d) {
if (doc.field == d.label && search_fields_datatypes.includes(d.fieldtype)) {
return d;
} else {
return null;
}
})[0];
doc.fieldname = df.fieldname;
frm.refresh_field("fields");
} }
}); });

View File

@@ -5,7 +5,8 @@
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"invoice_fields" "invoice_fields",
"pos_search_fields"
], ],
"fields": [ "fields": [
{ {
@@ -13,11 +14,17 @@
"fieldtype": "Table", "fieldtype": "Table",
"label": "POS Field", "label": "POS Field",
"options": "POS Field" "options": "POS Field"
},
{
"fieldname": "pos_search_fields",
"fieldtype": "Table",
"label": "POS Search Fields",
"options": "POS Search Fields"
} }
], ],
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2020-06-01 15:46:41.478928", "modified": "2021-04-19 14:56:24.465218",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Settings", "name": "POS Settings",

View File

@@ -60,8 +60,8 @@
</tbody> </tbody>
</table> </table>
<br><br> <br><br>
{% if aging %} {% if ageing %}
<h3 class="text-center">{{ _("Ageing Report Based On ") }} {{ aging.ageing_based_on }}</h3> <h3 class="text-center">{{ _("Ageing Report Based On ") }} {{ ageing.ageing_based_on }}</h3>
<h5 class="text-center"> <h5 class="text-center">
{{ _("Up to " ) }} {{ frappe.format(filters.to_date, 'Date')}} {{ _("Up to " ) }} {{ frappe.format(filters.to_date, 'Date')}}
</h5> </h5>
@@ -78,10 +78,10 @@
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>{{ aging.range1 }}</td> <td>{{ frappe.utils.fmt_money(ageing.range1, currency=filters.presentation_currency) }}</td>
<td>{{ aging.range2 }}</td> <td>{{ frappe.utils.fmt_money(ageing.range2, currency=filters.presentation_currency) }}</td>
<td>{{ aging.range3 }}</td> <td>{{ frappe.utils.fmt_money(ageing.range3, currency=filters.presentation_currency) }}</td>
<td>{{ aging.range4 }}</td> <td>{{ frappe.utils.fmt_money(ageing.range4, currency=filters.presentation_currency) }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -4,10 +4,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.accounts.report.general_ledger.general_ledger import execute as get_soa from erpnext.accounts.report.general_ledger.general_ledger import execute as get_soa
from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import execute as get_ageing from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import execute as get_ageing
from frappe.core.doctype.communication.email import make from erpnext import get_company_currency
from erpnext.accounts.party import get_party_account_currency
from frappe.utils.print_format import report_to_pdf from frappe.utils.print_format import report_to_pdf
from frappe.utils.pdf import get_pdf from frappe.utils.pdf import get_pdf
@@ -29,7 +31,7 @@ class ProcessStatementOfAccounts(Document):
validate_template(self.body) validate_template(self.body)
if not self.customers: if not self.customers:
frappe.throw(frappe._('Customers not selected.')) frappe.throw(_('Customers not selected.'))
if self.enable_auto_email: if self.enable_auto_email:
self.to_date = self.start_date self.to_date = self.start_date
@@ -38,7 +40,7 @@ class ProcessStatementOfAccounts(Document):
def get_report_pdf(doc, consolidated=True): def get_report_pdf(doc, consolidated=True):
statement_dict = {} statement_dict = {}
aging = '' ageing = ''
base_template_path = "frappe/www/printview.html" base_template_path = "frappe/www/printview.html"
template_path = "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html" template_path = "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
@@ -54,26 +56,30 @@ def get_report_pdf(doc, consolidated=True):
'range4': 120, 'range4': 120,
'customer': entry.customer 'customer': entry.customer
}) })
col1, aging = get_ageing(ageing_filters) col1, ageing = get_ageing(ageing_filters)
aging[0]['ageing_based_on'] = doc.ageing_based_on
if ageing:
ageing[0]['ageing_based_on'] = doc.ageing_based_on
tax_id = frappe.get_doc('Customer', entry.customer).tax_id tax_id = frappe.get_doc('Customer', entry.customer).tax_id
presentation_currency = get_party_account_currency('Customer', entry.customer, doc.company) \
or doc.currency or get_company_currency(doc.company)
filters= frappe._dict({ filters= frappe._dict({
'from_date': doc.from_date, 'from_date': doc.from_date,
'to_date': doc.to_date, 'to_date': doc.to_date,
'company': doc.company, 'company': doc.company,
'finance_book': doc.finance_book if doc.finance_book else None, 'finance_book': doc.finance_book if doc.finance_book else None,
"account": doc.account if doc.account else None, 'account': doc.account if doc.account else None,
'party_type': 'Customer', 'party_type': 'Customer',
'party': [entry.customer], 'party': [entry.customer],
'presentation_currency': presentation_currency,
'group_by': doc.group_by, 'group_by': doc.group_by,
'currency': doc.currency, 'currency': doc.currency,
'cost_center': [cc.cost_center_name for cc in doc.cost_center], 'cost_center': [cc.cost_center_name for cc in doc.cost_center],
'project': [p.project_name for p in doc.project], 'project': [p.project_name for p in doc.project],
'show_opening_entries': 0, 'show_opening_entries': 0,
'include_default_book_entries': 0, 'include_default_book_entries': 0,
'show_cancelled_entries': 1,
'tax_id': tax_id if tax_id else None 'tax_id': tax_id if tax_id else None
}) })
col, res = get_soa(filters) col, res = get_soa(filters)
@@ -83,11 +89,14 @@ def get_report_pdf(doc, consolidated=True):
if len(res) == 3: if len(res) == 3:
continue continue
html = frappe.render_template(template_path, \ html = frappe.render_template(template_path, \
{"filters": filters, "data": res, "aging": aging[0] if doc.include_ageing else None}) {"filters": filters, "data": res, "ageing": ageing[0] if (doc.include_ageing and ageing) else None})
html = frappe.render_template(base_template_path, {"body": html, \ html = frappe.render_template(base_template_path, {"body": html, \
"css": get_print_style(), "title": "Statement For " + entry.customer}) "css": get_print_style(), "title": "Statement For " + entry.customer})
statement_dict[entry.customer] = html statement_dict[entry.customer] = html
if not bool(statement_dict): if not bool(statement_dict):
return False return False
elif consolidated: elif consolidated:
@@ -167,7 +176,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
if customer_collection == 'Sales Person': if customer_collection == 'Sales Person':
customers = get_customers_based_on_sales_person(collection_name) customers = get_customers_based_on_sales_person(collection_name)
if not bool(customers): if not bool(customers):
frappe.throw('No Customers found with selected options.') frappe.throw(_('No Customers found with selected options.'))
else: else:
if customer_collection == 'Sales Partner': if customer_collection == 'Sales Partner':
customers = frappe.get_list('Customer', fields=['name', 'email_id'], \ customers = frappe.get_list('Customer', fields=['name', 'email_id'], \
@@ -199,14 +208,14 @@ def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=Tr
if len(billing_email) == 0 or (billing_email[0][0] is None): if len(billing_email) == 0 or (billing_email[0][0] is None):
if billing_and_primary: if billing_and_primary:
frappe.throw('No billing email found for customer: '+ customer_name) frappe.throw(_("No billing email found for customer: {0}").format(customer_name))
else: else:
return '' return ''
if billing_and_primary: if billing_and_primary:
primary_email = frappe.get_value('Customer', customer_name, 'email_id') primary_email = frappe.get_value('Customer', customer_name, 'email_id')
if primary_email is None and int(primary_mandatory): if primary_email is None and int(primary_mandatory):
frappe.throw('No primary email found for customer: '+ customer_name) frappe.throw(_("No primary email found for customer: {0}").format(customer_name))
return [primary_email or '', billing_email[0][0]] return [primary_email or '', billing_email[0][0]]
else: else:
return billing_email[0][0] or '' return billing_email[0][0] or ''

View File

@@ -514,6 +514,28 @@ frappe.ui.form.on("Purchase Invoice", {
} }
}, },
refresh: function(frm) {
frm.events.add_custom_buttons(frm);
},
add_custom_buttons: function(frm) {
if (frm.doc.per_received < 100) {
frm.add_custom_button(__('Purchase Receipt'), () => {
frm.events.make_purchase_receipt(frm);
}, __('Create'));
}
if (frm.doc.docstatus == 1 && frm.doc.per_received > 0) {
frm.add_custom_button(__('Purchase Receipt'), () => {
frappe.route_options = {
'purchase_invoice': frm.doc.name
}
frappe.set_route("List", "Purchase Receipt", "List")
}, __('View'));
}
},
onload: function(frm) { onload: function(frm) {
if(frm.doc.__onload && frm.is_new()) { if(frm.doc.__onload && frm.is_new()) {
if(frm.doc.supplier) { if(frm.doc.supplier) {
@@ -539,5 +561,13 @@ frappe.ui.form.on("Purchase Invoice", {
update_stock: function(frm) { update_stock: function(frm) {
hide_fields(frm.doc); hide_fields(frm.doc);
frm.fields_dict.items.grid.toggle_reqd("item_code", frm.doc.update_stock? true: false); frm.fields_dict.items.grid.toggle_reqd("item_code", frm.doc.update_stock? true: false);
},
make_purchase_receipt: function(frm) {
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_purchase_receipt",
frm: frm,
freeze_message: __("Creating Purchase Receipt ...")
})
} }
}) })

View File

@@ -163,7 +163,8 @@
"to_date", "to_date",
"column_break_114", "column_break_114",
"auto_repeat", "auto_repeat",
"update_auto_repeat_reference" "update_auto_repeat_reference",
"per_received"
], ],
"fields": [ "fields": [
{ {
@@ -1364,6 +1365,15 @@
"print_hide": 1, "print_hide": 1,
"print_width": "50px", "print_width": "50px",
"width": "50px" "width": "50px"
},
{
"fieldname": "per_received",
"fieldtype": "Percent",
"hidden": 1,
"label": "Per Received",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",

View File

@@ -1207,3 +1207,41 @@ def make_inter_company_sales_invoice(source_name, target_doc=None):
def on_doctype_update(): def on_doctype_update():
frappe.db.add_index("Purchase Invoice", ["supplier", "is_return", "return_against"]) frappe.db.add_index("Purchase Invoice", ["supplier", "is_return", "return_against"])
@frappe.whitelist()
def make_purchase_receipt(source_name, target_doc=None):
def update_item(obj, target, source_parent):
target.qty = flt(obj.qty) - flt(obj.received_qty)
target.received_qty = flt(obj.qty) - flt(obj.received_qty)
target.stock_qty = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.conversion_factor)
target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate)
target.base_amount = (flt(obj.qty) - flt(obj.received_qty)) * \
flt(obj.rate) * flt(source_parent.conversion_rate)
doc = get_mapped_doc("Purchase Invoice", source_name, {
"Purchase Invoice": {
"doctype": "Purchase Receipt",
"validation": {
"docstatus": ["=", 1],
}
},
"Purchase Invoice Item": {
"doctype": "Purchase Receipt Item",
"field_map": {
"name": "purchase_invoice_item",
"parent": "purchase_invoice",
"bom": "bom",
"purchase_order": "purchase_order",
"po_detail": "purchase_order_item",
"material_request": "material_request",
"material_request_item": "material_request_item"
},
"postprocess": update_item,
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty)
},
"Purchase Taxes and Charges": {
"doctype": "Purchase Taxes and Charges"
}
}, target_doc)
return doc

View File

@@ -397,7 +397,7 @@ class TestPurchaseInvoice(unittest.TestCase):
pi.update({ pi.update({
"payment_schedule": get_payment_terms("_Test Payment Term Template", "payment_schedule": get_payment_terms("_Test Payment Term Template",
pi.posting_date, pi.grand_total) pi.posting_date, pi.grand_total, pi.base_grand_total)
}) })
pi.save() pi.save()

View File

@@ -607,6 +607,7 @@
"oldfieldname": "purchase_order", "oldfieldname": "purchase_order",
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "Purchase Order", "options": "Purchase Order",
"print_hide": 1,
"read_only": 1, "read_only": 1,
"search_index": 1 "search_index": 1
}, },
@@ -853,7 +854,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-02-23 00:59:52.614805", "modified": "2021-03-30 09:02:39.256602",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@@ -36,6 +36,7 @@
"additional_discount_percentage", "additional_discount_percentage",
"additional_discount_amount", "additional_discount_amount",
"sb_3", "sb_3",
"submit_invoice",
"invoices", "invoices",
"accounting_dimensions_section", "accounting_dimensions_section",
"cost_center", "cost_center",
@@ -45,9 +46,7 @@
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "cb_1", "fieldname": "cb_1",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "status", "fieldname": "status",
@@ -55,97 +54,73 @@
"label": "Status", "label": "Status",
"no_copy": 1, "no_copy": 1,
"options": "\nTrialling\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted", "options": "\nTrialling\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted",
"read_only": 1, "read_only": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "subscription_period", "fieldname": "subscription_period",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Subscription Period", "label": "Subscription Period"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "cancelation_date", "fieldname": "cancelation_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Cancelation Date", "label": "Cancelation Date",
"read_only": 1, "read_only": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "trial_period_start", "fieldname": "trial_period_start",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Trial Period Start Date", "label": "Trial Period Start Date",
"set_only_once": 1, "set_only_once": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"depends_on": "eval:doc.trial_period_start", "depends_on": "eval:doc.trial_period_start",
"fieldname": "trial_period_end", "fieldname": "trial_period_end",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Trial Period End Date", "label": "Trial Period End Date",
"set_only_once": 1, "set_only_once": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_11", "fieldname": "column_break_11",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "current_invoice_start", "fieldname": "current_invoice_start",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Current Invoice Start Date", "label": "Current Invoice Start Date",
"read_only": 1, "read_only": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "current_invoice_end", "fieldname": "current_invoice_end",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Current Invoice End Date", "label": "Current Invoice End Date",
"read_only": 1, "read_only": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"description": "Number of days that the subscriber has to pay invoices generated by this subscription", "description": "Number of days that the subscriber has to pay invoices generated by this subscription",
"fieldname": "days_until_due", "fieldname": "days_until_due",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Days Until Due", "label": "Days Until Due"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "cancel_at_period_end", "fieldname": "cancel_at_period_end",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Cancel At End Of Period", "label": "Cancel At End Of Period"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "generate_invoice_at_period_start", "fieldname": "generate_invoice_at_period_start",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Generate Invoice At Beginning Of Period", "label": "Generate Invoice At Beginning Of Period"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "sb_4", "fieldname": "sb_4",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Plans", "label": "Plans"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
@@ -153,84 +128,62 @@
"fieldtype": "Table", "fieldtype": "Table",
"label": "Plans", "label": "Plans",
"options": "Subscription Plan Detail", "options": "Subscription Plan Detail",
"reqd": 1, "reqd": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"depends_on": "eval:['Customer', 'Supplier'].includes(doc.party_type)", "depends_on": "eval:['Customer', 'Supplier'].includes(doc.party_type)",
"fieldname": "sb_1", "fieldname": "sb_1",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Taxes", "label": "Taxes"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "sb_2", "fieldname": "sb_2",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Discounts", "label": "Discounts"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "apply_additional_discount", "fieldname": "apply_additional_discount",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Apply Additional Discount On", "label": "Apply Additional Discount On",
"options": "\nGrand Total\nNet Total", "options": "\nGrand Total\nNet Total"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "cb_2", "fieldname": "cb_2",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "additional_discount_percentage", "fieldname": "additional_discount_percentage",
"fieldtype": "Percent", "fieldtype": "Percent",
"label": "Additional DIscount Percentage", "label": "Additional DIscount Percentage"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "additional_discount_amount", "fieldname": "additional_discount_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Additional DIscount Amount", "label": "Additional DIscount Amount"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"depends_on": "eval:doc.invoices", "depends_on": "eval:doc.invoices",
"fieldname": "sb_3", "fieldname": "sb_3",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Invoices", "label": "Invoices"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "invoices", "fieldname": "invoices",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Invoices", "label": "Invoices",
"options": "Subscription Invoice", "options": "Subscription Invoice"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "accounting_dimensions_section", "fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Accounting Dimensions", "label": "Accounting Dimensions"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "dimension_col_break", "fieldname": "dimension_col_break",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "party_type", "fieldname": "party_type",
@@ -238,9 +191,7 @@
"label": "Party Type", "label": "Party Type",
"options": "DocType", "options": "DocType",
"reqd": 1, "reqd": 1,
"set_only_once": 1, "set_only_once": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "party", "fieldname": "party",
@@ -249,27 +200,21 @@
"label": "Party", "label": "Party",
"options": "party_type", "options": "party_type",
"reqd": 1, "reqd": 1,
"set_only_once": 1, "set_only_once": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"depends_on": "eval:doc.party_type === 'Customer'", "depends_on": "eval:doc.party_type === 'Customer'",
"fieldname": "sales_tax_template", "fieldname": "sales_tax_template",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Sales Taxes and Charges Template", "label": "Sales Taxes and Charges Template",
"options": "Sales Taxes and Charges Template", "options": "Sales Taxes and Charges Template"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"depends_on": "eval:doc.party_type === 'Supplier'", "depends_on": "eval:doc.party_type === 'Supplier'",
"fieldname": "purchase_tax_template", "fieldname": "purchase_tax_template",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Purchase Taxes and Charges Template", "label": "Purchase Taxes and Charges Template",
"options": "Purchase Taxes and Charges Template", "options": "Purchase Taxes and Charges Template"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
@@ -277,55 +222,49 @@
"fieldname": "follow_calendar_months", "fieldname": "follow_calendar_months",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Follow Calendar Months", "label": "Follow Calendar Months",
"set_only_once": 1, "set_only_once": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"description": "New invoices will be generated as per schedule even if current invoices are unpaid or past due date", "description": "New invoices will be generated as per schedule even if current invoices are unpaid or past due date",
"fieldname": "generate_new_invoices_past_due_date", "fieldname": "generate_new_invoices_past_due_date",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Generate New Invoices Past Due Date", "label": "Generate New Invoices Past Due Date"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "end_date", "fieldname": "end_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Subscription End Date", "label": "Subscription End Date",
"set_only_once": 1, "set_only_once": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "start_date", "fieldname": "start_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Subscription Start Date", "label": "Subscription Start Date",
"set_only_once": 1, "set_only_once": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "cost_center", "fieldname": "cost_center",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Cost Center", "label": "Cost Center",
"options": "Cost Center", "options": "Cost Center"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Company", "label": "Company",
"options": "Company", "options": "Company"
"show_days": 1, },
"show_seconds": 1 {
"default": "1",
"fieldname": "submit_invoice",
"fieldtype": "Check",
"label": "Submit Invoice Automatically"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-02-09 15:44:20.024789", "modified": "2021-04-19 15:24:27.550797",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Subscription", "name": "Subscription",

View File

@@ -276,7 +276,7 @@ class Subscription(Document):
frappe.throw(_('Subscription End Date is mandatory to follow calendar months')) frappe.throw(_('Subscription End Date is mandatory to follow calendar months'))
if billing_info[0]['billing_interval'] != 'Month': if billing_info[0]['billing_interval'] != 'Month':
frappe.throw('Billing Interval in Subscription Plan must be Month to follow calendar months') frappe.throw(_('Billing Interval in Subscription Plan must be Month to follow calendar months'))
def after_insert(self): def after_insert(self):
# todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype? # todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype?
@@ -383,6 +383,8 @@ class Subscription(Document):
invoice.flags.ignore_mandatory = True invoice.flags.ignore_mandatory = True
invoice.save() invoice.save()
if self.submit_invoice:
invoice.submit() invoice.submit()
return invoice return invoice

View File

@@ -18,7 +18,8 @@ def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, upd
gl_map = process_gl_map(gl_map, merge_entries) gl_map = process_gl_map(gl_map, merge_entries)
if gl_map and len(gl_map) > 1: if gl_map and len(gl_map) > 1:
save_entries(gl_map, adv_adj, update_outstanding, from_repost) save_entries(gl_map, adv_adj, update_outstanding, from_repost)
else: # Post GL Map proccess there may no be any GL Entries
elif gl_map:
frappe.throw(_("Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction.")) frappe.throw(_("Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction."))
else: else:
make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding) make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding)
@@ -170,7 +171,7 @@ def round_off_debit_credit(gl_map):
else: else:
allowance = .5 allowance = .5
if abs(debit_credit_diff) >= allowance: if abs(debit_credit_diff) > allowance:
frappe.throw(_("Debit and Credit not equal for {0} #{1}. Difference is {2}.") frappe.throw(_("Debit and Credit not equal for {0} #{1}. Difference is {2}.")
.format(gl_map[0].voucher_type, gl_map[0].voucher_no, debit_credit_diff)) .format(gl_map[0].voucher_type, gl_map[0].voucher_no, debit_credit_diff))

View File

@@ -0,0 +1,29 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports['Billed Items To Be Received'] = {
'filters': [
{
'label': __('Company'),
'fieldname': 'company',
'fieldtype': 'Link',
'options': 'Company',
'reqd': 1,
'default': frappe.defaults.get_default('Company')
},
{
'label': __('As on Date'),
'fieldname': 'posting_date',
'fieldtype': 'Date',
'reqd': 1,
'default': get_today()
},
{
'label': __('Purchase Invoice'),
'fieldname': 'purchase_invoice',
'fieldtype': 'Link',
'options': 'Purchase Invoice'
}
]
};

View File

@@ -0,0 +1,39 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-03-30 09:35:38.683028",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-03-31 08:48:30.944429",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Billed Items To Be Received",
"owner": "Administrator",
"prepared_report": 0,
"query": "",
"ref_doctype": "Purchase Invoice",
"report_name": "Billed Items To Be Received",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
{
"role": "Purchase User"
},
{
"role": "Accounts Manager"
},
{
"role": "Auditor"
},
{
"role": "Stock User"
}
]
}

View File

@@ -0,0 +1,107 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
def execute(filters=None):
data = get_data(filters) or []
columns = get_columns()
return columns, data
def get_data(report_filters):
filters = get_report_filters(report_filters)
fields = get_report_fields()
return frappe.get_all('Purchase Invoice',
fields= fields, filters=filters)
def get_report_filters(report_filters):
filters = [['Purchase Invoice','company','=',report_filters.get('company')],
['Purchase Invoice','posting_date','<=',report_filters.get('posting_date')], ['Purchase Invoice','docstatus','=',1],
['Purchase Invoice','per_received','<',100], ['Purchase Invoice','update_stock','=',0]]
if report_filters.get('purchase_invoice'):
filters.append(['Purchase Invoice','per_received','in',[report_filters.get('purchase_invoice')]])
return filters
def get_report_fields():
fields = []
for p_field in ['name', 'supplier', 'company', 'posting_date', 'currency']:
fields.append('`tabPurchase Invoice`.`{}`'.format(p_field))
for c_field in ['item_code', 'item_name', 'uom', 'qty', 'received_qty', 'rate', 'amount']:
fields.append('`tabPurchase Invoice Item`.`{}`'.format(c_field))
return fields
def get_columns():
return [
{
'label': _('Purchase Invoice'),
'fieldname': 'name',
'fieldtype': 'Link',
'options': 'Purchase Invoice',
'width': 170
},
{
'label': _('Supplier'),
'fieldname': 'supplier',
'fieldtype': 'Link',
'options': 'Supplier',
'width': 120
},
{
'label': _('Posting Date'),
'fieldname': 'posting_date',
'fieldtype': 'Date',
'width': 100
},
{
'label': _('Item Code'),
'fieldname': 'item_code',
'fieldtype': 'Link',
'options': 'Item',
'width': 100
},
{
'label': _('Item Name'),
'fieldname': 'item_name',
'fieldtype': 'Data',
'width': 100
},
{
'label': _('UOM'),
'fieldname': 'uom',
'fieldtype': 'Link',
'options': 'UOM',
'width': 100
},
{
'label': _('Invoiced Qty'),
'fieldname': 'qty',
'fieldtype': 'Float',
'width': 100
},
{
'label': _('Received Qty'),
'fieldname': 'received_qty',
'fieldtype': 'Float',
'width': 100
},
{
'label': _('Rate'),
'fieldname': 'rate',
'fieldtype': 'Currency',
'width': 100
},
{
'label': _('Amount'),
'fieldname': 'amount',
'fieldtype': 'Currency',
'width': 100
}
]

View File

@@ -435,6 +435,35 @@ class TestPurchaseOrder(unittest.TestCase):
po.load_from_db() po.load_from_db()
self.assertEqual(po.get("items")[0].received_qty, 5) self.assertEqual(po.get("items")[0].received_qty, 5)
def test_purchase_order_invoice_receipt_workflow(self):
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_purchase_receipt
po = create_purchase_order()
pi = make_pi_from_po(po.name)
pi.submit()
pr = make_purchase_receipt(pi.name)
pr.submit()
pi.load_from_db()
self.assertEquals(pi.per_received, 100.00)
self.assertEquals(pi.items[0].qty, pi.items[0].received_qty)
po.load_from_db()
self.assertEquals(po.per_received, 100.00)
self.assertEquals(po.per_billed, 100.00)
pr.cancel()
pi.load_from_db()
pi.cancel()
po.load_from_db()
po.cancel()
def test_make_purchase_invoice(self): def test_make_purchase_invoice(self):
po = create_purchase_order(do_not_submit=True) po = create_purchase_order(do_not_submit=True)

View File

@@ -0,0 +1,129 @@
# Version 13.1.0 Release Notes
### Features
- Recursive pricing rule ([#24922](https://github.com/frappe/erpnext/pull/24922))
- Discount configuration on early payments ([#24586](https://github.com/frappe/erpnext/pull/24586))
- Bulk e-invoice generation ([#24969](https://github.com/frappe/erpnext/pull/24969))
- Employee Self Service ([#24408](https://github.com/frappe/erpnext/pull/24408))
- Share doc with employee approvers if they don't have access ([#25190](https://github.com/frappe/erpnext/pull/25190))
- Price margin in buying ([#24685](https://github.com/frappe/erpnext/pull/24685))
- Allow changing Work Stations in Work Order & Job Card ([#24897](https://github.com/frappe/erpnext/pull/24897))
- Add document type field for e-invoicing (Italy) ([#25256](https://github.com/frappe/erpnext/pull/25256))
- Add checkbox for disabling leave notification in HR Settings ([#24877](https://github.com/frappe/erpnext/pull/24877))
- Enhancements in Material Request Plan Item in Production Plan ([#25025](https://github.com/frappe/erpnext/pull/25025))
### Fixes and Enhancements
- Mode of payments disappear on loading draft pos invoice ([#24917](https://github.com/frappe/erpnext/pull/24917))
- Sales order not saving due type mismatch in promo scheme (#24748) ([#25222](https://github.com/frappe/erpnext/pull/25222))
- Zero amount completed delivery notes being shown in Sales Invoice get items ([#25317](https://github.com/frappe/erpnext/pull/25317))
- Incorrect status creating PR from PO after creating PI ([#25109](https://github.com/frappe/erpnext/pull/25109))
- Precision and formatted document for stock level in item dashboard. ([#24921](https://github.com/frappe/erpnext/pull/24921))
- Precision issues while allocating advance amount ([#25086](https://github.com/frappe/erpnext/pull/25086))
- Round off final tax amount instead of current tax amount ([#25188](https://github.com/frappe/erpnext/pull/25188))
- Redesign fixes ([#24896](https://github.com/frappe/erpnext/pull/24896))
- TDS check getting checked after reload ([#24972](https://github.com/frappe/erpnext/pull/24972))
- Github Action not failing when tests fail ([#24867](https://github.com/frappe/erpnext/pull/24867))
- Calculate 80g certificate amount on validate for memberships ([#24925](https://github.com/frappe/erpnext/pull/24925))
- Purchase from registered composition dealer ([#25040](https://github.com/frappe/erpnext/pull/25040))
- Reduce number of queries for checking if future SL entry exists ([#24881](https://github.com/frappe/erpnext/pull/24881))
- Remove unwanted parameter in calculate_rate_and_amount ([#24883](https://github.com/frappe/erpnext/pull/24883))
- Membership renewal validation ([#24963](https://github.com/frappe/erpnext/pull/24963))
- Not able to save material request ([#25112](https://github.com/frappe/erpnext/pull/25112))
- POS print receipt ([#25330](https://github.com/frappe/erpnext/pull/25330))
- Supplier was not able to Submit RFQ due to insufficient permission ([#24622](https://github.com/frappe/erpnext/pull/24622))
- Unequal debit and credit issue on RCM Invoice ([#24836](https://github.com/frappe/erpnext/pull/24836))
- Picked Qty conversion from Stock Qty to Qty while creating DN from Pick List ([#25105](https://github.com/frappe/erpnext/pull/25105))
- Salary Structure object has no attribute set_totals ([#25113](https://github.com/frappe/erpnext/pull/25113))
- Incorrect Nil Exempt and Non GST amount in GSTR3B report ([#24916](https://github.com/frappe/erpnext/pull/24916))
- Add method for regional round off account back ([#24893](https://github.com/frappe/erpnext/pull/24893))
- Employee profile pic upload access for erpnext user ([#25022](https://github.com/frappe/erpnext/pull/25022))
- Make filters for payroll entry ([#25386](https://github.com/frappe/erpnext/pull/25386))
- Fix dynamically changing grid properties ([#25310](https://github.com/frappe/erpnext/pull/25310))
- Consider paid repayment entries in subsequent loan repayments ([#25271](https://github.com/frappe/erpnext/pull/25271))
- Allow duplicate additional salaries ([#24842](https://github.com/frappe/erpnext/pull/24842))
- Object referencing the same address issue ([#25159](https://github.com/frappe/erpnext/pull/25159))
- Validating party currency with doc currency ([#24318](https://github.com/frappe/erpnext/pull/24318))
- Non Profit fixes ([#25060](https://github.com/frappe/erpnext/pull/25060))
- Additional Salary component amount not getting set ([#25356](https://github.com/frappe/erpnext/pull/25356))
- Allow user to update exchange rate in Multi-currency LCV ([#24912](https://github.com/frappe/erpnext/pull/24912))
- Allow creating stock entry based on work order for customer provided items ([#24885](https://github.com/frappe/erpnext/pull/24885))
- Create property setters for shorter naming series on setup ([#25128](https://github.com/frappe/erpnext/pull/25128))
- Add GST category field in Delivery Note ([#25053](https://github.com/frappe/erpnext/pull/25053))
- Ignore Permission for Leave Ledger Entry ([#25172](https://github.com/frappe/erpnext/pull/25172))
- Pending shortfall update on processing loan security shortfall ([#24971](https://github.com/frappe/erpnext/pull/24971))
- Added flag for dont_fetch_price_list_rate in transaction ([#25041](https://github.com/frappe/erpnext/pull/25041))
- Exchange Rate not getting set in Salary Slip ([#25004](https://github.com/frappe/erpnext/pull/25004))
- Repost not completed backdated transactions ([#24980](https://github.com/frappe/erpnext/pull/24980))
- frappe.whitelist for doc methods ([#25230](https://github.com/frappe/erpnext/pull/25230))
- Opportunity-quotation mapping order status ([#25001](https://github.com/frappe/erpnext/pull/25001))
- GST on freight charge in e-invoicing ([#25000](https://github.com/frappe/erpnext/pull/25000))
- Role to override maintain same rate check in transactions ([#25193](https://github.com/frappe/erpnext/pull/25193))
- Added blank option for status in report related to issue ([#25082](https://github.com/frappe/erpnext/pull/25082))
- Cashier query in POS Opening/Closing Entry ([#25399](https://github.com/frappe/erpnext/pull/25399))
- Lead Source's module ([#24583](https://github.com/frappe/erpnext/pull/24583))
- Hide alt tag if item is not shown in website ([#24937](https://github.com/frappe/erpnext/pull/24937))
- Ignore Customer Group Perm on All Products page ([#25397](https://github.com/frappe/erpnext/pull/25397))
- Give first preference to loan security on repayment ([#25212](https://github.com/frappe/erpnext/pull/25212))
- Add shortfall ratio in Loan Security Shortfall ([#25138](https://github.com/frappe/erpnext/pull/25138))
- Condition for SLA status banner ([#25261](https://github.com/frappe/erpnext/pull/25261))
- Component amount calculation based on formula with abbr not working ([#25117](https://github.com/frappe/erpnext/pull/25117))
- Remove gst name validation for purchase Invoice ([#25235](https://github.com/frappe/erpnext/pull/25235))
- Do not fetch stopped MR in production plan ([#25063](https://github.com/frappe/erpnext/pull/25063))
- Backport missing commits to develop branch ([#25305](https://github.com/frappe/erpnext/pull/25305))
- UOM length unit in global setup list is empty ([#24855](https://github.com/frappe/erpnext/pull/24855))
- Round total quantity in job card ([#25240](https://github.com/frappe/erpnext/pull/25240))
- Default total_estimated_cost to zero ([#24939](https://github.com/frappe/erpnext/pull/24939))
- Serial no refresh issue ([#25127](https://github.com/frappe/erpnext/pull/25127))
- Correct calculation for discount amount when margin is set ([#25179](https://github.com/frappe/erpnext/pull/25179))
- Get correct holiday list when calculating dates; test fixes ([#24901](https://github.com/frappe/erpnext/pull/24901))
- POS print receipt ([#24924](https://github.com/frappe/erpnext/pull/24924))
- Condition for setting agreement status ([#25255](https://github.com/frappe/erpnext/pull/25255))
- Loan Repayment entry cancellation on salary slip cancel ([#24879](https://github.com/frappe/erpnext/pull/24879))
- Add company validation for e-invoicing ([#25349](https://github.com/frappe/erpnext/pull/25349))
- Query values incorrectly escaped while back updating Quality Inspection ([#25118](https://github.com/frappe/erpnext/pull/25118))
- Update Bin via Update Item on Purchase/Sales Order ([#23509](https://github.com/frappe/erpnext/pull/23509))
- Declare data before assigning ([#25287](https://github.com/frappe/erpnext/pull/25287))
- Do not set standard link in Sales Invoice as custom ([#25096](https://github.com/frappe/erpnext/pull/25096))
- Hide serial and batch selector in Stock Entry ([#25107](https://github.com/frappe/erpnext/pull/25107))
- Taxable value including Freight and Forwarding charges in GSTR-1 Report ([#25290](https://github.com/frappe/erpnext/pull/25290))
- Remove nonexistent method from pick list ([#25279](https://github.com/frappe/erpnext/pull/25279))
- Allow zero valuation in stock reconciliation ([#24888](https://github.com/frappe/erpnext/pull/24888))
- Place of supply of e-invoicing ([#25148](https://github.com/frappe/erpnext/pull/25148))
- Delivery note print error ([#25080](https://github.com/frappe/erpnext/pull/25080))
- Fix Payment references from disappearing on adding Cost Center in Payment Entry ([#24831](https://github.com/frappe/erpnext/pull/24831))
- Company field in Warehouse ([#25196](https://github.com/frappe/erpnext/pull/25196))
- Available employee for selection ([#25378](https://github.com/frappe/erpnext/pull/25378))
- Cannot set qty to less than zero ([#25258](https://github.com/frappe/erpnext/pull/25258))
- Don't delete mode of payment account details while deleting comp… ([#25217](https://github.com/frappe/erpnext/pull/25217))
- Exclude current doc while validation. ([#24914](https://github.com/frappe/erpnext/pull/24914))
- POS Opening Entry with empty balance detail rows ([#24876](https://github.com/frappe/erpnext/pull/24876))
- Unable to submit stock entry ([#25033](https://github.com/frappe/erpnext/pull/25033))
- BOM cost test case ([#25242](https://github.com/frappe/erpnext/pull/25242))
- Filter for employees in salary slip ([#25361](https://github.com/frappe/erpnext/pull/25361))
- Added correct path in hooks ([#24862](https://github.com/frappe/erpnext/pull/24862))
- Patch regional fields for old companies ([#24988](https://github.com/frappe/erpnext/pull/24988))
- consolidated sales invoice posting date ([#25119](https://github.com/frappe/erpnext/pull/25119))
- Don't set "Company:company:default_currency" as default for currency link fields ([#25095](https://github.com/frappe/erpnext/pull/25095))
- Healthcare lab module rename fields ([#25276](https://github.com/frappe/erpnext/pull/25276))
- Error message compensatory leave request ([#25206](https://github.com/frappe/erpnext/pull/25206))
- Adding company link to e invoice settings patch condition ([#25301](https://github.com/frappe/erpnext/pull/25301))
- Membership and Donation API fixes ([#24900](https://github.com/frappe/erpnext/pull/24900))
- Set correct ack no. on irn generation ([#25251](https://github.com/frappe/erpnext/pull/25251))
- Report Issue Summary fix for zero issues ([#24934](https://github.com/frappe/erpnext/pull/24934))
- Validation msg for TransDocNo e-invoicing ([#25121](https://github.com/frappe/erpnext/pull/25121))
- Correct state code for 'Other Territory' ([#24993](https://github.com/frappe/erpnext/pull/24993))
- Commit individual SLE rename for large datasets (develop) ([#25084](https://github.com/frappe/erpnext/pull/25084))
- Remove shipping address GSTIN validation for e-invoice ([#25153](https://github.com/frappe/erpnext/pull/25153))
- Period list for exponential smoothing forecasting report ([#24982](https://github.com/frappe/erpnext/pull/24982))
- Customer creation from shopping cart ([#25136](https://github.com/frappe/erpnext/pull/25136))
- Simplified logic for additional salary ([#24824](https://github.com/frappe/erpnext/pull/24824))
- Item wise tax rate for consolidated POS invoice ([#25029](https://github.com/frappe/erpnext/pull/25029))
- Column width in Recruitment analytics report ([#25003](https://github.com/frappe/erpnext/pull/25003))
- Filter Bank Account drop-down list in Bank Reconciliation Tool ([#24873](https://github.com/frappe/erpnext/pull/24873))
- Payroll issues ([#24540](https://github.com/frappe/erpnext/pull/24540))
- PO not created against all selected suppliers (drop shipping) ([#24863](https://github.com/frappe/erpnext/pull/24863))
- Can't multiply sequence by non-int of type 'float' ([#25092](https://github.com/frappe/erpnext/pull/25092))
- Make Discharge Schedule Date as Datetime ([#24940](https://github.com/frappe/erpnext/pull/24940))
- Serial no trim issue ([#24949](https://github.com/frappe/erpnext/pull/24949))

View File

@@ -0,0 +1,56 @@
# Version 13.2.0 Release Notes
### Features & Enhancements
- Employee Hours Utilization Report ([#25209](https://github.com/frappe/erpnext/pull/25209))
- Delayed Tasks Summary Report ([#25024](https://github.com/frappe/erpnext/pull/25024))
- Project Profitability Report ([#24944](https://github.com/frappe/erpnext/pull/24944))
- Timer in LMS Quiz ([#24246](https://github.com/frappe/erpnext/pull/24246))
- Role to allow over billing, delivery, receipt ([#24854](https://github.com/frappe/erpnext/pull/24854))
- Auto calculate distance for e-way bill generations ([#25480](https://github.com/frappe/erpnext/pull/25480))
- Add total available stock field in PO ([#24878](https://github.com/frappe/erpnext/pull/24878))
- Refactored Setup Taxes and Charges ([#24805](https://github.com/frappe/erpnext/pull/24805))
- Inpatient Occupancy Table Editable for Healthcare Admin ([#24989](https://github.com/frappe/erpnext/pull/24989))
- Added Disable Rounded Total in sales transactions ([#25362](https://github.com/frappe/erpnext/pull/25362))
### Fixes
- Incorrect GL Entry validation ([#25474](https://github.com/frappe/erpnext/pull/25474))
- Cannot create item variants ([#25433](https://github.com/frappe/erpnext/pull/25433))
- Leave policy in leave allocation ([#25334](https://github.com/frappe/erpnext/pull/25334))
- Let Administrator delete company transactions ([#25300](https://github.com/frappe/erpnext/pull/25300))
- Display reconcile tool when closing balance 0 ([#25417](https://github.com/frappe/erpnext/pull/25417))
- Bulk Salary Structure Assignment ([#25389](https://github.com/frappe/erpnext/pull/25389))
- Payment amount showing in foreign currency ([#25518](https://github.com/frappe/erpnext/pull/25518))
- Commit changes to shipment status in database ([#25374](https://github.com/frappe/erpnext/pull/25374))
- Add amend perm for loan and system manager for loan doctypes ([#25393](https://github.com/frappe/erpnext/pull/25393))
- Cashier query in POS Opening/Closing Entry ([#25398](https://github.com/frappe/erpnext/pull/25398))
- Apply single transaction threshold on net_total instead of supplier credit amount ([#25243](https://github.com/frappe/erpnext/pull/25243))
- Update allocated amount after paid amount is changed in PE ([#25528](https://github.com/frappe/erpnext/pull/25528))
- Remove non-standard module cards from Home Workspace ([#25391](https://github.com/frappe/erpnext/pull/25391))
- Cannot scan spacebar character in pos ([#25479](https://github.com/frappe/erpnext/pull/25479))
- Permission error after submitting exchange rate revaluation ([#25432](https://github.com/frappe/erpnext/pull/25432))
- Equality check instead of assignment in cart ([#25372](https://github.com/frappe/erpnext/pull/25372))
- Disable auto naming of customer during import ([#25152](https://github.com/frappe/erpnext/pull/25152))
- Additional Salary component amount not getting set ([#25355](https://github.com/frappe/erpnext/pull/25355))
- Round off values near to zero ([#25304](https://github.com/frappe/erpnext/pull/25304))
- Allow to cancel loan with cancelled repayment entry ([#25508](https://github.com/frappe/erpnext/pull/25508))
- Currency symbol in bank transaction list view ([#25336](https://github.com/frappe/erpnext/pull/25336))
- Incorrect batch picked in subcontracted purchase receipt ([#25186](https://github.com/frappe/erpnext/pull/25186))
- Issue in project custom status ([#25452](https://github.com/frappe/erpnext/pull/25452))
- Shipment pickup_to, pickup_from functionality. ([#25359](https://github.com/frappe/erpnext/pull/25359))
- Stock ledger entry created against draft stock entry ([#25539](https://github.com/frappe/erpnext/pull/25539))
- Ageing errors in PSOA ([#25529](https://github.com/frappe/erpnext/pull/25529))
- Permission error while adding weekly holidays ([#25450](https://github.com/frappe/erpnext/pull/25450))
- Filter for employees in salary slip ([#25360](https://github.com/frappe/erpnext/pull/25360))
- Backward compatibility for GSTR-1 report ([#25444](https://github.com/frappe/erpnext/pull/25444))
- Incorrect incoming rate for the sales return ([#25145](https://github.com/frappe/erpnext/pull/25145))
- POS print receipt ([#25328](https://github.com/frappe/erpnext/pull/25328))
- Laboratory Module patch ([#25431](https://github.com/frappe/erpnext/pull/25431))
- Performance: fetching exchange rate on every line item slows down PO ([#25345](https://github.com/frappe/erpnext/pull/25345))
- Presentation currency in statement of accounts ([#25367](https://github.com/frappe/erpnext/pull/25367))
- Serial No not updated correctly via Inter Company Stock Transfer ([#25006](https://github.com/frappe/erpnext/pull/25006))
- Ignore Customer Group Perm on All Products page ([#25396](https://github.com/frappe/erpnext/pull/25396))
- Change subcontracted item display ([#25425](https://github.com/frappe/erpnext/pull/25425))
- Add company validation for e-invoicing ([#25348](https://github.com/frappe/erpnext/pull/25348))

View File

@@ -90,6 +90,8 @@ class AccountsController(TransactionBase):
self.ensure_supplier_is_not_blocked() self.ensure_supplier_is_not_blocked()
self.validate_date_with_fiscal_year() self.validate_date_with_fiscal_year()
self.validate_party_accounts()
self.validate_inter_company_reference() self.validate_inter_company_reference()
self.set_incoming_rate() self.set_incoming_rate()
@@ -233,6 +235,23 @@ class AccountsController(TransactionBase):
validate_fiscal_year(self.get(date_field), self.fiscal_year, self.company, validate_fiscal_year(self.get(date_field), self.fiscal_year, self.company,
self.meta.get_label(date_field), self) self.meta.get_label(date_field), self)
def validate_party_accounts(self):
if self.doctype not in ('Sales Invoice', 'Purchase Invoice'):
return
if self.doctype == 'Sales Invoice':
party_account_field = 'debit_to'
item_field = 'income_account'
else:
party_account_field = 'credit_to'
item_field = 'expense_account'
for item in self.get('items'):
if item.get(item_field) == self.get(party_account_field):
frappe.throw(_("Row {0}: {1} {2} cannot be same as {3} (Party Account) {4}").format(item.idx,
frappe.bold(frappe.unscrub(item_field)), item.get(item_field),
frappe.bold(frappe.unscrub(party_account_field)), self.get(party_account_field)))
def validate_inter_company_reference(self): def validate_inter_company_reference(self):
if self.doctype not in ('Purchase Invoice', 'Purchase Receipt', 'Purchase Order'): if self.doctype not in ('Purchase Invoice', 'Purchase Receipt', 'Purchase Order'):
return return
@@ -904,29 +923,34 @@ class AccountsController(TransactionBase):
date = self.get("due_date") date = self.get("due_date")
due_date = date or posting_date due_date = date or posting_date
if party_account_currency == self.company_currency: base_grand_total = self.get("base_rounded_total") or self.base_grand_total
grand_total = self.get("base_rounded_total") or self.base_grand_total
else:
grand_total = self.get("rounded_total") or self.grand_total grand_total = self.get("rounded_total") or self.grand_total
if self.doctype in ("Sales Invoice", "Purchase Invoice"): if self.doctype in ("Sales Invoice", "Purchase Invoice"):
base_grand_total = base_grand_total - flt(self.base_write_off_amount)
grand_total = grand_total - flt(self.write_off_amount) grand_total = grand_total - flt(self.write_off_amount)
if self.get("total_advance"): if self.get("total_advance"):
if party_account_currency == self.company_currency:
base_grand_total -= self.get("total_advance")
grand_total = flt(base_grand_total / self.get("conversion_rate"), self.precision("grand_total"))
else:
grand_total -= self.get("total_advance") grand_total -= self.get("total_advance")
base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total"))
if not self.get("payment_schedule"): if not self.get("payment_schedule"):
if self.get("payment_terms_template"): if self.get("payment_terms_template"):
data = get_payment_terms(self.payment_terms_template, posting_date, grand_total) data = get_payment_terms(self.payment_terms_template, posting_date, grand_total, base_grand_total)
for item in data: for item in data:
self.append("payment_schedule", item) self.append("payment_schedule", item)
else: else:
data = dict(due_date=due_date, invoice_portion=100, payment_amount=grand_total) data = dict(due_date=due_date, invoice_portion=100, payment_amount=grand_total, base_payment_amount=base_grand_total)
self.append("payment_schedule", data) self.append("payment_schedule", data)
else: else:
for d in self.get("payment_schedule"): for d in self.get("payment_schedule"):
if d.invoice_portion: if d.invoice_portion:
d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount')) d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount'))
d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount'))
d.outstanding = d.payment_amount d.outstanding = d.payment_amount
def set_due_date(self): def set_due_date(self):
@@ -963,22 +987,28 @@ class AccountsController(TransactionBase):
if self.get("payment_schedule"): if self.get("payment_schedule"):
total = 0 total = 0
base_total = 0
for d in self.get("payment_schedule"): for d in self.get("payment_schedule"):
total += flt(d.payment_amount) total += flt(d.payment_amount)
base_total += flt(d.base_payment_amount)
if party_account_currency == self.company_currency: base_grand_total = self.get("base_rounded_total") or self.base_grand_total
total = flt(total, self.precision("base_grand_total")) grand_total = self.get("rounded_total") or self.grand_total
grand_total = flt(self.get("base_rounded_total") or self.base_grand_total, self.precision('base_grand_total'))
else:
total = flt(total, self.precision("grand_total"))
grand_total = flt(self.get("rounded_total") or self.grand_total, self.precision('grand_total'))
if self.get("total_advance"):
grand_total -= self.get("total_advance")
if self.doctype in ("Sales Invoice", "Purchase Invoice"): if self.doctype in ("Sales Invoice", "Purchase Invoice"):
base_grand_total = base_grand_total - flt(self.base_write_off_amount)
grand_total = grand_total - flt(self.write_off_amount) grand_total = grand_total - flt(self.write_off_amount)
if total != flt(grand_total, self.precision("grand_total")):
if self.get("total_advance"):
if party_account_currency == self.company_currency:
base_grand_total -= self.get("total_advance")
grand_total = flt(base_grand_total / self.get("conversion_rate"), self.precision("grand_total"))
else:
grand_total -= self.get("total_advance")
base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total"))
print(grand_total, base_grand_total)
if total != flt(grand_total, self.precision("grand_total")) or \
base_total != flt(base_grand_total, self.precision("base_grand_total")):
frappe.throw(_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total")) frappe.throw(_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total"))
def is_rounded_total_disabled(self): def is_rounded_total_disabled(self):
@@ -1218,7 +1248,7 @@ def update_invoice_status():
@frappe.whitelist() @frappe.whitelist()
def get_payment_terms(terms_template, posting_date=None, grand_total=None, bill_date=None): def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):
if not terms_template: if not terms_template:
return return
@@ -1226,14 +1256,14 @@ def get_payment_terms(terms_template, posting_date=None, grand_total=None, bill_
schedule = [] schedule = []
for d in terms_doc.get("terms"): for d in terms_doc.get("terms"):
term_details = get_payment_term_details(d, posting_date, grand_total, bill_date) term_details = get_payment_term_details(d, posting_date, grand_total, base_grand_total, bill_date)
schedule.append(term_details) schedule.append(term_details)
return schedule return schedule
@frappe.whitelist() @frappe.whitelist()
def get_payment_term_details(term, posting_date=None, grand_total=None, bill_date=None): def get_payment_term_details(term, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):
term_details = frappe._dict() term_details = frappe._dict()
if isinstance(term, text_type): if isinstance(term, text_type):
term = frappe.get_doc("Payment Term", term) term = frappe.get_doc("Payment Term", term)
@@ -1242,9 +1272,9 @@ def get_payment_term_details(term, posting_date=None, grand_total=None, bill_dat
term_details.description = term.description term_details.description = term.description
term_details.invoice_portion = term.invoice_portion term_details.invoice_portion = term.invoice_portion
term_details.payment_amount = flt(term.invoice_portion) * flt(grand_total) / 100 term_details.payment_amount = flt(term.invoice_portion) * flt(grand_total) / 100
term_details.base_payment_amount = flt(term.invoice_portion) * flt(base_grand_total) / 100
term_details.discount_type = term.discount_type term_details.discount_type = term.discount_type
term_details.discount = term.discount term_details.discount = term.discount
# term_details.discounted_amount = flt(grand_total) * (term.discount / 100) if term.discount_type == 'Percentage' else discount
term_details.outstanding = term_details.payment_amount term_details.outstanding = term_details.payment_amount
term_details.mode_of_payment = term.mode_of_payment term_details.mode_of_payment = term.mode_of_payment

View File

@@ -262,7 +262,8 @@ def copy_attributes_to_variant(item, variant):
# copy non no-copy fields # copy non no-copy fields
exclude_fields = ["naming_series", "item_code", "item_name", "show_in_website", exclude_fields = ["naming_series", "item_code", "item_name", "show_in_website",
"show_variant_in_website", "opening_stock", "variant_of", "valuation_rate"] "show_variant_in_website", "opening_stock", "variant_of", "valuation_rate",
"has_variants", "attributes"]
if item.variant_based_on=='Manufacturer': if item.variant_based_on=='Manufacturer':
# don't copy manufacturer values if based on part no # don't copy manufacturer values if based on part no

View File

@@ -17,10 +17,12 @@ class AmazonMWSSettings(Document):
else: else:
self.enable_sync = 0 self.enable_sync = 0
@frappe.whitelist()
def get_products_details(self): def get_products_details(self):
if self.enable_amazon == 1: if self.enable_amazon == 1:
frappe.enqueue('erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods.get_products_details') frappe.enqueue('erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods.get_products_details')
@frappe.whitelist()
def get_order_details(self): def get_order_details(self):
if self.enable_amazon == 1: if self.enable_amazon == 1:
after_date = dateutil.parser.parse(self.after_date).strftime("%Y-%m-%d") after_date = dateutil.parser.parse(self.after_date).strftime("%Y-%m-%d")

View File

@@ -50,6 +50,7 @@ class TherapyType(Document):
self.db_set('change_in_item', 0) self.db_set('change_in_item', 0)
@frappe.whitelist()
def add_exercises(self): def add_exercises(self):
exercises = self.get_exercises_for_body_parts() exercises = self.get_exercises_for_body_parts()
last_idx = max([cint(d.idx) for d in self.get('exercises')] or [0,]) last_idx = max([cint(d.idx) for d in self.get('exercises')] or [0,])

View File

@@ -34,7 +34,7 @@ frappe.ui.form.on('Employee Advance', {
}; };
}); });
frm.set_query('salary_component', function(doc) { frm.set_query('salary_component', function() {
return { return {
filters: { filters: {
"type": "Deduction" "type": "Deduction"
@@ -44,16 +44,17 @@ frappe.ui.form.on('Employee Advance', {
}, },
refresh: function(frm) { refresh: function(frm) {
if (frm.doc.docstatus===1 if (frm.doc.docstatus === 1 &&
&& (flt(frm.doc.paid_amount) < flt(frm.doc.advance_amount)) (flt(frm.doc.paid_amount) < flt(frm.doc.advance_amount)) &&
&& frappe.model.can_create("Payment Entry")) { frappe.model.can_create("Payment Entry")) {
frm.add_custom_button(__('Payment'), frm.add_custom_button(__('Payment'),
function() { frm.events.make_payment_entry(frm); }, __('Create')); function () {
} frm.events.make_payment_entry(frm);
else if ( }, __('Create'));
frm.doc.docstatus === 1 } else if (
&& flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount) - flt(frm.doc.return_amount) frm.doc.docstatus === 1 &&
&& frappe.model.can_create("Expense Claim") flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount) - flt(frm.doc.return_amount) &&
frappe.model.can_create("Expense Claim")
) { ) {
frm.add_custom_button( frm.add_custom_button(
__("Expense Claim"), __("Expense Claim"),
@@ -64,8 +65,8 @@ frappe.ui.form.on('Employee Advance', {
); );
} }
if (frm.doc.docstatus === 1 if (frm.doc.docstatus === 1 &&
&& (flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount) && flt(frm.doc.paid_amount) != flt(frm.doc.return_amount))) { (flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount) && flt(frm.doc.paid_amount) != flt(frm.doc.return_amount))) {
if (frm.doc.repay_unclaimed_amount_from_salary == 0 && frappe.model.can_create("Journal Entry")) { if (frm.doc.repay_unclaimed_amount_from_salary == 0 && frappe.model.can_create("Journal Entry")) {
frm.add_custom_button(__("Return"), function() { frm.add_custom_button(__("Return"), function() {
@@ -215,8 +216,8 @@ frappe.ui.form.on('Employee Advance', {
callback: function(r) { callback: function(r) {
frm.set_value("exchange_rate", flt(r.message)); frm.set_value("exchange_rate", flt(r.message));
frm.set_df_property('exchange_rate', 'hidden', 0); frm.set_df_property('exchange_rate', 'hidden', 0);
frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency +
+ " = [?] " + company_currency); " = [?] " + company_currency);
} }
}); });
} }

View File

@@ -0,0 +1,68 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Employee Referral", {
refresh: function(frm) {
if (frm.doc.docstatus === 1 && frm.doc.status === "Pending") {
frm.add_custom_button(__("Reject Employee Referral"), function() {
frappe.confirm(
__("Are you sure you want to reject the Employee Referral?"),
function() {
frm.doc.status = "Rejected";
frm.dirty();
frm.save_or_update();
},
function() {
window.close();
}
);
});
frm.add_custom_button(__("Create Job Applicant"), function() {
frm.events.create_job_applicant(frm);
}).addClass("btn-primary");
}
// To check whether Payment is done or not
if (frm.doc.docstatus === 1 && frm.doc.status === "Accepted") {
frappe.db.get_list("Additional Salary", {
filters: {
ref_docname: cur_frm.doc.name,
docstatus: 1
},
fields: ["count(name) as additional_salary_count"]
}).then((data) => {
let additional_salary_count = data[0].additional_salary_count;
if (frm.doc.is_applicable_for_referral_bonus && !additional_salary_count) {
frm.add_custom_button(__("Create Additional Salary"), function() {
frm.events.create_additional_salary(frm);
}).addClass("btn-primary");
}
});
}
},
create_job_applicant: function(frm) {
frappe.model.open_mapped_doc({
method: "erpnext.hr.doctype.employee_referral.employee_referral.create_job_applicant",
frm: frm
});
},
create_additional_salary: function(frm) {
frappe.call({
method: "erpnext.hr.doctype.employee_referral.employee_referral.create_additional_salary",
args: {
doc: frm.doc
},
callback: function (r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
}
});
},
});

View File

@@ -0,0 +1,294 @@
{
"actions": [],
"autoname": "format:HR-REF-{####}",
"creation": "2021-03-23 14:54:45.047051",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"first_name",
"last_name",
"full_name",
"email",
"contact_no",
"resume",
"resume_link",
"column_break_6",
"date",
"status",
"for_designation",
"current_employer",
"current_job_title",
"referrer_details_section",
"referrer",
"referrer_name",
"column_break_14",
"is_applicable_for_referral_bonus",
"referral_payment_status",
"department",
"additional_information_section",
"qualification_reason",
"work_references",
"amended_from"
],
"fields": [
{
"fieldname": "first_name",
"fieldtype": "Data",
"label": "First Name ",
"reqd": 1
},
{
"fieldname": "last_name",
"fieldtype": "Data",
"label": "Last Name",
"reqd": 1
},
{
"fieldname": "full_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Full Name",
"read_only": 1
},
{
"fieldname": "contact_no",
"fieldtype": "Data",
"in_standard_filter": 1,
"label": "Contact No.",
"options": "Phone"
},
{
"fieldname": "current_employer",
"fieldtype": "Data",
"label": "Current Employer "
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"fieldname": "date",
"fieldtype": "Date",
"in_standard_filter": 1,
"label": "Date",
"reqd": 1
},
{
"allow_on_submit": 1,
"fieldname": "status",
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "Pending\nIn Process\nAccepted\nRejected",
"permlevel": 1,
"read_only": 1,
"reqd": 1
},
{
"fieldname": "current_job_title",
"fieldtype": "Data",
"label": "Current Job Title"
},
{
"fieldname": "resume",
"fieldtype": "Attach",
"label": "Resume"
},
{
"fieldname": "referrer_details_section",
"fieldtype": "Section Break",
"label": "Referrer Details"
},
{
"fetch_from": "employee.department",
"fieldname": "department",
"fieldtype": "Link",
"label": "Department",
"options": "Department",
"read_only": 1
},
{
"fieldname": "additional_information_section",
"fieldtype": "Section Break",
"label": "Additional Information "
},
{
"fieldname": "work_references",
"fieldtype": "Text Editor",
"label": "Work References"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Employee Referral",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_14",
"fieldtype": "Column Break"
},
{
"fieldname": "for_designation",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "For Designation ",
"options": "Designation",
"reqd": 1
},
{
"fieldname": "email",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Email",
"options": "Email",
"reqd": 1,
"unique": 1
},
{
"default": "1",
"fieldname": "is_applicable_for_referral_bonus",
"fieldtype": "Check",
"label": "Is Applicable for Referral Bonus"
},
{
"fieldname": "qualification_reason",
"fieldtype": "Text Editor",
"label": "Why is this Candidate Qualified for this Position?"
},
{
"fieldname": "referrer",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Referrer",
"options": "Employee",
"reqd": 1
},
{
"fetch_from": "referrer.employee_name",
"fieldname": "referrer_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Referrer Name",
"read_only": 1
},
{
"fieldname": "resume_link",
"fieldtype": "Data",
"label": "Resume Link"
},
{
"fieldname": "referral_payment_status",
"fieldtype": "Select",
"label": "Referral Bonus Payment Status",
"options": "\nUnpaid\nPaid",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-04-26 21:21:38.094086",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Referral",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"amend": 1,
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"submit": 1,
"write": 1
},
{
"delete": 1,
"email": 1,
"export": 1,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"share": 1,
"write": 1
},
{
"delete": 1,
"email": 1,
"export": 1,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"share": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "full_name"
}

View File

@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import get_link_to_form
from frappe.model.document import Document
class EmployeeReferral(Document):
def validate(self):
self.set_full_name()
self.set_referral_bonus_payment_status()
def set_full_name(self):
self.full_name = " ".join(filter(None, [self.first_name, self.last_name]))
def set_referral_bonus_payment_status(self):
if not self.is_applicable_for_referral_bonus:
self.referral_payment_status = ""
else:
if not self.referral_payment_status:
self.referral_payment_status = "Unpaid"
@frappe.whitelist()
def create_job_applicant(source_name, target_doc=None):
emp_ref = frappe.get_doc("Employee Referral", source_name)
#just for Api call if some set status apart from default Status
status = emp_ref.status
if emp_ref.status in ["Pending", "In process"]:
status = "Open"
job_applicant = frappe.new_doc("Job Applicant")
job_applicant.employee_referral = emp_ref.name
job_applicant.status = status
job_applicant.applicant_name = emp_ref.full_name
job_applicant.email_id = emp_ref.email
job_applicant.phone_number = emp_ref.contact_no
job_applicant.resume_attachment = emp_ref.resume
job_applicant.resume_link = emp_ref.resume_link
job_applicant.save()
frappe.msgprint(_("Job Applicant {0} created successfully.").format(
get_link_to_form("Job Applicant", job_applicant.name)),
title=_("Success"), indicator="green")
emp_ref.db_set("status", "In Process")
return job_applicant
@frappe.whitelist()
def create_additional_salary(doc):
import json
from six import string_types
if isinstance(doc, string_types):
doc = frappe._dict(json.loads(doc))
if not frappe.db.exists("Additional Salary", {"ref_docname": doc.name}):
additional_salary = frappe.new_doc("Additional Salary")
additional_salary.employee = doc.referrer
additional_salary.company = frappe.db.get_value("Employee", doc.referrer, "company")
additional_salary.overwrite_salary_structure_amount = 0
additional_salary.ref_doctype = doc.doctype
additional_salary.ref_docname = doc.name
return additional_salary

View File

@@ -0,0 +1,15 @@
from __future__ import unicode_literals
def get_data():
return {
'fieldname': 'employee_referral',
'non_standard_fieldnames': {
'Additional Salary': 'ref_docname'
},
'transactions': [
{
'items': ['Job Applicant', 'Additional Salary']
},
]
}

View File

@@ -0,0 +1,14 @@
frappe.listview_settings['Employee Referral'] = {
add_fields: ["status"],
get_indicator: function (doc) {
if (doc.status == "Pending") {
return [__(doc.status), "grey", "status,=," + doc.status];
} else if (doc.status == "In Process") {
return [__(doc.status), "orange", "status,=," + doc.status];
} else if (doc.status == "Accepted") {
return [__(doc.status), "green", "status,=," + doc.status];
} else if (doc.status == "Rejected") {
return [__(doc.status), "red", "status,=," + doc.status];
}
},
};

View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
from frappe.utils import today
from erpnext.hr.doctype.designation.test_designation import create_designation
from erpnext.hr.doctype.employee_referral.employee_referral import create_job_applicant, create_additional_salary
from erpnext.hr.doctype.employee.test_employee import make_employee
import unittest
class TestEmployeeReferral(unittest.TestCase):
def test_workflow_and_status_sync(self):
emp_ref = create_employee_referral()
#Check Initial status
self.assertTrue(emp_ref.status, "Pending")
job_applicant = create_job_applicant(emp_ref.name)
#Check status sync
emp_ref.reload()
self.assertTrue(emp_ref.status, "In Process")
job_applicant.reload()
job_applicant.status = "Rejected"
job_applicant.save()
emp_ref.reload()
self.assertTrue(emp_ref.status, "Rejected")
job_applicant.reload()
job_applicant.status = "Accepted"
job_applicant.save()
emp_ref.reload()
self.assertTrue(emp_ref.status, "Accepted")
# Check for Referral reference in additional salary
add_sal = create_additional_salary(emp_ref)
self.assertTrue(add_sal.ref_docname, emp_ref.name)
def create_employee_referral():
emp_ref = frappe.new_doc("Employee Referral")
emp_ref.first_name = "Mahesh"
emp_ref.last_name = "Singh"
emp_ref.email = "a@b.c"
emp_ref.date = today()
emp_ref.for_designation = create_designation().name
emp_ref.referrer = make_employee("testassetmovemp@example.com", company="_Test Company")
emp_ref.is_applicable_for_employee_referral_compensation = 1
emp_ref.save()
emp_ref.submit()
return emp_ref

View File

@@ -16,6 +16,7 @@ class HolidayList(Document):
self.validate_days() self.validate_days()
self.total_holidays = len(self.holidays) self.total_holidays = len(self.holidays)
@frappe.whitelist()
def get_weekly_off_dates(self): def get_weekly_off_dates(self):
self.validate_values() self.validate_values()
date_list = self.get_weekly_off_date_list(self.from_date, self.to_date) date_list = self.get_weekly_off_date_list(self.from_date, self.to_date)
@@ -61,6 +62,7 @@ class HolidayList(Document):
return date_list return date_list
@frappe.whitelist()
def clear_table(self): def clear_table(self):
self.set('holidays', []) self.set('holidays', [])

View File

@@ -10,6 +10,7 @@
"retirement_age", "retirement_age",
"emp_created_by", "emp_created_by",
"column_break_4", "column_break_4",
"standard_working_hours",
"stop_birthday_reminders", "stop_birthday_reminders",
"expense_approver_mandatory_in_expense_claim", "expense_approver_mandatory_in_expense_claim",
"leave_settings", "leave_settings",
@@ -143,13 +144,18 @@
"fieldname": "send_leave_notification", "fieldname": "send_leave_notification",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Send Leave Notification" "label": "Send Leave Notification"
},
{
"fieldname": "standard_working_hours",
"fieldtype": "Int",
"label": "Standard Working Hours"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
"idx": 1, "idx": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-03-14 02:04:22.907159", "modified": "2021-04-26 10:52:56.192773",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "HR Settings", "name": "HR Settings",

View File

@@ -18,6 +18,7 @@
"job_title", "job_title",
"source", "source",
"source_name", "source_name",
"employee_referral",
"applicant_rating", "applicant_rating",
"section_break_6", "section_break_6",
"notes", "notes",
@@ -152,13 +153,20 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Currency", "label": "Currency",
"options": "Currency" "options": "Currency"
},
{
"fieldname": "employee_referral",
"fieldtype": "Link",
"label": "Employee Referral",
"options": "Employee Referral",
"read_only": 1
} }
], ],
"icon": "fa fa-user", "icon": "fa fa-user",
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-09-18 12:39:02.557563", "modified": "2021-03-24 15:51:11.117517",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Job Applicant", "name": "Job Applicant",

View File

@@ -28,10 +28,21 @@ class JobApplicant(Document):
if self.email_id: if self.email_id:
validate_email_address(self.email_id, True) validate_email_address(self.email_id, True)
if self.employee_referral:
self.set_status_for_employee_referral()
if not self.applicant_name and self.email_id: if not self.applicant_name and self.email_id:
guess = self.email_id.split('@')[0] guess = self.email_id.split('@')[0]
self.applicant_name = ' '.join([p.capitalize() for p in guess.split('.')]) self.applicant_name = ' '.join([p.capitalize() for p in guess.split('.')])
def set_status_for_employee_referral(self):
emp_ref = frappe.get_doc("Employee Referral", self.employee_referral)
if self.status in ["Open", "Replied", "Hold"]:
emp_ref.db_set("status", "In Process")
elif self.status in ["Accepted", "Rejected"]:
emp_ref.db_set("status", self.status)
def check_email_id_is_unique(self): def check_email_id_is_unique(self):
if self.email_id: if self.email_id:
names = frappe.db.sql_list("""select name from `tabJob Applicant` names = frappe.db.sql_list("""select name from `tabJob Applicant`

View File

@@ -29,6 +29,7 @@ class LeaveControlPanel(Document):
frappe.throw(_("{0} is required").format(self.meta.get_label(f))) frappe.throw(_("{0} is required").format(self.meta.get_label(f)))
self.validate_from_to_dates('from_date', 'to_date') self.validate_from_to_dates('from_date', 'to_date')
@frappe.whitelist()
def allocate_leave(self): def allocate_leave(self):
self.validate_values() self.validate_values()
leave_allocated_for = [] leave_allocated_for = []

View File

@@ -520,6 +520,15 @@
"onboard": 1, "onboard": 1,
"type": "Link" "type": "Link"
}, },
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Referral",
"link_to": "Employee Referral",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{ {
"dependencies": "", "dependencies": "",
"hidden": 0, "hidden": 0,
@@ -814,7 +823,7 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2021-03-24 17:35:21.483297", "modified": "2021-04-26 13:36:15.413819",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "HR", "name": "HR",

View File

@@ -44,6 +44,7 @@ class Loan(AccountsController):
def on_cancel(self): def on_cancel(self):
self.unlink_loan_security_pledge() self.unlink_loan_security_pledge()
self.ignore_linked_doctypes = ['GL Entry']
def set_missing_fields(self): def set_missing_fields(self):
if not self.company: if not self.company:
@@ -70,7 +71,6 @@ class Loan(AccountsController):
frappe.throw(_("Repay From Salary can be selected only for term loans")) frappe.throw(_("Repay From Salary can be selected only for term loans"))
def make_repayment_schedule(self): def make_repayment_schedule(self):
if not self.repayment_start_date: if not self.repayment_start_date:
frappe.throw(_("Repayment Start Date is mandatory for term loans")) frappe.throw(_("Repayment Start Date is mandatory for term loans"))
@@ -78,10 +78,9 @@ class Loan(AccountsController):
payment_date = self.repayment_start_date payment_date = self.repayment_start_date
balance_amount = self.loan_amount balance_amount = self.loan_amount
while(balance_amount > 0): while(balance_amount > 0):
interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100)) interest_amount = flt(balance_amount * flt(self.rate_of_interest) / (12*100))
principal_amount = self.monthly_repayment_amount - interest_amount principal_amount = self.monthly_repayment_amount - interest_amount
balance_amount = rounded(balance_amount + interest_amount - self.monthly_repayment_amount) balance_amount = flt(balance_amount + interest_amount - self.monthly_repayment_amount)
if balance_amount < 0: if balance_amount < 0:
principal_amount += balance_amount principal_amount += balance_amount
balance_amount = 0.0 balance_amount = 0.0
@@ -195,7 +194,8 @@ def request_loan_closure(loan, posting_date=None):
posting_date = getdate() posting_date = getdate()
amounts = calculate_amounts(loan, posting_date) amounts = calculate_amounts(loan, posting_date)
pending_amount = amounts['payable_amount'] + amounts['unaccrued_interest'] pending_amount = amounts['pending_principal_amount'] + amounts['unaccrued_interest'] + \
amounts['interest_amount'] + amounts['penalty_amount']
loan_type = frappe.get_value('Loan', loan, 'loan_type') loan_type = frappe.get_value('Loan', loan, 'loan_type')
write_off_limit = frappe.get_value('Loan Type', loan_type, 'write_off_amount') write_off_limit = frappe.get_value('Loan Type', loan_type, 'write_off_amount')

View File

@@ -56,25 +56,25 @@ class TestLoan(unittest.TestCase):
def test_loan(self): def test_loan(self):
loan = frappe.get_doc("Loan", {"applicant":self.applicant1}) loan = frappe.get_doc("Loan", {"applicant":self.applicant1})
self.assertEquals(loan.monthly_repayment_amount, 15052) self.assertEquals(loan.monthly_repayment_amount, 15052)
self.assertEquals(loan.total_interest_payable, 21034) self.assertEquals(flt(loan.total_interest_payable, 0), 21034)
self.assertEquals(loan.total_payment, 301034) self.assertEquals(flt(loan.total_payment, 0), 301034)
schedule = loan.repayment_schedule schedule = loan.repayment_schedule
self.assertEqual(len(schedule), 20) self.assertEqual(len(schedule), 20)
for idx, principal_amount, interest_amount, balance_loan_amount in [[3, 13369, 1683, 227079], [19, 14941, 105, 0], [17, 14740, 312, 29785]]: for idx, principal_amount, interest_amount, balance_loan_amount in [[3, 13369, 1683, 227080], [19, 14941, 105, 0], [17, 14740, 312, 29785]]:
self.assertEqual(schedule[idx].principal_amount, principal_amount) self.assertEqual(flt(schedule[idx].principal_amount, 0), principal_amount)
self.assertEqual(schedule[idx].interest_amount, interest_amount) self.assertEqual(flt(schedule[idx].interest_amount, 0), interest_amount)
self.assertEqual(schedule[idx].balance_loan_amount, balance_loan_amount) self.assertEqual(flt(schedule[idx].balance_loan_amount, 0), balance_loan_amount)
loan.repayment_method = "Repay Fixed Amount per Period" loan.repayment_method = "Repay Fixed Amount per Period"
loan.monthly_repayment_amount = 14000 loan.monthly_repayment_amount = 14000
loan.save() loan.save()
self.assertEquals(len(loan.repayment_schedule), 22) self.assertEquals(len(loan.repayment_schedule), 22)
self.assertEquals(loan.total_interest_payable, 22712) self.assertEquals(flt(loan.total_interest_payable, 0), 22712)
self.assertEquals(loan.total_payment, 302712) self.assertEquals(flt(loan.total_payment, 0), 302712)
def test_loan_with_security(self): def test_loan_with_security(self):

View File

@@ -435,7 +435,6 @@ def get_amounts(amounts, against_loan, posting_date):
@frappe.whitelist() @frappe.whitelist()
def calculate_amounts(against_loan, posting_date, payment_type=''): def calculate_amounts(against_loan, posting_date, payment_type=''):
amounts = { amounts = {
'penalty_amount': 0.0, 'penalty_amount': 0.0,
'interest_amount': 0.0, 'interest_amount': 0.0,

View File

@@ -773,3 +773,4 @@ erpnext.patches.v13_0.fix_non_unique_represents_company
erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing
erpnext.patches.v13_0.make_non_standard_user_type #13-04-2021 erpnext.patches.v13_0.make_non_standard_user_type #13-04-2021
erpnext.patches.v13_0.update_shipment_status erpnext.patches.v13_0.update_shipment_status
erpnext.patches.v13_0.remove_attribute_field_from_item_variant_setting

View File

@@ -22,5 +22,7 @@ def execute():
frappe.delete_doc("Page", "bank-reconciliation", force=1) frappe.delete_doc("Page", "bank-reconciliation", force=1)
frappe.reload_doc('accounts', 'doctype', 'bank_transaction')
rename_field("Bank Transaction", "debit", "deposit") rename_field("Bank Transaction", "debit", "deposit")
rename_field("Bank Transaction", "credit", "withdrawal") rename_field("Bank Transaction", "credit", "withdrawal")

View File

@@ -38,16 +38,37 @@ def execute():
""".format(doctype), {'parentfield': parentfield}) """.format(doctype), {'parentfield': parentfield})
# copy renamed child table fields (fields were already renamed in old doctype json, hence sql) # copy renamed child table fields (fields were already renamed in old doctype json, hence sql)
frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_name = test_name""") rename_fields = {
frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_event = test_event""") 'lab_test_name': 'test_name',
frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_uom = test_uom""") 'lab_test_event': 'test_event',
frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_comment = test_comment""") 'lab_test_uom': 'test_uom',
'lab_test_comment': 'test_comment'
}
for new, old in rename_fields.items():
if frappe.db.has_column('Normal Test Result', old):
frappe.db.sql("""UPDATE `tabNormal Test Result` SET {} = {}"""
.format(new, old))
if frappe.db.has_column('Normal Test Template', 'test_event'):
frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_event = test_event""") frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_event = test_event""")
if frappe.db.has_column('Normal Test Template', 'test_uom'):
frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_uom = test_uom""") frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_uom = test_uom""")
if frappe.db.has_column('Descriptive Test Result', 'test_particulars'):
frappe.db.sql("""UPDATE `tabDescriptive Test Result` SET lab_test_particulars = test_particulars""") frappe.db.sql("""UPDATE `tabDescriptive Test Result` SET lab_test_particulars = test_particulars""")
frappe.db.sql("""UPDATE `tabLab Test Group Template` SET lab_test_template = test_template""")
frappe.db.sql("""UPDATE `tabLab Test Group Template` SET lab_test_description = test_description""") rename_fields = {
frappe.db.sql("""UPDATE `tabLab Test Group Template` SET lab_test_rate = test_rate""") 'lab_test_template': 'test_template',
'lab_test_description': 'test_description',
'lab_test_rate': 'test_rate'
}
for new, old in rename_fields.items():
if frappe.db.has_column('Lab Test Group Template', old):
frappe.db.sql("""UPDATE `tabLab Test Group Template` SET {} = {}"""
.format(new, old))
# rename field # rename field
frappe.reload_doc('healthcare', 'doctype', 'lab_test') frappe.reload_doc('healthcare', 'doctype', 'lab_test')

View File

@@ -0,0 +1,8 @@
import frappe
def execute():
"""Remove has_variants and attribute fields from item variant settings."""
frappe.reload_doc("stock", "doctype", "Item Variant Settings")
frappe.db.sql("""delete from `tabVariant Field`
where field_name in ('attributes', 'has_variants')""")

View File

@@ -13,12 +13,19 @@ class AdditionalSalary(Document):
if self.ref_doctype == "Employee Advance" and self.ref_docname: if self.ref_doctype == "Employee Advance" and self.ref_docname:
frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", self.amount) frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", self.amount)
self.update_employee_referral()
def on_cancel(self):
self.update_employee_referral(cancel=True)
def validate(self): def validate(self):
self.validate_dates() self.validate_dates()
self.validate_salary_structure() self.validate_salary_structure()
self.validate_recurring_additional_salary_overlap() self.validate_recurring_additional_salary_overlap()
self.validate_employee_referral()
if self.amount < 0: if self.amount < 0:
frappe.throw(_("Amount should not be less than zero.")) frappe.throw(_("Amount should not be less than zero"))
def validate_salary_structure(self): def validate_salary_structure(self):
if not frappe.db.exists('Salary Structure Assignment', {'employee': self.employee}): if not frappe.db.exists('Salary Structure Assignment', {'employee': self.employee}):
@@ -70,6 +77,27 @@ class AdditionalSalary(Document):
if self.payroll_date and getdate(self.payroll_date) > getdate(relieving_date): if self.payroll_date and getdate(self.payroll_date) > getdate(relieving_date):
frappe.throw(_("Payroll date can not be greater than employee's relieving date.")) frappe.throw(_("Payroll date can not be greater than employee's relieving date."))
def validate_employee_referral(self):
if self.ref_doctype == "Employee Referral":
referral_details = frappe.db.get_value("Employee Referral", self.ref_docname,
["is_applicable_for_referral_bonus", "status"], as_dict=1)
if not referral_details.is_applicable_for_referral_bonus:
frappe.throw(_("Employee Referral {0} is not applicable for referral bonus.").format(
self.ref_docname))
if self.type == "Deduction":
frappe.throw(_("Earning Salary Component is required for Employee Referral Bonus."))
if referral_details.status != "Accepted":
frappe.throw(_("Additional Salary for referral bonus can only be created against Employee Referral with status {0}").format(
frappe.bold("Accepted")))
def update_employee_referral(self, cancel=False):
if self.ref_doctype == "Employee Referral":
status = "Unpaid" if cancel else "Paid"
frappe.db.set_value("Employee Referral", self.ref_docname, "referral_payment_status", status)
def get_amount(self, sal_start_date, sal_end_date): def get_amount(self, sal_start_date, sal_end_date):
start_date = getdate(sal_start_date) start_date = getdate(sal_start_date)
end_date = getdate(sal_end_date) end_date = getdate(sal_end_date)
@@ -110,8 +138,7 @@ def get_additional_salaries(employee, start_date, end_date, component_type):
for d in additional_salary_list: for d in additional_salary_list:
if d.overwrite: if d.overwrite:
if d.component in components_to_overwrite: if d.component in components_to_overwrite:
frappe.throw(_("Multiple Additional Salaries with overwrite " frappe.throw(_("Multiple Additional Salaries with overwrite property exist for Salary Component {0} between {1} and {2}.").format(
"property exist for Salary Component {0} between {1} and {2}.").format(
frappe.bold(d.component), start_date, end_date), title=_("Error")) frappe.bold(d.component), start_date, end_date), title=_("Error"))
components_to_overwrite.append(d.component) components_to_overwrite.append(d.component)

View File

@@ -16,11 +16,11 @@ frappe.ui.form.on('Salary Structure', {
onload: function(frm) { onload: function(frm) {
let help_button = $(`<a class = 'control-label'> let help_button = $(`<a class = 'control-label'>
Condition and Formula Help ${__("Condition and Formula Help")}
</a>`).click(()=>{ </a>`).click(()=>{
let d = new frappe.ui.Dialog({ let d = new frappe.ui.Dialog({
title: 'Condition and Formula Help', title: __('Condition and Formula Help'),
fields: [ fields: [
{ {
fieldname: 'msg_wrapper', fieldname: 'msg_wrapper',

View File

@@ -179,9 +179,6 @@ class Project(Document):
if self.percent_complete == 100: if self.percent_complete == 100:
self.status = "Completed" self.status = "Completed"
else:
self.status = "Open"
def update_costing(self): def update_costing(self):
from_time_sheet = frappe.db.sql("""select from_time_sheet = frappe.db.sql("""select
sum(costing_amount) as costing_amount, sum(costing_amount) as costing_amount,

View File

@@ -32,7 +32,8 @@ frappe.ui.form.on("Task", {
frm.set_query("parent_task", function () { frm.set_query("parent_task", function () {
let filters = { let filters = {
"is_group": 1 "is_group": 1,
"name": ["!=", frm.doc.name]
}; };
if (frm.doc.project) filters["project"] = frm.doc.project; if (frm.doc.project) filters["project"] = frm.doc.project;
return { return {

View File

@@ -151,11 +151,11 @@ class TestTimesheet(unittest.TestCase):
settings.save() settings.save()
def make_salary_structure_for_timesheet(employee): def make_salary_structure_for_timesheet(employee, company=None):
salary_structure_name = "Timesheet Salary Structure Test" salary_structure_name = "Timesheet Salary Structure Test"
frequency = "Monthly" frequency = "Monthly"
salary_structure = make_salary_structure(salary_structure_name, frequency, dont_submit=True) salary_structure = make_salary_structure(salary_structure_name, frequency, company=company, dont_submit=True)
salary_structure.salary_component = "Timesheet Component" salary_structure.salary_component = "Timesheet Component"
salary_structure.salary_slip_based_on_timesheet = 1 salary_structure.salary_slip_based_on_timesheet = 1
salary_structure.hour_rate = 50.0 salary_structure.hour_rate = 50.0

View File

@@ -0,0 +1,48 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Employee Hours Utilization Based On Timesheet"] = {
"filters": [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1
},
{
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date",
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
reqd: 1
},
{
fieldname:"to_date",
label: __("To Date"),
fieldtype: "Date",
default: frappe.datetime.now_date(),
reqd: 1
},
{
fieldname: "employee",
label: __("Employee"),
fieldtype: "Link",
options: "Employee"
},
{
fieldname: "department",
label: __("Department"),
fieldtype: "Link",
options: "Department"
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "Link",
options: "Project"
}
]
};

View File

@@ -0,0 +1,22 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-04-05 19:23:43.838623",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-04-05 19:23:43.838623",
"modified_by": "Administrator",
"module": "Projects",
"name": "Employee Hours Utilization Based On Timesheet",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Timesheet",
"report_name": "Employee Hours Utilization Based On Timesheet",
"report_type": "Script Report",
"roles": []
}

View File

@@ -0,0 +1,280 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import flt, getdate
from six import iteritems
def execute(filters=None):
return EmployeeHoursReport(filters).run()
class EmployeeHoursReport:
'''Employee Hours Utilization Report Based On Timesheet'''
def __init__(self, filters=None):
self.filters = frappe._dict(filters or {})
self.from_date = getdate(self.filters.from_date)
self.to_date = getdate(self.filters.to_date)
self.validate_dates()
self.validate_standard_working_hours()
def validate_dates(self):
self.day_span = (self.to_date - self.from_date).days
if self.day_span <= 0:
frappe.throw(_('From Date must come before To Date'))
def validate_standard_working_hours(self):
self.standard_working_hours = frappe.db.get_single_value('HR Settings', 'standard_working_hours')
if not self.standard_working_hours:
msg = _('The metrics for this report are calculated based on the Standard Working Hours. Please set {0} in {1}.').format(
frappe.bold('Standard Working Hours'), frappe.utils.get_link_to_form('HR Settings', 'HR Settings'))
frappe.throw(msg)
def run(self):
self.generate_columns()
self.generate_data()
self.generate_report_summary()
self.generate_chart_data()
return self.columns, self.data, None, self.chart, self.report_summary
def generate_columns(self):
self.columns = [
{
'label': _('Employee'),
'options': 'Employee',
'fieldname': 'employee',
'fieldtype': 'Link',
'width': 230
},
{
'label': _('Department'),
'options': 'Department',
'fieldname': 'department',
'fieldtype': 'Link',
'width': 120
},
{
'label': _('Total Hours (T)'),
'fieldname': 'total_hours',
'fieldtype': 'Float',
'width': 120
},
{
'label': _('Billed Hours (B)'),
'fieldname': 'billed_hours',
'fieldtype': 'Float',
'width': 170
},
{
'label': _('Non-Billed Hours (NB)'),
'fieldname': 'non_billed_hours',
'fieldtype': 'Float',
'width': 170
},
{
'label': _('Untracked Hours (U)'),
'fieldname': 'untracked_hours',
'fieldtype': 'Float',
'width': 170
},
{
'label': _('% Utilization (B + NB) / T'),
'fieldname': 'per_util',
'fieldtype': 'Percentage',
'width': 200
},
{
'label': _('% Utilization (B / T)'),
'fieldname': 'per_util_billed_only',
'fieldtype': 'Percentage',
'width': 200
}
]
def generate_data(self):
self.generate_filtered_time_logs()
self.generate_stats_by_employee()
self.set_employee_department_and_name()
if self.filters.department:
self.filter_stats_by_department()
self.calculate_utilizations()
self.data = []
for emp, data in iteritems(self.stats_by_employee):
row = frappe._dict()
row['employee'] = emp
row.update(data)
self.data.append(row)
# Sort by descending order of percentage utilization
self.data.sort(key=lambda x: x['per_util'], reverse=True)
def filter_stats_by_department(self):
filtered_data = frappe._dict()
for emp, data in self.stats_by_employee.items():
if data['department'] == self.filters.department:
filtered_data[emp] = data
# Update stats
self.stats_by_employee = filtered_data
def generate_filtered_time_logs(self):
additional_filters = ''
filter_fields = ['employee', 'project', 'company']
for field in filter_fields:
if self.filters.get(field):
if field == 'project':
additional_filters += f"AND ttd.{field} = '{self.filters.get(field)}'"
else:
additional_filters += f"AND tt.{field} = '{self.filters.get(field)}'"
self.filtered_time_logs = frappe.db.sql('''
SELECT tt.employee AS employee, ttd.hours AS hours, ttd.billable AS billable, ttd.project AS project
FROM `tabTimesheet Detail` AS ttd
JOIN `tabTimesheet` AS tt
ON ttd.parent = tt.name
WHERE tt.employee IS NOT NULL
AND tt.start_date BETWEEN '{0}' AND '{1}'
AND tt.end_date BETWEEN '{0}' AND '{1}'
{2}
'''.format(self.filters.from_date, self.filters.to_date, additional_filters))
def generate_stats_by_employee(self):
self.stats_by_employee = frappe._dict()
for emp, hours, billable, project in self.filtered_time_logs:
self.stats_by_employee.setdefault(
emp, frappe._dict()
).setdefault('billed_hours', 0.0)
self.stats_by_employee[emp].setdefault('non_billed_hours', 0.0)
if billable:
self.stats_by_employee[emp]['billed_hours'] += flt(hours, 2)
else:
self.stats_by_employee[emp]['non_billed_hours'] += flt(hours, 2)
def set_employee_department_and_name(self):
for emp in self.stats_by_employee:
emp_name = frappe.db.get_value(
'Employee', emp, 'employee_name'
)
emp_dept = frappe.db.get_value(
'Employee', emp, 'department'
)
self.stats_by_employee[emp]['department'] = emp_dept
self.stats_by_employee[emp]['employee_name'] = emp_name
def calculate_utilizations(self):
TOTAL_HOURS = flt(self.standard_working_hours * self.day_span, 2)
for emp, data in iteritems(self.stats_by_employee):
data['total_hours'] = TOTAL_HOURS
data['untracked_hours'] = flt(TOTAL_HOURS - data['billed_hours'] - data['non_billed_hours'], 2)
# To handle overtime edge-case
if data['untracked_hours'] < 0:
data['untracked_hours'] = 0.0
data['per_util'] = flt(((data['billed_hours'] + data['non_billed_hours']) / TOTAL_HOURS) * 100, 2)
data['per_util_billed_only'] = flt((data['billed_hours'] / TOTAL_HOURS) * 100, 2)
def generate_report_summary(self):
self.report_summary = []
if not self.data:
return
avg_utilization = 0.0
avg_utilization_billed_only = 0.0
total_billed, total_non_billed = 0.0, 0.0
total_untracked = 0.0
for row in self.data:
avg_utilization += row['per_util']
avg_utilization_billed_only += row['per_util_billed_only']
total_billed += row['billed_hours']
total_non_billed += row['non_billed_hours']
total_untracked += row['untracked_hours']
avg_utilization /= len(self.data)
avg_utilization = flt(avg_utilization, 2)
avg_utilization_billed_only /= len(self.data)
avg_utilization_billed_only = flt(avg_utilization_billed_only, 2)
THRESHOLD_PERCENTAGE = 70.0
self.report_summary = [
{
'value': f'{avg_utilization}%',
'indicator': 'Red' if avg_utilization < THRESHOLD_PERCENTAGE else 'Green',
'label': _('Avg Utilization'),
'datatype': 'Percentage'
},
{
'value': f'{avg_utilization_billed_only}%',
'indicator': 'Red' if avg_utilization_billed_only < THRESHOLD_PERCENTAGE else 'Green',
'label': _('Avg Utilization (Billed Only)'),
'datatype': 'Percentage'
},
{
'value': total_billed,
'label': _('Total Billed Hours'),
'datatype': 'Float'
},
{
'value': total_non_billed,
'label': _('Total Non-Billed Hours'),
'datatype': 'Float'
}
]
def generate_chart_data(self):
self.chart = {}
labels = []
billed_hours = []
non_billed_hours = []
untracked_hours = []
for row in self.data:
labels.append(row.get('employee_name'))
billed_hours.append(row.get('billed_hours'))
non_billed_hours.append(row.get('non_billed_hours'))
untracked_hours.append(row.get('untracked_hours'))
self.chart = {
'data': {
'labels': labels[:30],
'datasets': [
{
'name': _('Billed Hours'),
'values': billed_hours[:30]
},
{
'name': _('Non-Billed Hours'),
'values': non_billed_hours[:30]
},
{
'name': _('Untracked Hours'),
'values': untracked_hours[:30]
}
]
},
'type': 'bar',
'barOptions': {
'stacked': True
}
}

View File

@@ -0,0 +1,198 @@
from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils.make_random import get_random
from erpnext.projects.report.employee_hours_utilization_based_on_timesheet.employee_hours_utilization_based_on_timesheet import execute
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.projects.doctype.project.test_project import make_project
class TestEmployeeUtilization(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Create test employee
cls.test_emp1 = make_employee("test1@employeeutil.com", "_Test Company")
cls.test_emp2 = make_employee("test2@employeeutil.com", "_Test Company")
# Create test project
cls.test_project = make_project({"project_name": "_Test Project"})
# Create test timesheets
cls.create_test_timesheets()
frappe.db.set_value("HR Settings", "HR Settings", "standard_working_hours", 9)
@classmethod
def create_test_timesheets(cls):
timesheet1 = frappe.new_doc("Timesheet")
timesheet1.employee = cls.test_emp1
timesheet1.company = '_Test Company'
timesheet1.append("time_logs", {
"activity_type": get_random("Activity Type"),
"hours": 5,
"billable": 1,
"from_time": '2021-04-01 13:30:00.000000',
"to_time": '2021-04-01 18:30:00.000000'
})
timesheet1.save()
timesheet1.submit()
timesheet2 = frappe.new_doc("Timesheet")
timesheet2.employee = cls.test_emp2
timesheet2.company = '_Test Company'
timesheet2.append("time_logs", {
"activity_type": get_random("Activity Type"),
"hours": 10,
"billable": 0,
"from_time": '2021-04-01 13:30:00.000000',
"to_time": '2021-04-01 23:30:00.000000',
"project": cls.test_project.name
})
timesheet2.save()
timesheet2.submit()
@classmethod
def tearDownClass(cls):
# Delete time logs
frappe.db.sql("""
DELETE FROM `tabTimesheet Detail`
WHERE parent IN (
SELECT name
FROM `tabTimesheet`
WHERE company = '_Test Company'
)
""")
frappe.db.sql("DELETE FROM `tabTimesheet` WHERE company='_Test Company'")
frappe.db.sql(f"DELETE FROM `tabProject` WHERE name='{cls.test_project.name}'")
def test_utilization_report_with_required_filters_only(self):
filters = {
"company": "_Test Company",
"from_date": "2021-04-01",
"to_date": "2021-04-03"
}
report = execute(filters)
expected_data = self.get_expected_data_for_test_employees()
self.assertEqual(report[1], expected_data)
def test_utilization_report_for_single_employee(self):
filters = {
"company": "_Test Company",
"from_date": "2021-04-01",
"to_date": "2021-04-03",
"employee": self.test_emp1
}
report = execute(filters)
emp1_data = frappe.get_doc('Employee', self.test_emp1)
expected_data = [
{
'employee': self.test_emp1,
'employee_name': 'test1@employeeutil.com',
'billed_hours': 5.0,
'non_billed_hours': 0.0,
'department': emp1_data.department,
'total_hours': 18.0,
'untracked_hours': 13.0,
'per_util': 27.78,
'per_util_billed_only': 27.78
}
]
self.assertEqual(report[1], expected_data)
def test_utilization_report_for_project(self):
filters = {
"company": "_Test Company",
"from_date": "2021-04-01",
"to_date": "2021-04-03",
"project": self.test_project.name
}
report = execute(filters)
emp2_data = frappe.get_doc('Employee', self.test_emp2)
expected_data = [
{
'employee': self.test_emp2,
'employee_name': 'test2@employeeutil.com',
'billed_hours': 0.0,
'non_billed_hours': 10.0,
'department': emp2_data.department,
'total_hours': 18.0,
'untracked_hours': 8.0,
'per_util': 55.56,
'per_util_billed_only': 0.0
}
]
self.assertEqual(report[1], expected_data)
def test_utilization_report_for_department(self):
emp1_data = frappe.get_doc('Employee', self.test_emp1)
filters = {
"company": "_Test Company",
"from_date": "2021-04-01",
"to_date": "2021-04-03",
"department": emp1_data.department
}
report = execute(filters)
expected_data = self.get_expected_data_for_test_employees()
self.assertEqual(report[1], expected_data)
def test_report_summary_data(self):
filters = {
"company": "_Test Company",
"from_date": "2021-04-01",
"to_date": "2021-04-03"
}
report = execute(filters)
summary = report[4]
expected_summary_values = ['41.67%', '13.89%', 5.0, 10.0]
self.assertEqual(len(summary), 4)
for i in range(4):
self.assertEqual(
summary[i]['value'], expected_summary_values[i]
)
def get_expected_data_for_test_employees(self):
emp1_data = frappe.get_doc('Employee', self.test_emp1)
emp2_data = frappe.get_doc('Employee', self.test_emp2)
return [
{
'employee': self.test_emp2,
'employee_name': 'test2@employeeutil.com',
'billed_hours': 0.0,
'non_billed_hours': 10.0,
'department': emp2_data.department,
'total_hours': 18.0,
'untracked_hours': 8.0,
'per_util': 55.56,
'per_util_billed_only': 0.0
},
{
'employee': self.test_emp1,
'employee_name': 'test1@employeeutil.com',
'billed_hours': 5.0,
'non_billed_hours': 0.0,
'department': emp1_data.department,
'total_hours': 18.0,
'untracked_hours': 13.0,
'per_util': 27.78,
'per_util_billed_only': 27.78
}
]

View File

@@ -0,0 +1,48 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Project Profitability"] = {
"filters": [
{
"fieldname": "company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"default": frappe.defaults.get_user_default("Company"),
"reqd": 1
},
{
"fieldname": "start_date",
"label": __("Start Date"),
"fieldtype": "Date",
"reqd": 1,
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1)
},
{
"fieldname": "end_date",
"label": __("End Date"),
"fieldtype": "Date",
"reqd": 1,
"default": frappe.datetime.now_date()
},
{
"fieldname": "customer_name",
"label": __("Customer"),
"fieldtype": "Link",
"options": "Customer"
},
{
"fieldname": "employee",
"label": __("Employee"),
"fieldtype": "Link",
"options": "Employee"
},
{
"fieldname": "project",
"label": __("Project"),
"fieldtype": "Link",
"options": "Project"
}
]
};

View File

@@ -0,0 +1,44 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-04-16 15:50:28.914872",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-04-16 15:50:48.490866",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project Profitability",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Timesheet",
"report_name": "Project Profitability",
"report_type": "Script Report",
"roles": [
{
"role": "HR User"
},
{
"role": "Accounts User"
},
{
"role": "Employee"
},
{
"role": "Projects User"
},
{
"role": "Manufacturing User"
},
{
"role": "Employee Self Service"
},
{
"role": "HR Manager"
}
]
}

View File

@@ -0,0 +1,210 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
def execute(filters=None):
columns, data = [], []
data = get_data(filters)
columns = get_columns()
charts = get_chart_data(data)
return columns, data, None, charts
def get_data(filters):
data = get_rows(filters)
data = calculate_cost_and_profit(data)
return data
def get_rows(filters):
conditions = get_conditions(filters)
standard_working_hours = frappe.db.get_single_value("HR Settings", "standard_working_hours")
if not standard_working_hours:
msg = _("The metrics for this report are calculated based on the Standard Working Hours. Please set {0} in {1}.").format(
frappe.bold("Standard Working Hours"), frappe.utils.get_link_to_form("HR Settings", "HR Settings"))
frappe.msgprint(msg)
return []
sql = """
SELECT
*
FROM
(SELECT
si.customer_name,si.base_grand_total,
si.name as voucher_no,tabTimesheet.employee,
tabTimesheet.title as employee_name,tabTimesheet.parent_project as project,
tabTimesheet.start_date,tabTimesheet.end_date,
tabTimesheet.total_billed_hours,tabTimesheet.name as timesheet,
ss.base_gross_pay,ss.total_working_days,
tabTimesheet.total_billed_hours/(ss.total_working_days * {0}) as utilization
FROM
`tabSalary Slip Timesheet` as sst join `tabTimesheet` on tabTimesheet.name = sst.time_sheet
join `tabSales Invoice Timesheet` as sit on sit.time_sheet = tabTimesheet.name
join `tabSales Invoice` as si on si.name = sit.parent and si.status != "Cancelled"
join `tabSalary Slip` as ss on ss.name = sst.parent and ss.status != "Cancelled" """.format(standard_working_hours)
if conditions:
sql += """
WHERE
{0}) as t""".format(conditions)
return frappe.db.sql(sql,filters, as_dict=True)
def calculate_cost_and_profit(data):
for row in data:
row.fractional_cost = row.base_gross_pay * row.utilization
row.profit = row.base_grand_total - row.base_gross_pay * row.utilization
return data
def get_conditions(filters):
conditions = []
if filters.get("company"):
conditions.append("tabTimesheet.company={0}".format(frappe.db.escape(filters.get("company"))))
if filters.get("start_date"):
conditions.append("tabTimesheet.start_date>='{0}'".format(filters.get("start_date")))
if filters.get("end_date"):
conditions.append("tabTimesheet.end_date<='{0}'".format(filters.get("end_date")))
if filters.get("customer_name"):
conditions.append("si.customer_name={0}".format(frappe.db.escape(filters.get("customer_name"))))
if filters.get("employee"):
conditions.append("tabTimesheet.employee={0}".format(frappe.db.escape(filters.get("employee"))))
if filters.get("project"):
conditions.append("tabTimesheet.parent_project={0}".format(frappe.db.escape(filters.get("project"))))
conditions = " and ".join(conditions)
return conditions
def get_chart_data(data):
if not data:
return None
labels = []
utilization = []
for entry in data:
labels.append(entry.get("employee_name") + " - " + str(entry.get("end_date")))
utilization.append(entry.get("utilization"))
charts = {
"data": {
"labels": labels,
"datasets": [
{
"name": "Utilization",
"values": utilization
}
]
},
"type": "bar",
"colors": ["#84BDD5"]
}
return charts
def get_columns():
return [
{
"fieldname": "customer_name",
"label": _("Customer"),
"fieldtype": "Link",
"options": "Customer",
"width": 150
},
{
"fieldname": "employee",
"label": _("Employee"),
"fieldtype": "Link",
"options": "Employee",
"width": 130
},
{
"fieldname": "employee_name",
"label": _("Employee Name"),
"fieldtype": "Data",
"width": 120
},
{
"fieldname": "voucher_no",
"label": _("Sales Invoice"),
"fieldtype": "Link",
"options": "Sales Invoice",
"width": 120
},
{
"fieldname": "timesheet",
"label": _("Timesheet"),
"fieldtype": "Link",
"options": "Timesheet",
"width": 120
},
{
"fieldname": "project",
"label": _("Project"),
"fieldtype": "Link",
"options": "Project",
"width": 100
},
{
"fieldname": "base_grand_total",
"label": _("Bill Amount"),
"fieldtype": "Currency",
"options": "currency",
"width": 100
},
{
"fieldname": "base_gross_pay",
"label": _("Cost"),
"fieldtype": "Currency",
"options": "currency",
"width": 100
},
{
"fieldname": "profit",
"label": _("Profit"),
"fieldtype": "Currency",
"options": "currency",
"width": 100
},
{
"fieldname": "utilization",
"label": _("Utilization"),
"fieldtype": "Percentage",
"width": 100
},
{
"fieldname": "fractional_cost",
"label": _("Fractional Cost"),
"fieldtype": "Int",
"width": 120
},
{
"fieldname": "total_billed_hours",
"label": _("Total Billed Hours"),
"fieldtype": "Int",
"width": 150
},
{
"fieldname": "start_date",
"label": _("Start Date"),
"fieldtype": "Date",
"width": 100
},
{
"fieldname": "end_date",
"label": _("End Date"),
"fieldtype": "Date",
"width": 100
},
{
"label": _("Currency"),
"fieldname": "currency",
"fieldtype": "Link",
"options": "Currency",
"width": 80
}
]

View File

@@ -0,0 +1,58 @@
from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils import getdate, nowdate
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.projects.doctype.timesheet.test_timesheet import make_salary_structure_for_timesheet, make_timesheet
from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_sales_invoice
from erpnext.projects.report.project_profitability.project_profitability import execute
class TestProjectProfitability(unittest.TestCase):
@classmethod
def setUp(self):
emp = make_employee('test_employee_9@salary.com', company='_Test Company')
if not frappe.db.exists('Salary Component', 'Timesheet Component'):
frappe.get_doc({'doctype': 'Salary Component', 'salary_component': 'Timesheet Component'}).insert()
make_salary_structure_for_timesheet(emp, company='_Test Company')
self.timesheet = make_timesheet(emp, simulate = True, billable=1)
self.salary_slip = make_salary_slip(self.timesheet.name)
self.salary_slip.submit()
self.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer')
self.sales_invoice.due_date = nowdate()
self.sales_invoice.submit()
frappe.db.set_value("HR Settings", "HR Settings", "standard_working_hours", 8)
def test_project_profitability(self):
filters = {
'company': '_Test Company',
'start_date': getdate(),
'end_date': getdate()
}
report = execute(filters)
row = report[1][0]
timesheet = frappe.get_doc("Timesheet", self.timesheet.name)
self.assertEqual(self.sales_invoice.customer, row.customer_name)
self.assertEqual(timesheet.title, row.employee_name)
self.assertEqual(self.sales_invoice.base_grand_total, row.base_grand_total)
self.assertEqual(self.salary_slip.base_gross_pay, row.base_gross_pay)
self.assertEqual(timesheet.total_billed_hours, row.total_billed_hours)
self.assertEqual(self.salary_slip.total_working_days, row.total_working_days)
standard_working_hours = frappe.db.get_single_value("HR Settings", "standard_working_hours")
utilization = timesheet.total_billed_hours/(self.salary_slip.total_working_days * standard_working_hours)
self.assertEqual(utilization, row.utilization)
profit = self.sales_invoice.base_grand_total - self.salary_slip.base_gross_pay * utilization
self.assertEqual(profit, row.profit)
fractional_cost = self.salary_slip.base_gross_pay * utilization
self.assertEqual(fractional_cost, row.fractional_cost)
def tearDown(self):
frappe.get_doc("Sales Invoice", self.sales_invoice.name).cancel()
frappe.get_doc("Salary Slip", self.salary_slip.name).cancel()
frappe.get_doc("Timesheet", self.timesheet.name).cancel()

View File

@@ -131,25 +131,25 @@ def get_report_summary(data):
{ {
"value": avg_completion, "value": avg_completion,
"indicator": "Green" if avg_completion > 50 else "Red", "indicator": "Green" if avg_completion > 50 else "Red",
"label": "Average Completion", "label": _("Average Completion"),
"datatype": "Percent", "datatype": "Percent",
}, },
{ {
"value": total, "value": total,
"indicator": "Blue", "indicator": "Blue",
"label": "Total Tasks", "label": _("Total Tasks"),
"datatype": "Int", "datatype": "Int",
}, },
{ {
"value": completed, "value": completed,
"indicator": "Green", "indicator": "Green",
"label": "Completed Tasks", "label": _("Completed Tasks"),
"datatype": "Int", "datatype": "Int",
}, },
{ {
"value": total_overdue, "value": total_overdue,
"indicator": "Green" if total_overdue == 0 else "Red", "indicator": "Green" if total_overdue == 0 else "Red",
"label": "Overdue Tasks", "label": _("Overdue Tasks"),
"datatype": "Int", "datatype": "Int",
} }
] ]

View File

@@ -130,6 +130,26 @@
"onboard": 1, "onboard": 1,
"type": "Link" "type": "Link"
}, },
{
"dependencies": "Timesheet",
"hidden": 0,
"is_query_report": 1,
"label": "Employee Hours Utilization",
"link_to": "Employee Hours Utilization Based On Timesheet",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Timesheet, Sales Invoice, Salary Slip",
"hidden": 0,
"is_query_report": 1,
"label": "Project Profitability",
"link_to": "Project Profitability",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{ {
"dependencies": "Project", "dependencies": "Project",
"hidden": 0, "hidden": 0,
@@ -161,7 +181,7 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2021-03-26 16:32:00.628561", "modified": "2021-04-25 16:27:16.548780",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Projects", "module": "Projects",
"name": "Projects", "name": "Projects",

View File

@@ -640,6 +640,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
let key = item.name; let key = item.name;
me.apply_rule_on_other_items({key: item}); me.apply_rule_on_other_items({key: item});
} }
},
() => {
var company_currency = me.get_company_currency();
me.update_item_grid_labels(company_currency);
} }
]); ]);
} }
@@ -1321,11 +1325,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
change_grid_labels: function(company_currency) { change_grid_labels: function(company_currency) {
var me = this; var me = this;
this.frm.set_currency_labels(["base_rate", "base_net_rate", "base_price_list_rate", "base_amount", "base_net_amount", "base_rate_with_margin"], this.update_item_grid_labels(company_currency);
company_currency, "items");
this.frm.set_currency_labels(["rate", "net_rate", "price_list_rate", "amount", "net_amount", "stock_uom_rate", "rate_with_margin"], this.toggle_item_grid_columns(company_currency);
this.frm.doc.currency, "items");
if(this.frm.fields_dict["operations"]) { if(this.frm.fields_dict["operations"]) {
this.frm.set_currency_labels(["operating_cost", "hour_rate"], this.frm.doc.currency, "operations"); this.frm.set_currency_labels(["operating_cost", "hour_rate"], this.frm.doc.currency, "operations");
@@ -1360,6 +1362,39 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
this.frm.doc.party_account_currency, "advances"); this.frm.doc.party_account_currency, "advances");
} }
this.update_payment_schedule_grid_labels(company_currency);
},
update_item_grid_labels: function(company_currency) {
this.frm.set_currency_labels([
"base_rate", "base_net_rate", "base_price_list_rate",
"base_amount", "base_net_amount", "base_rate_with_margin"
], company_currency, "items");
this.frm.set_currency_labels([
"rate", "net_rate", "price_list_rate", "amount",
"net_amount", "stock_uom_rate", "rate_with_margin"
], this.frm.doc.currency, "items");
},
update_payment_schedule_grid_labels: function(company_currency) {
const me = this;
if (this.frm.fields_dict["payment_schedule"]) {
this.frm.set_currency_labels(["base_payment_amount", "base_outstanding", "base_paid_amount"],
company_currency, "payment_schedule");
this.frm.set_currency_labels(["payment_amount", "outstanding", "paid_amount"],
this.frm.doc.currency, "payment_schedule");
var schedule_grid = this.frm.fields_dict["payment_schedule"].grid;
$.each(["base_payment_amount", "base_outstanding", "base_paid_amount"], function(i, fname) {
if (frappe.meta.get_docfield(schedule_grid.doctype, fname))
schedule_grid.set_column_disp(fname, me.frm.doc.currency != company_currency);
});
}
},
toggle_item_grid_columns: function(company_currency) {
const me = this;
// toggle columns // toggle columns
var item_grid = this.frm.fields_dict["items"].grid; var item_grid = this.frm.fields_dict["items"].grid;
$.each(["base_rate", "base_price_list_rate", "base_amount", "base_rate_with_margin"], function(i, fname) { $.each(["base_rate", "base_price_list_rate", "base_amount", "base_rate_with_margin"], function(i, fname) {
@@ -1379,9 +1414,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
if(frappe.meta.get_docfield(item_grid.doctype, fname)) if(frappe.meta.get_docfield(item_grid.doctype, fname))
item_grid.set_column_disp(fname, (show && (me.frm.doc.currency != company_currency))); item_grid.set_column_disp(fname, (show && (me.frm.doc.currency != company_currency)));
}); });
// set labels
var $wrapper = $(this.frm.wrapper);
}, },
recalculate: function() { recalculate: function() {
@@ -1995,11 +2027,14 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
terms_template: doc.payment_terms_template, terms_template: doc.payment_terms_template,
posting_date: posting_date, posting_date: posting_date,
grand_total: doc.rounded_total || doc.grand_total, grand_total: doc.rounded_total || doc.grand_total,
base_grand_total: doc.base_rounded_total || doc.base_grand_total,
bill_date: doc.bill_date bill_date: doc.bill_date
}, },
callback: function(r) { callback: function(r) {
if(r.message && !r.exc) { if(r.message && !r.exc) {
me.frm.set_value("payment_schedule", r.message); me.frm.set_value("payment_schedule", r.message);
const company_currency = me.get_company_currency();
this.update_payment_schedule_grid_labels(company_currency);
} }
} }
}) })
@@ -2007,6 +2042,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
}, },
payment_term: function(doc, cdt, cdn) { payment_term: function(doc, cdt, cdn) {
const me = this;
var row = locals[cdt][cdn]; var row = locals[cdt][cdn];
if(row.payment_term) { if(row.payment_term) {
frappe.call({ frappe.call({
@@ -2015,12 +2051,15 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
term: row.payment_term, term: row.payment_term,
bill_date: this.frm.doc.bill_date, bill_date: this.frm.doc.bill_date,
posting_date: this.frm.doc.posting_date || this.frm.doc.transaction_date, posting_date: this.frm.doc.posting_date || this.frm.doc.transaction_date,
grand_total: this.frm.doc.rounded_total || this.frm.doc.grand_total grand_total: this.frm.doc.rounded_total || this.frm.doc.grand_total,
base_grand_total: this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total
}, },
callback: function(r) { callback: function(r) {
if(r.message && !r.exc) { if(r.message && !r.exc) {
for (var d in r.message) { for (var d in r.message) {
frappe.model.set_value(cdt, cdn, d, r.message[d]); frappe.model.set_value(cdt, cdn, d, r.message[d]);
const company_currency = me.get_company_currency();
me.update_payment_schedule_grid_labels(company_currency);
} }
} }
} }

View File

@@ -712,7 +712,7 @@ erpnext.utils.map_current_doc = function(opts) {
} }
frappe.form.link_formatters['Item'] = function(value, doc) { frappe.form.link_formatters['Item'] = function(value, doc) {
if (doc && value && doc.item_name && doc.item_name !== value) { if (doc && value && doc.item_name && doc.item_name !== value && doc.item_code === value) {
return value + ': ' + doc.item_name; return value + ': ' + doc.item_name;
} else if (!value && doc.doctype && doc.item_name) { } else if (!value && doc.doctype && doc.item_name) {
// format blank value in child table // format blank value in child table

View File

@@ -1,4 +1,5 @@
{ {
"actions": [],
"creation": "2019-10-15 12:33:21.845329", "creation": "2019-10-15 12:33:21.845329",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@@ -86,12 +87,14 @@
"reqd": 1 "reqd": 1
}, },
{ {
"depends_on": "eval:!doc.__islocal",
"fieldname": "upload_xml_invoices_section", "fieldname": "upload_xml_invoices_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Upload XML Invoices" "label": "Upload XML Invoices"
} }
], ],
"modified": "2020-05-25 21:32:49.064579", "links": [],
"modified": "2021-04-24 10:33:12.250687",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Regional", "module": "Regional",
"name": "Import Supplier Invoice", "name": "Import Supplier Invoice",

View File

@@ -28,14 +28,19 @@ class ImportSupplierInvoice(Document):
self.name = "Import Invoice on " + format_datetime(self.creation) self.name = "Import Invoice on " + format_datetime(self.creation)
def import_xml_data(self): def import_xml_data(self):
import_file = frappe.get_doc("File", {"file_url": self.zip_file}) zip_file = frappe.get_doc("File", {
"file_url": self.zip_file,
"attached_to_doctype": self.doctype,
"attached_to_name": self.name
})
self.publish("File Import", _("Processing XML Files"), 1, 3) self.publish("File Import", _("Processing XML Files"), 1, 3)
self.file_count = 0 self.file_count = 0
self.purchase_invoices_count = 0 self.purchase_invoices_count = 0
self.default_uom = frappe.db.get_value("Stock Settings", fieldname="stock_uom") self.default_uom = frappe.db.get_value("Stock Settings", fieldname="stock_uom")
with zipfile.ZipFile(get_full_path(self.zip_file)) as zf: with zipfile.ZipFile(zip_file.get_full_path()) as zf:
for file_name in zf.namelist(): for file_name in zf.namelist():
content = get_file_content(file_name, zf) content = get_file_content(file_name, zf)
file_content = bs(content, "xml") file_content = bs(content, "xml")
@@ -124,9 +129,9 @@ class ImportSupplierInvoice(Document):
if disc_line.find("Percentuale"): if disc_line.find("Percentuale"):
invoices_args["total_discount"] += flt((flt(disc_line.Percentuale.text) / 100) * (rate * qty)) invoices_args["total_discount"] += flt((flt(disc_line.Percentuale.text) / 100) * (rate * qty))
@frappe.whitelist()
def process_file_data(self): def process_file_data(self):
self.status = "Processing File Data" self.db_set("status", "Processing File Data", notify=True, commit=True)
self.save()
frappe.enqueue_doc(self.doctype, self.name, "import_xml_data", queue="long", timeout=3600) frappe.enqueue_doc(self.doctype, self.name, "import_xml_data", queue="long", timeout=3600)
def publish(self, title, message, count, total): def publish(self, title, message, count, total):
@@ -380,24 +385,3 @@ def create_uom(uom):
new_uom.uom_name = uom new_uom.uom_name = uom
new_uom.save() new_uom.save()
return new_uom.uom_name return new_uom.uom_name
def get_full_path(file_name):
"""Returns file path from given file name"""
file_path = file_name
if "/" not in file_path:
file_path = "/files/" + file_path
if file_path.startswith("/private/files/"):
file_path = get_files_path(*file_path.split("/private/files/", 1)[1].split("/"), is_private=1)
elif file_path.startswith("/files/"):
file_path = get_files_path(*file_path.split("/files/", 1)[1].split("/"))
elif file_path.startswith("http"):
pass
elif not self.file_url:
frappe.throw(_("There is some problem with the file url: {0}").format(file_path))
return file_path

View File

@@ -115,17 +115,19 @@ erpnext.setup_einvoice_actions = (doctype) => {
message += '<br><br>'; message += '<br><br>';
message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.'); message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
frappe.msgprint({ const dialog = frappe.msgprint({
title: __('Update E-Way Bill Cancelled Status?'), title: __('Update E-Way Bill Cancelled Status?'),
message: message, message: message,
indicator: 'orange', indicator: 'orange',
primary_action: function() { primary_action: {
action: function() {
frappe.call({ frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
args: { doctype, docname: name }, args: { doctype, docname: name },
freeze: true, freeze: true,
callback: () => frm.reload_doc() callback: () => frm.reload_doc() || dialog.hide()
}); });
}
}, },
primary_action_label: __('Yes') primary_action_label: __('Yes')
}); });

View File

@@ -340,8 +340,6 @@ def get_eway_bill_details(invoice):
frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes. Please clear fields in the Transporter Section of the invoice.'), frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes. Please clear fields in the Transporter Section of the invoice.'),
title=_('Invalid Fields')) title=_('Invalid Fields'))
if not invoice.distance:
frappe.throw(_('Distance is mandatory for generating e-way bill for an e-invoice.'), title=_('Missing Field'))
mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' } mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' }
vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' } vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' }
@@ -450,7 +448,7 @@ def make_einvoice(invoice):
if invoice.is_return: if invoice.is_return:
prev_doc_details = get_return_doc_reference(invoice) prev_doc_details = get_return_doc_reference(invoice)
if invoice.transporter and flt(invoice.distance) and not invoice.is_return: if invoice.transporter and not invoice.is_return:
eway_bill_details = get_eway_bill_details(invoice) eway_bill_details = get_eway_bill_details(invoice)
# not yet implemented # not yet implemented
@@ -1027,12 +1025,12 @@ def generate_eway_bill(doctype, docname, **kwargs):
gsp_connector.generate_eway_bill(**kwargs) gsp_connector.generate_eway_bill(**kwargs)
@frappe.whitelist() @frappe.whitelist()
def cancel_eway_bill(doctype, docname, eway_bill, reason, remark): def cancel_eway_bill(doctype, docname):
# TODO: uncomment when eway_bill api from Adequare is enabled # TODO: uncomment when eway_bill api from Adequare is enabled
# gsp_connector = GSPConnector(doctype, docname) # gsp_connector = GSPConnector(doctype, docname)
# gsp_connector.cancel_eway_bill(eway_bill, reason, remark) # gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
# update cancelled status only, to be able to cancel irn next frappe.db.set_value(doctype, docname, 'ewaybill', '')
frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 1) frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 1)
@frappe.whitelist() @frappe.whitelist()

View File

@@ -199,7 +199,7 @@ class Gstr1Report(object):
self.item_tax_rate = frappe._dict() self.item_tax_rate = frappe._dict()
items = frappe.db.sql(""" items = frappe.db.sql("""
select item_code, parent, taxable_value, item_tax_rate select item_code, parent, taxable_value, base_net_amount, item_tax_rate
from `tab%s Item` from `tab%s Item`
where parent in (%s) where parent in (%s)
""" % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1) """ % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1)
@@ -207,7 +207,7 @@ class Gstr1Report(object):
for d in items: for d in items:
if d.item_code not in self.invoice_items.get(d.parent, {}): if d.item_code not in self.invoice_items.get(d.parent, {}):
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code,
sum(i.get('taxable_value', 0) for i in items sum((i.get('taxable_value', 0) or i.get('base_net_amount', 0)) for i in items
if i.item_code == d.item_code and i.parent == d.parent)) if i.item_code == d.item_code and i.parent == d.parent))
item_tax_rate = {} item_tax_rate = {}

View File

@@ -12,6 +12,7 @@ from frappe.model.document import Document
from frappe.core.doctype.sms_settings.sms_settings import send_sms from frappe.core.doctype.sms_settings.sms_settings import send_sms
class SMSCenter(Document): class SMSCenter(Document):
@frappe.whitelist()
def create_receiver_list(self): def create_receiver_list(self):
rec, where_clause = '', '' rec, where_clause = '', ''
if self.send_to == 'All Customer Contact': if self.send_to == 'All Customer Contact':
@@ -73,6 +74,7 @@ class SMSCenter(Document):
return receiver_nos return receiver_nos
@frappe.whitelist()
def send_sms(self): def send_sms(self):
receiver_list = [] receiver_list = []
if not self.message: if not self.message:

View File

@@ -139,8 +139,24 @@ def get_conditions(item_code, serial_no, batch_no, barcode):
if serial_no or batch_no or barcode: if serial_no or batch_no or barcode:
return "item.name = {0}".format(frappe.db.escape(item_code)) return "item.name = {0}".format(frappe.db.escape(item_code))
return """(item.name like {item_code} return make_condition(item_code)
or item.item_name like {item_code})""".format(item_code = frappe.db.escape('%' + item_code + '%'))
def make_condition(item_code):
condition = "("
condition += """item.name like {item_code}
or item.item_name like {item_code}""".format(item_code = frappe.db.escape('%' + item_code + '%'))
condition += add_search_fields_condition(item_code)
condition += ")"
return condition
def add_search_fields_condition(item_code):
condition = ''
search_fields = frappe.get_all('POS Search Fields', fields = ['fieldname'])
if search_fields:
for field in search_fields:
condition += " or item.{0} like {1}".format(field['fieldname'], frappe.db.escape('%' + item_code + '%'))
return condition
def get_item_group_condition(pos_profile): def get_item_group_condition(pos_profile):
cond = "and 1=1" cond = "and 1=1"

View File

@@ -168,6 +168,7 @@ erpnext.PointOfSale.ItemSelector = class {
case (iCode >= 160 && iCode <= 164) || iCode == 170: // ^ ! # $ * case (iCode >= 160 && iCode <= 164) || iCode == 170: // ^ ! # $ *
case iCode >= 186 && iCode <= 194: // (; = , - . / `) case iCode >= 186 && iCode <= 194: // (; = , - . / `)
case iCode >= 219 && iCode <= 222: // ([ \ ] ') case iCode >= 219 && iCode <= 222: // ([ \ ] ')
case iCode == 32: // spacebar
if (oEvent.key !== undefined && oEvent.key !== '') { if (oEvent.key !== undefined && oEvent.key !== '') {
return oEvent.key; return oEvent.key;
} }

View File

@@ -105,7 +105,7 @@ erpnext.PointOfSale.PastOrderList = class {
<svg class="mr-2" width="12" height="12" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"> <svg class="mr-2" width="12" height="12" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/> <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
</svg> </svg>
${invoice.customer} ${frappe.ellipsis(invoice.customer, 20)}
</div> </div>
</div> </div>
<div class="invoice-total-status"> <div class="invoice-total-status">

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,5 @@
{ {
"absolute_value": 0,
"align_labels_right": 0, "align_labels_right": 0,
"creation": "2011-12-21 11:08:55", "creation": "2011-12-21 11:08:55",
"custom_format": 1, "custom_format": 1,
@@ -6,10 +7,10 @@
"doc_type": "POS Invoice", "doc_type": "POS Invoice",
"docstatus": 0, "docstatus": 0,
"doctype": "Print Format", "doctype": "Print Format",
"html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tfont-family: Tahoma, sans-serif;\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n<p class=\"text-center\" style=\"margin-bottom: 1rem\">\n\t{{ doc.company }}<br>\n\t{{ doc.select_print_heading or _(\"Invoice\") }}<br>\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t<b>{{ _(\"Customer\") }}:</b> {{ doc.customer_name }}\n</p>\n\n<hr>\n<table class=\"table table-condensed cart no-border\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"50%\">{{ _(\"Item\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"SR.No\") }}:</b><br>\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}<br>@ {{ item.get_formatted(\"rate\") }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% else %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% endif %}\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t {%- if not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t {% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t {%- endif -%}\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.change_amount -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t{%- endif -%}\n\t</tbody>\n</table>\n<hr>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>", "html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n<p class=\"text-center\" style=\"margin-bottom: 1rem\">\n\t{{ doc.company }}<br>\n\t<b>{{ doc.select_print_heading or _(\"Invoice\") }}</b><br>\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Cashier\") }}:</b> {{ doc.owner }}<br>\n\t<b>{{ _(\"Customer\") }}:</b> {{ doc.customer_name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t<b>{{ _(\"Time\") }}:</b> {{ doc.get_formatted(\"posting_time\") }}<br>\n</p>\n\n<hr>\n<table class=\"table table-condensed\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"50%\">{{ _(\"Item\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"SR.No\") }}:</b><br>\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}<br>@ {{ item.get_formatted(\"rate\") }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% else %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% endif %}\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t {%- if not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t {% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t {%- endif -%}\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t{%- for row in doc.payments -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t {{ row.mode_of_payment }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t{%- endfor -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.change_amount -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t{%- endif -%}\n\t</tbody>\n</table>\n<hr>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
"idx": 1, "idx": 1,
"line_breaks": 0, "line_breaks": 0,
"modified": "2020-04-29 16:45:58.942375", "modified": "2021-04-15 15:23:28.867135",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "POS Invoice", "name": "POS Invoice",

View File

@@ -159,6 +159,7 @@ class NamingSeries(Document):
if frappe.db.get_value('Series', series, 'name', order_by="name") == None: if frappe.db.get_value('Series', series, 'name', order_by="name") == None:
frappe.db.sql("insert into tabSeries (name, current) values (%s, 0)", (series)) frappe.db.sql("insert into tabSeries (name, current) values (%s, 0)", (series))
@frappe.whitelist()
def update_series_start(self): def update_series_start(self):
if self.prefix: if self.prefix:
prefix = self.parse_naming_series() prefix = self.parse_naming_series()

View File

@@ -46,9 +46,6 @@ frappe.ui.form.on("Item", {
}, __("View")); }, __("View"));
} }
if (!frm.doc.is_fixed_asset) {
erpnext.item.make_dashboard(frm);
}
if (frm.doc.is_fixed_asset) { if (frm.doc.is_fixed_asset) {
frm.trigger('is_fixed_asset'); frm.trigger('is_fixed_asset');
@@ -97,6 +94,10 @@ frappe.ui.form.on("Item", {
erpnext.item.edit_prices_button(frm); erpnext.item.edit_prices_button(frm);
erpnext.item.toggle_attributes(frm); erpnext.item.toggle_attributes(frm);
if (!frm.doc.is_fixed_asset) {
erpnext.item.make_dashboard(frm);
}
frm.add_custom_button(__('Duplicate'), function() { frm.add_custom_button(__('Duplicate'), function() {
var new_item = frappe.model.copy_doc(frm.doc); var new_item = frappe.model.copy_doc(frm.doc);
if(new_item.item_name===new_item.item_code) { if(new_item.item_name===new_item.item_code) {
@@ -473,11 +474,15 @@ $.extend(erpnext.item, {
me.multiple_variant_dialog.get_primary_btn().html(__('Create Variants')); me.multiple_variant_dialog.get_primary_btn().html(__('Create Variants'));
me.multiple_variant_dialog.disable_primary_action(); me.multiple_variant_dialog.disable_primary_action();
} else { } else {
let no_of_combinations = lengths.reduce((a, b) => a * b, 1); let no_of_combinations = lengths.reduce((a, b) => a * b, 1);
me.multiple_variant_dialog.get_primary_btn() let msg;
.html(__( if (no_of_combinations === 1) {
`Make ${no_of_combinations} Variant${no_of_combinations === 1 ? '' : 's'}` msg = __("Make {0} Variant", [no_of_combinations]);
)); } else {
msg = __("Make {0} Variants", [no_of_combinations]);
}
me.multiple_variant_dialog.get_primary_btn().html(msg);
me.multiple_variant_dialog.enable_primary_action(); me.multiple_variant_dialog.enable_primary_action();
} }
} }

View File

@@ -63,7 +63,7 @@ class Item(WebsiteGenerator):
if self.variant_of: if self.variant_of:
if not self.item_code: if not self.item_code:
template_item_name = frappe.db.get_value("Item", self.variant_of, "item_name") template_item_name = frappe.db.get_value("Item", self.variant_of, "item_name")
self.item_code = make_variant_item_code(self.variant_of, template_item_name, self) make_variant_item_code(self.variant_of, template_item_name, self)
else: else:
from frappe.model.naming import set_name_by_naming_series from frappe.model.naming import set_name_by_naming_series
set_name_by_naming_series(self) set_name_by_naming_series(self)

View File

@@ -13,10 +13,11 @@ class ItemVariantSettings(Document):
def set_default_fields(self): def set_default_fields(self):
self.fields = [] self.fields = []
fields = frappe.get_meta('Item').fields fields = frappe.get_meta('Item').fields
exclude_fields = ["naming_series", "item_code", "item_name", "show_in_website", exclude_fields = {"naming_series", "item_code", "item_name", "show_in_website",
"show_variant_in_website", "standard_rate", "opening_stock", "image", "description", "show_variant_in_website", "standard_rate", "opening_stock", "image", "description",
"variant_of", "valuation_rate", "description", "barcodes", "variant_of", "valuation_rate", "description", "barcodes",
"website_image", "thumbnail", "website_specifiations", "web_long_description"] "website_image", "thumbnail", "website_specifiations", "web_long_description",
"has_variants", "attributes"}
for d in fields: for d in fields:
if not d.no_copy and d.fieldname not in exclude_fields and \ if not d.no_copy and d.fieldname not in exclude_fields and \

View File

@@ -73,6 +73,34 @@ frappe.ui.form.on("Purchase Receipt", {
}) })
}, __('Create')); }, __('Create'));
} }
frm.events.add_custom_buttons(frm);
},
add_custom_buttons: function(frm) {
if (frm.doc.docstatus == 0) {
frm.add_custom_button(__('Purchase Invoice'), function () {
if (!frm.doc.supplier) {
frappe.throw({
title: __("Mandatory"),
message: __("Please Select a Supplier")
});
}
erpnext.utils.map_current_doc({
method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_purchase_receipt",
source_doctype: "Purchase Invoice",
target: frm,
setters: {
supplier: frm.doc.supplier,
},
get_query_filters: {
docstatus: 1,
per_received: ["<", 100],
company: frm.doc.company
}
})
}, __("Get Items From"));
}
}, },
company: function(frm) { company: function(frm) {

View File

@@ -53,7 +53,20 @@ class PurchaseReceipt(BuyingController):
'target_ref_field': 'stock_qty', 'target_ref_field': 'stock_qty',
'source_field': 'stock_qty', 'source_field': 'stock_qty',
'percent_join_field': 'material_request' 'percent_join_field': 'material_request'
},
{
'source_dt': 'Purchase Receipt Item',
'target_dt': 'Purchase Invoice Item',
'join_field': 'purchase_invoice_item',
'target_field': 'received_qty',
'target_parent_dt': 'Purchase Invoice',
'target_parent_field': 'per_received',
'target_ref_field': 'qty',
'source_field': 'received_qty',
'percent_join_field': 'purchase_invoice',
'overflow_type': 'receipt'
}] }]
if cint(self.is_return): if cint(self.is_return):
self.status_updater.extend([ self.status_updater.extend([
{ {
@@ -221,6 +234,7 @@ class PurchaseReceipt(BuyingController):
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
self.delete_auto_created_batches() self.delete_auto_created_batches()
@frappe.whitelist()
def get_current_stock(self): def get_current_stock(self):
for d in self.get('supplied_items'): for d in self.get('supplied_items'):
if self.supplier_warehouse: if self.supplier_warehouse:
@@ -513,7 +527,9 @@ class PurchaseReceipt(BuyingController):
def update_billing_status(self, update_modified=True): def update_billing_status(self, update_modified=True):
updated_pr = [self.name] updated_pr = [self.name]
for d in self.get("items"): for d in self.get("items"):
if d.purchase_order_item: if d.purchase_invoice and d.purchase_invoice_item:
d.db_set('billed_amt', d.amount, update_modified=update_modified)
elif d.purchase_order_item:
updated_pr += update_billed_amount_based_on_po(d.purchase_order_item, update_modified) updated_pr += update_billed_amount_based_on_po(d.purchase_order_item, update_modified)
for pr in set(updated_pr): for pr in set(updated_pr):

View File

@@ -72,16 +72,18 @@
"warehouse", "warehouse",
"rejected_warehouse", "rejected_warehouse",
"from_warehouse", "from_warehouse",
"purchase_order",
"material_request", "material_request",
"purchase_order",
"purchase_invoice",
"column_break_40", "column_break_40",
"is_fixed_asset", "is_fixed_asset",
"asset_location", "asset_location",
"asset_category", "asset_category",
"schedule_date", "schedule_date",
"quality_inspection", "quality_inspection",
"purchase_order_item",
"material_request_item", "material_request_item",
"purchase_order_item",
"purchase_invoice_item",
"purchase_receipt_item", "purchase_receipt_item",
"delivery_note_item", "delivery_note_item",
"putaway_rule", "putaway_rule",
@@ -937,7 +939,21 @@
"fieldname": "base_rate_with_margin", "fieldname": "base_rate_with_margin",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rate With Margin (Company Currency)", "label": "Rate With Margin (Company Currency)",
"options": "Company:company:default_currency", "options": "Company:company:default_currency"
},
{
"fieldname": "purchase_invoice",
"fieldtype": "Link",
"label": "Purchase Invoice",
"options": "Purchase Invoice",
"read_only": 1
},
{
"fieldname": "purchase_invoice_item",
"fieldtype": "Data",
"hidden": 1,
"label": "Purchase Invoice Item",
"no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
} }
@@ -945,7 +961,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-02-23 00:59:14.360847", "modified": "2021-03-29 04:17:00.336298",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt Item", "name": "Purchase Receipt Item",

View File

@@ -398,8 +398,12 @@ class StockEntry(StockController):
and item_code = %s and item_code = %s
and ifnull(s_warehouse,'')='' """ % (", ".join(["%s" * len(other_ste)]), "%s"), args)[0][0] and ifnull(s_warehouse,'')='' """ % (", ".join(["%s" * len(other_ste)]), "%s"), args)[0][0]
if fg_qty_already_entered and fg_qty_already_entered >= qty: if fg_qty_already_entered and fg_qty_already_entered >= qty:
frappe.throw(_("Stock Entries already created for Work Order ") frappe.throw(
+ self.work_order + ":" + ", ".join(other_ste), DuplicateEntryForWorkOrderError) _("Stock Entries already created for Work Order {0}: {1}").format(
self.work_order, ", ".join(other_ste)
),
DuplicateEntryForWorkOrderError,
)
def set_actual_qty(self): def set_actual_qty(self):
allow_negative_stock = cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock")) allow_negative_stock = cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock"))
@@ -435,6 +439,7 @@ class StockEntry(StockController):
if transferred_serial_no: if transferred_serial_no:
d.serial_no = transferred_serial_no d.serial_no = transferred_serial_no
@frappe.whitelist()
def get_stock_and_rate(self): def get_stock_and_rate(self):
""" """
Updates rate and availability of all the items. Updates rate and availability of all the items.

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