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
commit 0ae702aab9
107 changed files with 5163 additions and 1556 deletions

View File

@ -80,14 +80,29 @@ jobs:
env:
TYPE: ${{ matrix.TYPE }}
- name: Coverage
if: matrix.TYPE == 'server'
- name: Coverage - Pull Request
if: matrix.TYPE == 'server' && github.event_name == 'pull_request'
run: |
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
cd ${GITHUB_WORKSPACE}
pip install coveralls==3.0.1
pip install coverage==5.5
pip install coveralls==2.2.0
pip install coverage==4.5.4
coveralls --service=github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_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
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 frappe.utils import getdate
__version__ = '13.0.0-dev'
__version__ = '13.2.0'
def get_default_company(user=None):
'''Get default company for user'''

View File

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

View File

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

View File

@ -21,21 +21,17 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
refresh: function(frm) {
if(frm.doc.docstatus==1) {
frappe.db.get_value("Journal Entry Account", {
'reference_type': 'Exchange Rate Revaluation',
'reference_name': frm.doc.name,
'docstatus': 1
}, "sum(debit) as sum", (r) =>{
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() {
return frm.events.make_jv(frm);
}, __('Create'));
frappe.call({
method: 'check_journal_entry_condition',
doc: frm.doc,
callback: function(r) {
if (r.message) {
frm.add_custom_button(__('Journal Entry'), function() {
return frm.events.make_jv(frm);
}, __('Create'));
}
}
}, 'Journal Entry');
});
}
},

View File

@ -27,6 +27,23 @@ class ExchangeRateRevaluation(Document):
if not (self.company and self.posting_date):
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()
def get_accounts_data(self, account=None):
accounts = []

View File

@ -592,6 +592,7 @@ class JournalEntry(AccountsController):
self.validate_total_debit_and_credit()
@frappe.whitelist()
def get_outstanding_invoices(self):
self.set('accounts', [])
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));
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
frm.events.set_unallocated_amount(frm);
@ -582,7 +582,7 @@ frappe.ui.form.on('Payment Entry', {
}
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
frm.events.set_unallocated_amount(frm);
},
@ -606,9 +606,9 @@ frappe.ui.form.on('Payment Entry', {
{fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"},
{fieldtype:"Section Break"},
{fieldtype:"Link", label:__("Cost Center"), fieldname:"cost_center", options:"Cost Center",
"get_query": function() {
return {
"filters": {"company": frm.doc.company}
"get_query": function() {
return {
"filters": {"company": frm.doc.company}
}
}
},
@ -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_negative_outstanding = 0;
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
row.allocated_amount = 0;
} else if (frappe.flags.allocate_payment_amount != 0 && !row.allocated_amount) {
if (row.outstanding_amount > 0 && allocated_positive_outstanding > 0) {
if (row.outstanding_amount >= allocated_positive_outstanding) {
row.allocated_amount = allocated_positive_outstanding;
} else {
row.allocated_amount = row.outstanding_amount;
}
} else if (frappe.flags.allocate_payment_amount != 0 && (!row.allocated_amount || paid_amount_change)) {
if (row.outstanding_amount > 0 && allocated_positive_outstanding >= 0) {
row.allocated_amount = (row.outstanding_amount >= allocated_positive_outstanding) ?
allocated_positive_outstanding : row.outstanding_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));
}
}

View File

@ -234,7 +234,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
});
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")
);

View File

@ -20,10 +20,11 @@
"discount",
"section_break_9",
"payment_amount",
"outstanding",
"paid_amount",
"discounted_amount",
"column_break_3",
"outstanding",
"paid_amount"
"base_payment_amount"
],
"fields": [
{
@ -78,7 +79,8 @@
"depends_on": "paid_amount",
"fieldname": "paid_amount",
"fieldtype": "Currency",
"label": "Paid Amount"
"label": "Paid Amount",
"options": "currency"
},
{
"fieldname": "column_break_3",
@ -97,6 +99,7 @@
"fieldname": "outstanding",
"fieldtype": "Currency",
"label": "Outstanding",
"options": "currency",
"read_only": 1
},
{
@ -145,12 +148,18 @@
{
"fieldname": "section_break_4",
"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,
"istable": 1,
"links": [],
"modified": "2021-02-15 21:03:12.540546",
"modified": "2021-04-28 05:41:35.084233",
"modified_by": "Administrator",
"module": "Accounts",
"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":
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
self.validate_pos_closing()
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):
invalid_rows = []
for d in self.pos_transactions:

View File

@ -96,30 +96,45 @@ class POSInvoice(SalesInvoice):
if paid_amt and pay.amount != paid_amt:
return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment))
def validate_pos_reserved_serial_nos(self, item):
serial_nos = get_serial_nos(item.serial_no)
filters = {"item_code": item.item_code, "warehouse": item.warehouse}
if item.batch_no:
filters["batch_no"] = item.batch_no
reserved_serial_nos = get_pos_reserved_serial_nos(filters)
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))
if len(invalid_serial_nos) == 1:
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
elif invalid_serial_nos:
frappe.throw(_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.")
.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_value('Stock Settings', None, 'allow_negative_stock')
error_msg = []
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
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)
serial_nos = get_serial_nos(d.serial_no)
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))
if len(invalid_serial_nos) == 1:
msg = (_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
.format(d.idx, bold_invalid_serial_nos))
elif invalid_serial_nos:
msg = (_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.")
.format(d.idx, bold_invalid_serial_nos))
self.validate_pos_reserved_serial_nos(d)
self.validate_delivered_serial_nos(d)
else:
if allow_negative_stock:
return
@ -127,15 +142,11 @@ class POSInvoice(SalesInvoice):
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)
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):
msg = (_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.')
.format(d.idx, item_code, warehouse, qty))
if msg:
error_msg.append(msg)
if error_msg:
frappe.throw(error_msg, title=_("Item Unavailable"), as_list=True)
frappe.throw(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.')
.format(d.idx, item_code, warehouse, available_stock), title=_("Item Unavailable"))
def validate_serialised_or_batched_item(self):
error_msg = []
@ -202,9 +213,8 @@ class POSInvoice(SalesInvoice):
for d in self.get("items"):
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "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(
d.idx, frappe.bold(d.item_code)
), title=_("Invalid Item"))
frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice. ")
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
def validate_mode_of_payment(self):
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.purchase_receipt.test_purchase_receipt import make_purchase_receipt
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):
@classmethod
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`")
def tearDown(self):
@ -320,6 +322,34 @@ class TestPOSInvoice(unittest.TestCase):
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):
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

View File

@ -235,11 +235,11 @@ def get_invoice_customer_map(pos_invoices):
return pos_invoice_customer_map
def consolidate_pos_invoices(pos_invoices=[], closing_entry={}):
invoices = pos_invoices or closing_entry.get('pos_transactions') or get_all_unconsolidated_invoices()
def consolidate_pos_invoices(pos_invoices=None, closing_entry=None):
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)
if len(invoices) >= 5 and closing_entry:
if len(invoices) >= 1 and closing_entry:
closing_entry.set_status(update=True, status='Queued')
enqueue_job(create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry)
else:
@ -252,18 +252,18 @@ def unconsolidate_pos_invoices(closing_entry):
pluck='name'
)
if len(merge_logs) >= 5:
if len(merge_logs) >= 1:
closing_entry.set_status(update=True, status='Queued')
enqueue_job(cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry)
else:
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):
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.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.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.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:
merge_log = frappe.get_doc('POS Invoice Merge Log', log)
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.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()
closing_entry = kwargs.get('closing_entry') or {}
job_name = closing_entry.get("name")
if not job_already_enqueued(job_name):
enqueue(
job,
**kwargs,
queue="long",
timeout=10000,
event="processing_merge_logs",
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
)

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
// 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', {
onload: function(frm) {
frm.trigger("get_invoice_fields");
frm.trigger("add_search_options");
},
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,
"engine": "InnoDB",
"field_order": [
"invoice_fields"
"invoice_fields",
"pos_search_fields"
],
"fields": [
{
@ -13,11 +14,17 @@
"fieldtype": "Table",
"label": "POS Field",
"options": "POS Field"
},
{
"fieldname": "pos_search_fields",
"fieldtype": "Table",
"label": "POS Search Fields",
"options": "POS Search Fields"
}
],
"issingle": 1,
"links": [],
"modified": "2020-06-01 15:46:41.478928",
"modified": "2021-04-19 14:56:24.465218",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Settings",

View File

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

View File

@ -4,10 +4,12 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
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 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.pdf import get_pdf
@ -29,7 +31,7 @@ class ProcessStatementOfAccounts(Document):
validate_template(self.body)
if not self.customers:
frappe.throw(frappe._('Customers not selected.'))
frappe.throw(_('Customers not selected.'))
if self.enable_auto_email:
self.to_date = self.start_date
@ -38,7 +40,7 @@ class ProcessStatementOfAccounts(Document):
def get_report_pdf(doc, consolidated=True):
statement_dict = {}
aging = ''
ageing = ''
base_template_path = "frappe/www/printview.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,
'customer': entry.customer
})
col1, aging = get_ageing(ageing_filters)
aging[0]['ageing_based_on'] = doc.ageing_based_on
col1, ageing = get_ageing(ageing_filters)
if ageing:
ageing[0]['ageing_based_on'] = doc.ageing_based_on
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({
'from_date': doc.from_date,
'to_date': doc.to_date,
'company': doc.company,
'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': [entry.customer],
'presentation_currency': presentation_currency,
'group_by': doc.group_by,
'currency': doc.currency,
'cost_center': [cc.cost_center_name for cc in doc.cost_center],
'project': [p.project_name for p in doc.project],
'show_opening_entries': 0,
'include_default_book_entries': 0,
'show_cancelled_entries': 1,
'tax_id': tax_id if tax_id else None
})
col, res = get_soa(filters)
@ -83,11 +89,14 @@ def get_report_pdf(doc, consolidated=True):
if len(res) == 3:
continue
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, \
"css": get_print_style(), "title": "Statement For " + entry.customer})
statement_dict[entry.customer] = html
if not bool(statement_dict):
return False
elif consolidated:
@ -167,7 +176,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
if customer_collection == 'Sales Person':
customers = get_customers_based_on_sales_person(collection_name)
if not bool(customers):
frappe.throw('No Customers found with selected options.')
frappe.throw(_('No Customers found with selected options.'))
else:
if customer_collection == 'Sales Partner':
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 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:
return ''
if billing_and_primary:
primary_email = frappe.get_value('Customer', customer_name, 'email_id')
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]]
else:
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) {
if(frm.doc.__onload && frm.is_new()) {
if(frm.doc.supplier) {
@ -539,5 +561,13 @@ frappe.ui.form.on("Purchase Invoice", {
update_stock: function(frm) {
hide_fields(frm.doc);
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",
"column_break_114",
"auto_repeat",
"update_auto_repeat_reference"
"update_auto_repeat_reference",
"per_received"
],
"fields": [
{
@ -1364,6 +1365,15 @@
"print_hide": 1,
"print_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",

View File

@ -1207,3 +1207,41 @@ def make_inter_company_sales_invoice(source_name, target_doc=None):
def on_doctype_update():
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({
"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()

View File

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

View File

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

View File

@ -276,7 +276,7 @@ class Subscription(Document):
frappe.throw(_('Subscription End Date is mandatory to follow calendar months'))
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):
# todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype?
@ -383,7 +383,9 @@ class Subscription(Document):
invoice.flags.ignore_mandatory = True
invoice.save()
invoice.submit()
if self.submit_invoice:
invoice.submit()
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)
if gl_map and len(gl_map) > 1:
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."))
else:
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:
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}.")
.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()
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):
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.validate_date_with_fiscal_year()
self.validate_party_accounts()
self.validate_inter_company_reference()
self.set_incoming_rate()
@ -233,6 +235,23 @@ class AccountsController(TransactionBase):
validate_fiscal_year(self.get(date_field), self.fiscal_year, self.company,
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):
if self.doctype not in ('Purchase Invoice', 'Purchase Receipt', 'Purchase Order'):
return
@ -240,7 +259,7 @@ class AccountsController(TransactionBase):
if self.is_internal_transfer():
if not (self.get('inter_company_reference') or self.get('inter_company_invoice_reference')
or self.get('inter_company_order_reference')):
msg = _("Internal Sale or Delivery Reference missing. ")
msg = _("Internal Sale or Delivery Reference missing.")
msg += _("Please create purchase from internal sale or delivery document itself")
frappe.throw(msg, title=_("Internal Sales Reference Missing"))
@ -904,29 +923,34 @@ class AccountsController(TransactionBase):
date = self.get("due_date")
due_date = date or posting_date
if party_account_currency == self.company_currency:
grand_total = self.get("base_rounded_total") or self.base_grand_total
else:
grand_total = self.get("rounded_total") or self.grand_total
base_grand_total = self.get("base_rounded_total") or self.base_grand_total
grand_total = self.get("rounded_total") or self.grand_total
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)
if self.get("total_advance"):
grand_total -= 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"))
if not self.get("payment_schedule"):
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:
self.append("payment_schedule", item)
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)
else:
for d in self.get("payment_schedule"):
if d.invoice_portion:
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
def set_due_date(self):
@ -963,22 +987,28 @@ class AccountsController(TransactionBase):
if self.get("payment_schedule"):
total = 0
base_total = 0
for d in self.get("payment_schedule"):
total += flt(d.payment_amount)
base_total += flt(d.base_payment_amount)
if party_account_currency == self.company_currency:
total = flt(total, self.precision("base_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")
base_grand_total = self.get("base_rounded_total") or self.base_grand_total
grand_total = self.get("rounded_total") or self.grand_total
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)
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"))
def is_rounded_total_disabled(self):
@ -1218,7 +1248,7 @@ def update_invoice_status():
@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:
return
@ -1226,14 +1256,14 @@ def get_payment_terms(terms_template, posting_date=None, grand_total=None, bill_
schedule = []
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)
return schedule
@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()
if isinstance(term, text_type):
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.invoice_portion = term.invoice_portion
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 = 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.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
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':
# don't copy manufacturer values if based on part no

View File

@ -17,10 +17,12 @@ class AmazonMWSSettings(Document):
else:
self.enable_sync = 0
@frappe.whitelist()
def get_products_details(self):
if self.enable_amazon == 1:
frappe.enqueue('erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods.get_products_details')
@frappe.whitelist()
def get_order_details(self):
if self.enable_amazon == 1:
after_date = dateutil.parser.parse(self.after_date).strftime("%Y-%m-%d")
@ -40,4 +42,4 @@ def setup_custom_fields():
fieldtype='Data', insert_after='title', read_only=1, print_hide=1)]
}
create_custom_fields(custom_fields)
create_custom_fields(custom_fields)

View File

@ -50,6 +50,7 @@ class TherapyType(Document):
self.db_set('change_in_item', 0)
@frappe.whitelist()
def add_exercises(self):
exercises = self.get_exercises_for_body_parts()
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 {
filters: {
"type": "Deduction"
@ -44,48 +44,49 @@ frappe.ui.form.on('Employee Advance', {
},
refresh: function(frm) {
if (frm.doc.docstatus===1
&& (flt(frm.doc.paid_amount) < flt(frm.doc.advance_amount))
&& frappe.model.can_create("Payment Entry")) {
if (frm.doc.docstatus === 1 &&
(flt(frm.doc.paid_amount) < flt(frm.doc.advance_amount)) &&
frappe.model.can_create("Payment Entry")) {
frm.add_custom_button(__('Payment'),
function() { frm.events.make_payment_entry(frm); }, __('Create'));
}
else if (
frm.doc.docstatus === 1
&& flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount) - flt(frm.doc.return_amount)
&& frappe.model.can_create("Expense Claim")
function () {
frm.events.make_payment_entry(frm);
}, __('Create'));
} else if (
frm.doc.docstatus === 1 &&
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(
__("Expense Claim"),
function() {
function () {
frm.events.make_expense_claim(frm);
},
__('Create')
);
}
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))) {
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))) {
if (frm.doc.repay_unclaimed_amount_from_salary == 0 && frappe.model.can_create("Journal Entry")){
frm.add_custom_button(__("Return"), function() {
if (frm.doc.repay_unclaimed_amount_from_salary == 0 && frappe.model.can_create("Journal Entry")) {
frm.add_custom_button(__("Return"), function() {
frm.trigger('make_return_entry');
}, __('Create'));
}else if (frm.doc.repay_unclaimed_amount_from_salary == 1 && frappe.model.can_create("Additional Salary")){
frm.add_custom_button(__("Deduction from salary"), function() {
} else if (frm.doc.repay_unclaimed_amount_from_salary == 1 && frappe.model.can_create("Additional Salary")) {
frm.add_custom_button(__("Deduction from salary"), function() {
frm.events.make_deduction_via_additional_salary(frm);
}, __('Create'));
}
}
},
make_deduction_via_additional_salary: function(frm){
make_deduction_via_additional_salary: function(frm) {
frappe.call({
method: "erpnext.hr.doctype.employee_advance.employee_advance.create_return_through_additional_salary",
args: {
doc: frm.doc
},
callback: function (r){
callback: function(r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
}
@ -94,7 +95,7 @@ frappe.ui.form.on('Employee Advance', {
make_payment_entry: function(frm) {
var method = "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry";
if(frm.doc.__onload && frm.doc.__onload.make_payment_via_journal_entry) {
if (frm.doc.__onload && frm.doc.__onload.make_payment_via_journal_entry) {
method = "erpnext.hr.doctype.employee_advance.employee_advance.make_bank_entry";
}
return frappe.call({
@ -148,11 +149,11 @@ frappe.ui.form.on('Employee Advance', {
});
},
employee: function (frm) {
employee: function(frm) {
if (frm.doc.employee) {
frappe.run_serially([
() => frm.trigger('get_employee_currency'),
() => frm.trigger('get_pending_amount')
() => frm.trigger('get_employee_currency'),
() => frm.trigger('get_pending_amount')
]);
}
},
@ -199,7 +200,7 @@ frappe.ui.form.on('Employee Advance', {
} else {
frm.set_value("exchange_rate", 1.0);
frm.set_df_property('exchange_rate', 'hidden', 1);
frm.set_df_property("exchange_rate", "description", "" );
frm.set_df_property("exchange_rate", "description", "");
}
frm.refresh_fields();
}
@ -215,8 +216,8 @@ frappe.ui.form.on('Employee Advance', {
callback: function(r) {
frm.set_value("exchange_rate", flt(r.message));
frm.set_df_property('exchange_rate', 'hidden', 0);
frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency
+ " = [?] " + company_currency);
frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency +
" = [?] " + company_currency);
}
});
}

View File

@ -4,10 +4,10 @@ from frappe import _
def get_data():
return {
'fieldname': 'employee_advance',
'non_standard_fieldnames': {
'Payment Entry': 'reference_name',
'Journal Entry': 'reference_name'
},
'non_standard_fieldnames': {
'Payment Entry': 'reference_name',
'Journal Entry': 'reference_name'
},
'transactions': [
{
'items': ['Expense Claim']

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

View File

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

View File

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

View File

@ -28,10 +28,21 @@ class JobApplicant(Document):
if self.email_id:
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:
guess = self.email_id.split('@')[0]
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):
if self.email_id:
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)))
self.validate_from_to_dates('from_date', 'to_date')
@frappe.whitelist()
def allocate_leave(self):
self.validate_values()
leave_allocated_for = []

View File

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

View File

@ -44,6 +44,7 @@ class Loan(AccountsController):
def on_cancel(self):
self.unlink_loan_security_pledge()
self.ignore_linked_doctypes = ['GL Entry']
def set_missing_fields(self):
if not self.company:
@ -70,7 +71,6 @@ class Loan(AccountsController):
frappe.throw(_("Repay From Salary can be selected only for term loans"))
def make_repayment_schedule(self):
if not self.repayment_start_date:
frappe.throw(_("Repayment Start Date is mandatory for term loans"))
@ -78,10 +78,9 @@ class Loan(AccountsController):
payment_date = self.repayment_start_date
balance_amount = self.loan_amount
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
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:
principal_amount += balance_amount
balance_amount = 0.0
@ -195,7 +194,8 @@ def request_loan_closure(loan, posting_date=None):
posting_date = getdate()
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')
write_off_limit = frappe.get_value('Loan Type', loan_type, 'write_off_amount')
@ -359,4 +359,4 @@ def get_shortfall_applicants():
return {
"value": len(applicants),
"fieldtype": "Int"
}
}

View File

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

View File

@ -435,7 +435,6 @@ def get_amounts(amounts, against_loan, posting_date):
@frappe.whitelist()
def calculate_amounts(against_loan, posting_date, payment_type=''):
amounts = {
'penalty_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.v13_0.make_non_standard_user_type #13-04-2021
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.reload_doc('accounts', 'doctype', 'bank_transaction')
rename_field("Bank Transaction", "debit", "deposit")
rename_field("Bank Transaction", "credit", "withdrawal")

View File

@ -38,16 +38,37 @@ def execute():
""".format(doctype), {'parentfield': parentfield})
# 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""")
frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_event = test_event""")
frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_uom = test_uom""")
frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_comment = test_comment""")
frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_event = test_event""")
frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_uom = test_uom""")
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""")
frappe.db.sql("""UPDATE `tabLab Test Group Template` SET lab_test_rate = test_rate""")
rename_fields = {
'lab_test_name': 'test_name',
'lab_test_event': 'test_event',
'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""")
if frappe.db.has_column('Normal Test Template', '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""")
rename_fields = {
'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
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:
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):
self.validate_dates()
self.validate_salary_structure()
self.validate_recurring_additional_salary_overlap()
self.validate_employee_referral()
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):
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):
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):
start_date = getdate(sal_start_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:
if d.overwrite:
if d.component in components_to_overwrite:
frappe.throw(_("Multiple Additional Salaries with overwrite "
"property exist for Salary Component {0} between {1} and {2}.").format(
frappe.throw(_("Multiple Additional Salaries with overwrite property exist for Salary Component {0} between {1} and {2}.").format(
frappe.bold(d.component), start_date, end_date), title=_("Error"))
components_to_overwrite.append(d.component)

View File

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

View File

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

View File

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

View File

@ -151,11 +151,11 @@ class TestTimesheet(unittest.TestCase):
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"
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_slip_based_on_timesheet = 1
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,
"indicator": "Green" if avg_completion > 50 else "Red",
"label": "Average Completion",
"label": _("Average Completion"),
"datatype": "Percent",
},
{
"value": total,
"indicator": "Blue",
"label": "Total Tasks",
"label": _("Total Tasks"),
"datatype": "Int",
},
{
"value": completed,
"indicator": "Green",
"label": "Completed Tasks",
"label": _("Completed Tasks"),
"datatype": "Int",
},
{
"value": total_overdue,
"indicator": "Green" if total_overdue == 0 else "Red",
"label": "Overdue Tasks",
"label": _("Overdue Tasks"),
"datatype": "Int",
}
]

View File

@ -130,6 +130,26 @@
"onboard": 1,
"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",
"hidden": 0,
@ -161,7 +181,7 @@
"type": "Link"
}
],
"modified": "2021-03-26 16:32:00.628561",
"modified": "2021-04-25 16:27:16.548780",
"modified_by": "Administrator",
"module": "Projects",
"name": "Projects",

View File

@ -640,6 +640,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
let key = item.name;
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) {
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"],
company_currency, "items");
this.update_item_grid_labels(company_currency);
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");
this.toggle_item_grid_columns(company_currency);
if(this.frm.fields_dict["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.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
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) {
@ -1379,9 +1414,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
if(frappe.meta.get_docfield(item_grid.doctype, fname))
item_grid.set_column_disp(fname, (show && (me.frm.doc.currency != company_currency)));
});
// set labels
var $wrapper = $(this.frm.wrapper);
},
recalculate: function() {
@ -1995,11 +2027,14 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
terms_template: doc.payment_terms_template,
posting_date: posting_date,
grand_total: doc.rounded_total || doc.grand_total,
base_grand_total: doc.base_rounded_total || doc.base_grand_total,
bill_date: doc.bill_date
},
callback: function(r) {
if(r.message && !r.exc) {
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) {
const me = this;
var row = locals[cdt][cdn];
if(row.payment_term) {
frappe.call({
@ -2015,12 +2051,15 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
term: row.payment_term,
bill_date: this.frm.doc.bill_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) {
if(r.message && !r.exc) {
for (var d in r.message) {
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) {
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;
} else if (!value && doc.doctype && doc.item_name) {
// format blank value in child table

View File

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

View File

@ -28,14 +28,19 @@ class ImportSupplierInvoice(Document):
self.name = "Import Invoice on " + format_datetime(self.creation)
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.file_count = 0
self.purchase_invoices_count = 0
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():
content = get_file_content(file_name, zf)
file_content = bs(content, "xml")
@ -124,9 +129,9 @@ class ImportSupplierInvoice(Document):
if disc_line.find("Percentuale"):
invoices_args["total_discount"] += flt((flt(disc_line.Percentuale.text) / 100) * (rate * qty))
@frappe.whitelist()
def process_file_data(self):
self.status = "Processing File Data"
self.save()
self.db_set("status", "Processing File Data", notify=True, commit=True)
frappe.enqueue_doc(self.doctype, self.name, "import_xml_data", queue="long", timeout=3600)
def publish(self, title, message, count, total):
@ -380,24 +385,3 @@ def create_uom(uom):
new_uom.uom_name = uom
new_uom.save()
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 += __('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?'),
message: message,
indicator: 'orange',
primary_action: function() {
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
args: { doctype, docname: name },
freeze: true,
callback: () => frm.reload_doc()
});
primary_action: {
action: function() {
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
args: { doctype, docname: name },
freeze: true,
callback: () => frm.reload_doc() || dialog.hide()
});
}
},
primary_action_label: __('Yes')
});

View File

@ -339,9 +339,7 @@ def get_eway_bill_details(invoice):
if invoice.is_return:
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'))
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' }
vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' }
@ -450,7 +448,7 @@ def make_einvoice(invoice):
if invoice.is_return:
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)
# not yet implemented
@ -1027,12 +1025,12 @@ def generate_eway_bill(doctype, docname, **kwargs):
gsp_connector.generate_eway_bill(**kwargs)
@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
# gsp_connector = GSPConnector(doctype, docname)
# 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.whitelist()

View File

@ -199,7 +199,7 @@ class Gstr1Report(object):
self.item_tax_rate = frappe._dict()
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`
where parent in (%s)
""" % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1)
@ -207,7 +207,7 @@ class Gstr1Report(object):
for d in items:
if d.item_code not in self.invoice_items.get(d.parent, {}):
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))
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
class SMSCenter(Document):
@frappe.whitelist()
def create_receiver_list(self):
rec, where_clause = '', ''
if self.send_to == 'All Customer Contact':
@ -73,6 +74,7 @@ class SMSCenter(Document):
return receiver_nos
@frappe.whitelist()
def send_sms(self):
receiver_list = []
if not self.message:

View File

@ -23,7 +23,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_va
if search_value:
data = search_serial_or_batch_or_barcode_number(search_value)
item_code = data.get("item_code") if data.get("item_code") else search_value
serial_no = data.get("serial_no") if data.get("serial_no") else ""
batch_no = data.get("batch_no") if data.get("batch_no") else ""
@ -31,7 +31,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_va
if data:
item_info = frappe.db.get_value(
"Item", data.get("item_code"),
"Item", data.get("item_code"),
["name as item_code", "item_name", "description", "stock_uom", "image as item_image", "is_stock_item"]
, as_dict=1)
item_info.setdefault('serial_no', serial_no)
@ -139,8 +139,24 @@ def get_conditions(item_code, serial_no, batch_no, barcode):
if serial_no or batch_no or barcode:
return "item.name = {0}".format(frappe.db.escape(item_code))
return """(item.name like {item_code}
or item.item_name like {item_code})""".format(item_code = frappe.db.escape('%' + item_code + '%'))
return make_condition(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):
cond = "and 1=1"
@ -257,4 +273,4 @@ def set_customer_info(fieldname, customer, value=""):
elif fieldname == 'mobile_no':
contact_doc.set('phone_nos', [{ 'phone': value, 'is_primary_mobile_no': 1}])
frappe.db.set_value('Customer', customer, 'mobile_no', value)
contact_doc.save()
contact_doc.save()

View File

@ -168,6 +168,7 @@ erpnext.PointOfSale.ItemSelector = class {
case (iCode >= 160 && iCode <= 164) || iCode == 170: // ^ ! # $ *
case iCode >= 186 && iCode <= 194: // (; = , - . / `)
case iCode >= 219 && iCode <= 222: // ([ \ ] ')
case iCode == 32: // spacebar
if (oEvent.key !== undefined && 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">
<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>
${invoice.customer}
${frappe.ellipsis(invoice.customer, 20)}
</div>
</div>
<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,
"creation": "2011-12-21 11:08:55",
"custom_format": 1,
@ -6,10 +7,10 @@
"doc_type": "POS Invoice",
"docstatus": 0,
"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,
"line_breaks": 0,
"modified": "2020-04-29 16:45:58.942375",
"modified": "2021-04-15 15:23:28.867135",
"modified_by": "Administrator",
"module": "Selling",
"name": "POS Invoice",

View File

@ -159,6 +159,7 @@ class NamingSeries(Document):
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.whitelist()
def update_series_start(self):
if self.prefix:
prefix = self.parse_naming_series()

View File

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

View File

@ -63,7 +63,7 @@ class Item(WebsiteGenerator):
if self.variant_of:
if not self.item_code:
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:
from frappe.model.naming import set_name_by_naming_series
set_name_by_naming_series(self)
@ -674,10 +674,10 @@ class Item(WebsiteGenerator):
if not records: return
document = _("Stock Reconciliation") if len(records) == 1 else _("Stock Reconciliations")
msg = _("The items {0} and {1} are present in the following {2} : ").format(
msg = _("The items {0} and {1} are present in the following {2} :").format(
frappe.bold(old_name), frappe.bold(new_name), document)
msg += '<br>'
msg += ' <br>'
msg += ', '.join([get_link_to_form("Stock Reconciliation", d.parent) for d in records]) + "<br><br>"
msg += _("Note: To merge the items, create a separate Stock Reconciliation for the old item {0}").format(

View File

@ -13,10 +13,11 @@ class ItemVariantSettings(Document):
def set_default_fields(self):
self.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",
"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:
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'));
}
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) {

View File

@ -53,7 +53,20 @@ class PurchaseReceipt(BuyingController):
'target_ref_field': 'stock_qty',
'source_field': 'stock_qty',
'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):
self.status_updater.extend([
{
@ -221,6 +234,7 @@ class PurchaseReceipt(BuyingController):
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
self.delete_auto_created_batches()
@frappe.whitelist()
def get_current_stock(self):
for d in self.get('supplied_items'):
if self.supplier_warehouse:
@ -513,7 +527,9 @@ class PurchaseReceipt(BuyingController):
def update_billing_status(self, update_modified=True):
updated_pr = [self.name]
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)
for pr in set(updated_pr):

View File

@ -72,16 +72,18 @@
"warehouse",
"rejected_warehouse",
"from_warehouse",
"purchase_order",
"material_request",
"purchase_order",
"purchase_invoice",
"column_break_40",
"is_fixed_asset",
"asset_location",
"asset_category",
"schedule_date",
"quality_inspection",
"purchase_order_item",
"material_request_item",
"purchase_order_item",
"purchase_invoice_item",
"purchase_receipt_item",
"delivery_note_item",
"putaway_rule",
@ -937,7 +939,21 @@
"fieldname": "base_rate_with_margin",
"fieldtype": "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,
"read_only": 1
}
@ -945,7 +961,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-02-23 00:59:14.360847",
"modified": "2021-03-29 04:17:00.336298",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",

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