Merge branch 'version-13-hotfix' into item-tax-templates

This commit is contained in:
barredterra 2021-06-02 12:17:27 +02:00
commit e3557ff131
75 changed files with 2044 additions and 2945 deletions

69
.github/workflows/patch.yml vendored Normal file
View File

@ -0,0 +1,69 @@
name: Patch
on: [pull_request, workflow_dispatch]
jobs:
test:
runs-on: ubuntu-18.04
name: Patch Test
services:
mysql:
image: mariadb:10.3
env:
MYSQL_ALLOW_EMPTY_PASSWORD: YES
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: Clone
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: 3.6
- name: Add to Hosts
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
- name: Run Patch Tests
run: cd ~/frappe-bench/ && wget http://build.erpnext.com/20171108_190013_955977f8_database.sql.gz && bench --site test_site --force restore ~/frappe-bench/20171108_190013_955977f8_database.sql.gz && bench --site test_site migrate

View File

@ -1,6 +1,6 @@
name: CI
name: Server
on: [pull_request, workflow_dispatch, push]
on: [pull_request, workflow_dispatch]
jobs:
test:
@ -10,15 +10,9 @@ jobs:
fail-fast: false
matrix:
include:
- TYPE: "server"
JOB_NAME: "Server"
RUN_COMMAND: cd ~/frappe-bench/ && bench --site test_site run-tests --app erpnext --coverage
- TYPE: "patch"
JOB_NAME: "Patch"
RUN_COMMAND: cd ~/frappe-bench/ && wget http://build.erpnext.com/20171108_190013_955977f8_database.sql.gz && bench --site test_site --force restore ~/frappe-bench/20171108_190013_955977f8_database.sql.gz && bench --site test_site migrate
container: [1, 2, 3]
name: ${{ matrix.JOB_NAME }}
name: Python Unit Tests
services:
mysql:
@ -36,7 +30,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: 3.6
python-version: 3.7
- name: Add to Hosts
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
@ -49,6 +43,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Cache node modules
uses: actions/cache@v2
env:
@ -60,6 +55,7 @@ jobs:
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
@ -76,33 +72,39 @@ jobs:
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
- name: Run Tests
run: ${{ matrix.RUN_COMMAND }}
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator --with-coverage
env:
TYPE: ${{ matrix.TYPE }}
TYPE: server
CI_BUILD_ID: ${{ github.run_id }}
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
- name: Coverage - Pull Request
if: matrix.TYPE == 'server' && github.event_name == 'pull_request'
- name: Upload Coverage Data
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
pip3 install coverage==5.5
pip3 install coveralls==3.0.1
coveralls
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
COVERALLS_FLAG_NAME: run-${{ matrix.container }}
COVERALLS_SERVICE_NAME: ${{ github.event_name == 'pull_request' && 'github' || 'github-actions' }}
COVERALLS_PARALLEL: true
coveralls:
name: Coverage Wrap Up
needs: test
container: python:3-slim
runs-on: ubuntu-18.04
steps:
- name: Clone
uses: actions/checkout@v2
- name: Coveralls Finished
run: |
cd ${GITHUB_WORKSPACE}
pip3 install coverage==5.5
pip3 install coveralls==3.0.1
coveralls --finish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -7,7 +7,8 @@ import frappe
import unittest
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import delete_accounting_dimension
test_dependencies = ['Cost Center', 'Location', 'Warehouse', 'Department']
class TestAccountingDimension(unittest.TestCase):
def setUp(self):

View File

@ -9,6 +9,8 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import create_dimension, disable_dimension
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
test_dependencies = ['Location', 'Cost Center', 'Department']
class TestAccountingDimensionFilter(unittest.TestCase):
def setUp(self):
create_dimension()

View File

@ -10,6 +10,8 @@ from erpnext.accounts.general_ledger import ClosedAccountingPeriod
from erpnext.accounts.doctype.accounting_period.accounting_period import OverlapError
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
test_dependencies = ['Item']
class TestAccountingPeriod(unittest.TestCase):
def test_overlap(self):
ap1 = create_accounting_period(start_date = "2018-04-01",
@ -38,7 +40,7 @@ def create_accounting_period(**args):
accounting_period.start_date = args.start_date or nowdate()
accounting_period.end_date = args.end_date or add_months(nowdate(), 1)
accounting_period.company = args.company or "_Test Company"
accounting_period.period_name =args.period_name or "_Test_Period_Name_1"
accounting_period.period_name = args.period_name or "_Test_Period_Name_1"
accounting_period.append("closed_documents", {
"document_type": 'Sales Invoice', "closed": 1
})

View File

@ -120,4 +120,4 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
plaid_success(token, response) {
frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
}
};
};

View File

@ -11,6 +11,8 @@ from erpnext.buying.doctype.purchase_order.test_purchase_order import create_pur
from erpnext.accounts.doctype.budget.budget import get_actual_expense, BudgetError
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
test_dependencies = ['Monthly Distribution']
class TestBudget(unittest.TestCase):
def test_monthly_budget_crossed_ignore(self):
set_total_expense_zero(nowdate(), "cost_center")

View File

@ -293,7 +293,7 @@ def validate_accounts(file_name):
accounts_dict = {}
for account in accounts:
accounts_dict.setdefault(account["account_name"], account)
if not hasattr(account, "parent_account"):
if "parent_account" not in account:
msg = _("Please make sure the file you are using has 'Parent Account' column present in the header.")
msg += "<br><br>"
msg += _("Alternatively, you can download the template and fill your data in.")

View File

@ -29,7 +29,7 @@ class TestDunning(unittest.TestCase):
self.assertEqual(round(amounts.get('interest_amount'), 2), 0.44)
self.assertEqual(round(amounts.get('dunning_amount'), 2), 20.44)
self.assertEqual(round(amounts.get('grand_total'), 2), 120.44)
def test_gl_entries(self):
dunning = create_dunning()
dunning.submit()

View File

@ -0,0 +1,17 @@
frappe.ui.form.on("Journal Entry", {
refresh: function(frm) {
frm.set_query('company_address', function(doc) {
if(!doc.company) {
frappe.throw(__('Please set Company'));
}
return {
query: 'frappe.contacts.doctype.address.address.address_query',
filters: {
link_doctype: 'Company',
link_name: doc.company
}
};
});
}
});

View File

@ -107,7 +107,7 @@ frappe.ui.form.on('POS Closing Entry', {
frm.set_value("taxes", []);
for (let row of frm.doc.payment_reconciliation) {
row.expected_amount = 0;
row.expected_amount = row.opening_amount;
}
for (let row of frm.doc.pos_transactions) {
@ -154,6 +154,9 @@ function add_to_pos_transaction(d, frm) {
function refresh_payments(d, frm) {
d.payments.forEach(p => {
const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment);
if (p.account == d.account_for_change_amount) {
p.amount -= flt(d.change_amount);
}
if (payment) {
payment.expected_amount += flt(p.amount);
payment.difference = payment.closing_amount - payment.expected_amount;

View File

@ -140,6 +140,7 @@ class POSInvoice(SalesInvoice):
return
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:
frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.')
@ -213,8 +214,9 @@ 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"))
if not frappe.db.exists('Product Bundle', d.item_code):
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:
@ -455,15 +457,36 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
if frappe.db.get_value('Item', item_code, 'is_stock_item'):
bin_qty = get_bin_qty(item_code, warehouse)
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
return bin_qty - pos_sales_qty
else:
if frappe.db.exists('Product Bundle', item_code):
return get_bundle_availability(item_code, warehouse)
def get_bundle_availability(bundle_item_code, warehouse):
product_bundle = frappe.get_doc('Product Bundle', bundle_item_code)
bundle_bin_qty = 1000000
for item in product_bundle.items:
item_bin_qty = get_bin_qty(item.item_code, warehouse)
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
available_qty = item_bin_qty - item_pos_reserved_qty
max_available_bundles = available_qty / item.qty
if bundle_bin_qty > max_available_bundles:
bundle_bin_qty = max_available_bundles
pos_sales_qty = get_pos_reserved_qty(bundle_item_code, warehouse)
return bundle_bin_qty - pos_sales_qty
def get_bin_qty(item_code, warehouse):
bin_qty = frappe.db.sql("""select actual_qty from `tabBin`
where item_code = %s and warehouse = %s
limit 1""", (item_code, warehouse), as_dict=1)
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
bin_qty = bin_qty[0].actual_qty or 0 if bin_qty else 0
return bin_qty - pos_sales_qty
return bin_qty[0].actual_qty or 0 if bin_qty else 0
def get_pos_reserved_qty(item_code, warehouse):
reserved_qty = frappe.db.sql("""select sum(p_item.qty) as qty
@ -522,4 +545,4 @@ def add_return_modes(doc, pos_profile):
mode_of_payment = pos_payment_method.mode_of_payment
if pos_payment_method.allow_in_returns and not [d for d in doc.get('payments') if d.mode_of_payment == mode_of_payment]:
payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company)
append_payment(payment_mode[0])
append_payment(payment_mode[0])

View File

@ -152,7 +152,7 @@ class PricingRule(Document):
frappe.throw(_("Valid from date must be less than valid upto date"))
def validate_condition(self):
if self.condition and ("=" in self.condition) and re.match("""[\w\.:_]+\s*={1}\s*[\w\.@'"]+""", self.condition):
if self.condition and ("=" in self.condition) and re.match(r'[\w\.:_]+\s*={1}\s*[\w\.@\'"]+', self.condition):
frappe.throw(_("Invalid condition expression"))
#--------------------------------------------------------------------------------

View File

@ -94,7 +94,7 @@ def get_report_pdf(doc, consolidated=True):
continue
html = frappe.render_template(template_path, \
{"filters": filters, "data": res, "ageing": ageing[0] if doc.include_ageing else None,
{"filters": filters, "data": res, "ageing": ageing[0] if (doc.include_ageing and ageing) else None,
"letter_head": letter_head if doc.letter_head else None,
"terms_and_conditions": frappe.db.get_value('Terms and Conditions', doc.terms_and_conditions, 'terms')
if doc.terms_and_conditions else None})

View File

@ -636,8 +636,8 @@ class TestPurchaseInvoice(unittest.TestCase):
def test_rejected_serial_no(self):
pi = make_purchase_invoice(item_code="_Test Serialized Item With Series", received_qty=2, qty=1,
rejected_qty=1, rate=500, update_stock=1,
rejected_warehouse = "_Test Rejected Warehouse - _TC")
rejected_qty=1, rate=500, update_stock=1, rejected_warehouse = "_Test Rejected Warehouse - _TC",
allow_zero_valuation_rate=1)
self.assertEqual(frappe.db.get_value("Serial No", pi.get("items")[0].serial_no, "warehouse"),
pi.get("items")[0].warehouse)
@ -994,7 +994,8 @@ def make_purchase_invoice(**args):
"project": args.project,
"rejected_warehouse": args.rejected_warehouse or "",
"rejected_serial_no": args.rejected_serial_no or "",
"asset_location": args.location or ""
"asset_location": args.location or "",
"allow_zero_valuation_rate": args.get("allow_zero_valuation_rate") or 0
})
if args.get_taxes_and_charges:

View File

@ -566,7 +566,7 @@ class TestAsset(unittest.TestCase):
doc = make_invoice(pr.name)
self.assertEqual('Asset Received But Not Billed - _TC', doc.items[0].expense_account)
def test_asset_cwip_toggling_cases(self):
cwip = frappe.db.get_value("Asset Category", "Computers", "enable_cwip_accounting")
name = frappe.db.get_value("Asset Category Account", filters={"parent": "Computers"}, fieldname=["name"])

View File

@ -9,12 +9,12 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.buying.report.subcontracted_raw_materials_to_be_transferred.subcontracted_raw_materials_to_be_transferred import execute
import json, frappe, unittest
class TestSubcontractedItemToBeReceived(unittest.TestCase):
class TestSubcontractedItemToBeTransferred(unittest.TestCase):
def test_pending_and_received_qty(self):
def test_pending_and_transferred_qty(self):
po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes')
make_stock_entry(item_code='_Test Item', target='_Test Warehouse 1 - _TC', qty=100, basic_rate=100)
make_stock_entry(item_code='_Test Item Home Desktop 100', target='_Test Warehouse 1 - _TC', qty=100, basic_rate=100)
make_stock_entry(item_code='_Test Item', target='_Test Warehouse - _TC', qty=100, basic_rate=100)
make_stock_entry(item_code='_Test Item Home Desktop 100', target='_Test Warehouse - _TC', qty=100, basic_rate=100)
transfer_subcontracted_raw_materials(po.name)
col, data = execute(filters=frappe._dict({'supplier': po.supplier,
'from_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=-10)),
@ -38,7 +38,8 @@ def transfer_subcontracted_raw_materials(po):
'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 200, 'stock_uom': 'Nos'}]
rm_item_string = json.dumps(rm_item)
se = frappe.get_doc(make_rm_stock_entry(po, rm_item_string))
se.from_warehouse = '_Test Warehouse 1 - _TC'
se.to_warehouse = '_Test Warehouse 1 - _TC'
se.stock_entry_type = 'Send to Subcontractor'
se.save()
se.submit()
se.submit()

View File

@ -1011,7 +1011,6 @@ class AccountsController(TransactionBase):
else:
grand_total -= self.get("total_advance")
base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("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"))

View File

@ -9,8 +9,7 @@ from frappe.utils import nowdate
from frappe.utils.make_random import get_random
from erpnext.education.doctype.program.test_program import make_program_and_linked_courses
# test_records = frappe.get_test_records('Fees')
test_dependencies = ['Company']
class TestFees(unittest.TestCase):
def test_fees(self):

View File

@ -81,7 +81,7 @@ class TestMpesaSettings(unittest.TestCase):
integration_request.reload()
self.assertEqual(pos_invoice.mpesa_receipt_number, "LGR7OWQX0R")
self.assertEqual(integration_request.status, "Completed")
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
integration_request.delete()
pr.reload()
@ -139,7 +139,7 @@ class TestMpesaSettings(unittest.TestCase):
pr.cancel()
pr.delete()
pos_invoice.delete()
def test_processing_of_only_one_succes_callback_payload(self):
create_mpesa_settings(payment_gateway_name="Payment")
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")

View File

@ -99,5 +99,7 @@ class PlaidConnector():
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions))
transactions.extend(response["transactions"])
return transactions
except ItemError as e:
raise e
except Exception:
frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error"))

View File

@ -16,6 +16,10 @@ frappe.ui.form.on('Plaid Settings', {
new erpnext.integrations.plaidLink(frm);
});
frm.add_custom_button(__('Reset Plaid Link'), () => {
new erpnext.integrations.plaidLink(frm);
});
frm.add_custom_button(__("Sync Now"), () => {
frappe.call({
method: "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.enqueue_synchronization",

View File

@ -12,6 +12,7 @@ from frappe.desk.doctype.tag.tag import add_tag
from frappe.model.document import Document
from frappe.utils import add_months, formatdate, getdate, today
from plaid.errors import ItemError
class PlaidSettings(Document):
@staticmethod
@ -51,7 +52,7 @@ def add_institution(token, response):
})
bank.insert()
except Exception:
frappe.throw(frappe.get_traceback())
frappe.log_error(frappe.get_traceback(), title=_('Plaid Link Error'))
else:
bank = frappe.get_doc("Bank", response["institution"]["name"])
bank.plaid_access_token = access_token
@ -83,7 +84,12 @@ def add_bank_accounts(response, bank, company):
if not acc_subtype:
add_account_subtype(account["subtype"])
if not frappe.db.exists("Bank Account", dict(integration_id=account["id"])):
existing_bank_account = frappe.db.exists("Bank Account", {
'account_name': account["name"],
'bank': bank["bank_name"]
})
if not existing_bank_account:
try:
new_account = frappe.get_doc({
"doctype": "Bank Account",
@ -103,10 +109,27 @@ def add_bank_accounts(response, bank, company):
except frappe.UniqueValidationError:
frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(account["name"]))
except Exception:
frappe.throw(frappe.get_traceback())
frappe.log_error(frappe.get_traceback(), title=_("Plaid Link Error"))
frappe.throw(_("There was an error creating Bank Account while linking with Plaid."),
title=_("Plaid Link Failed"))
else:
result.append(frappe.db.get_value("Bank Account", dict(integration_id=account["id"]), "name"))
try:
existing_account = frappe.get_doc('Bank Account', existing_bank_account)
existing_account.update({
"bank": bank["bank_name"],
"account_name": account["name"],
"account_type": account.get("type", ""),
"account_subtype": account.get("subtype", ""),
"mask": account.get("mask", ""),
"integration_id": account["id"]
})
existing_account.save()
result.append(existing_bank_account)
except Exception:
frappe.log_error(frappe.get_traceback(), title=_("Plaid Link Error"))
frappe.throw(_("There was an error updating Bank Account {} while linking with Plaid.").format(
existing_bank_account), title=_("Plaid Link Failed"))
return result
@ -172,9 +195,16 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
account_id = None
plaid = PlaidConnector(access_token)
transactions = plaid.get_transactions(start_date=start_date, end_date=end_date, account_id=account_id)
return transactions
try:
transactions = plaid.get_transactions(start_date=start_date, end_date=end_date, account_id=account_id)
except ItemError as e:
if e.code == "ITEM_LOGIN_REQUIRED":
msg = _("There was an error syncing transactions.") + " "
msg += _("Please refresh or reset the Plaid linking of the Bank {}.").format(bank) + " "
frappe.log_error(msg, title=_("Plaid Link Refresh Required"))
return transactions or []
def new_bank_transaction(transaction):

View File

@ -29,7 +29,7 @@ class TestTherapyPlan(unittest.TestCase):
self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed')
patient, medical_department, practitioner = create_healthcare_docs()
appointment = create_appointment(patient, practitioner, nowdate())
appointment = create_appointment(patient, practitioner, nowdate())
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name)
session = frappe.get_doc(session)
session.submit()

View File

@ -4,8 +4,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, \
comma_or, get_fullname, add_days, nowdate, get_datetime_str
from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, get_fullname, add_days, nowdate
from erpnext.hr.utils import set_employee_name, get_leave_period, share_doc_with_approver
from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
@ -85,7 +84,7 @@ class LeaveApplication(Document):
def validate_dates(self):
if frappe.db.get_single_value("HR Settings", "restrict_backdated_leave_application"):
if self.from_date and self.from_date < frappe.utils.today():
if self.from_date and getdate(self.from_date) < getdate():
allowed_role = frappe.db.get_single_value("HR Settings", "role_allowed_to_create_backdated_leave_application")
if allowed_role not in frappe.get_roles():
frappe.throw(_("Only users with the {0} role can create backdated leave applications").format(allowed_role))
@ -248,9 +247,9 @@ class LeaveApplication(Document):
self.throw_overlap_error(d)
def throw_overlap_error(self, d):
msg = _("Employee {0} has already applied for {1} between {2} and {3} : ").format(self.employee,
d['leave_type'], formatdate(d['from_date']), formatdate(d['to_date'])) \
+ """ <b><a href="/app/Form/Leave Application/{0}">{0}</a></b>""".format(d["name"])
form_link = get_link_to_form("Leave Application", d.name)
msg = _("Employee {0} has already applied for {1} between {2} and {3} : {4}").format(self.employee,
d['leave_type'], formatdate(d['from_date']), formatdate(d['to_date']), form_link)
frappe.throw(msg, OverlapError)
def get_total_leaves_on_half_day(self):
@ -356,7 +355,7 @@ class LeaveApplication(Document):
sender = dict()
sender['email'] = frappe.get_doc('User', frappe.session.user).email
sender['full_name'] = frappe.utils.get_fullname(sender['email'])
sender['full_name'] = get_fullname(sender['email'])
try:
frappe.sendmail(
@ -823,4 +822,4 @@ def get_leave_approver(employee):
leave_approver = frappe.db.get_value('Department Approver', {'parent': department,
'parentfield': 'leave_approvers', 'idx': 1}, 'approver')
return leave_approver
return leave_approver

View File

@ -5,11 +5,18 @@ from __future__ import unicode_literals
import frappe
import unittest
import erpnext
from frappe.utils import getdate
from erpnext.hr.doctype.upload_attendance.upload_attendance import get_data
from erpnext.hr.doctype.employee.test_employee import make_employee
test_dependencies = ['Holiday List']
class TestUploadAttendance(unittest.TestCase):
@classmethod
def setUpClass(cls):
frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", '_Test Holiday List')
def test_date_range(self):
employee = make_employee("test_employee@company.com")
employee_doc = frappe.get_doc("Employee", employee)

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe
import unittest
from frappe.utils import nowdate,flt, cstr,random_string
from frappe.utils import nowdate, flt, cstr, random_string
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.vehicle_log.vehicle_log import make_expense_claim
@ -18,23 +18,13 @@ class TestVehicleLog(unittest.TestCase):
self.employee_id = make_employee("testdriver@example.com", company="_Test Company")
self.license_plate = get_vehicle(self.employee_id)
def tearDown(self):
frappe.delete_doc("Vehicle", self.license_plate, force=1)
frappe.delete_doc("Employee", self.employee_id, force=1)
def test_make_vehicle_log_and_syncing_of_odometer_value(self):
vehicle_log = frappe.get_doc({
"doctype": "Vehicle Log",
"license_plate": cstr(self.license_plate),
"employee": self.employee_id,
"date":frappe.utils.nowdate(),
"odometer":5010,
"fuel_qty":frappe.utils.flt(50),
"price": frappe.utils.flt(500)
})
vehicle_log.save()
vehicle_log.submit()
vehicle_log = make_vehicle_log(self.license_plate, self.employee_id)
#checking value of vehicle odometer value on submit.
vehicle = frappe.get_doc("Vehicle", self.license_plate)
@ -51,19 +41,9 @@ class TestVehicleLog(unittest.TestCase):
self.assertEqual(vehicle.last_odometer, current_odometer - distance_travelled)
vehicle_log.delete()
def test_vehicle_log_fuel_expense(self):
vehicle_log = frappe.get_doc({
"doctype": "Vehicle Log",
"license_plate": cstr(self.license_plate),
"employee": self.employee_id,
"date": frappe.utils.nowdate(),
"odometer":5010,
"fuel_qty":frappe.utils.flt(50),
"price": frappe.utils.flt(500)
})
vehicle_log.save()
vehicle_log.submit()
vehicle_log = make_vehicle_log(self.license_plate, self.employee_id)
expense_claim = make_expense_claim(vehicle_log.name)
fuel_expense = expense_claim.expenses[0].amount
@ -73,6 +53,18 @@ class TestVehicleLog(unittest.TestCase):
frappe.delete_doc("Expense Claim", expense_claim.name)
frappe.delete_doc("Vehicle Log", vehicle_log.name)
def test_vehicle_log_with_service_expenses(self):
vehicle_log = make_vehicle_log(self.license_plate, self.employee_id, with_services=True)
expense_claim = make_expense_claim(vehicle_log.name)
expenses = expense_claim.expenses[0].amount
self.assertEqual(expenses, 27000)
vehicle_log.cancel()
frappe.delete_doc("Expense Claim", expense_claim.name)
frappe.delete_doc("Vehicle Log", vehicle_log.name)
def get_vehicle(employee_id):
license_plate=random_string(10).upper()
vehicle = frappe.get_doc({
@ -81,15 +73,46 @@ def get_vehicle(employee_id):
"make": "Maruti",
"model": "PCM",
"employee": employee_id,
"last_odometer":5000,
"acquisition_date":frappe.utils.nowdate(),
"last_odometer": 5000,
"acquisition_date": nowdate(),
"location": "Mumbai",
"chassis_no": "1234ABCD",
"uom": "Litre",
"vehicle_value":frappe.utils.flt(500000)
"vehicle_value": flt(500000)
})
try:
vehicle.insert()
except frappe.DuplicateEntryError:
pass
return license_plate
return license_plate
def make_vehicle_log(license_plate, employee_id, with_services=False):
vehicle_log = frappe.get_doc({
"doctype": "Vehicle Log",
"license_plate": cstr(license_plate),
"employee": employee_id,
"date": nowdate(),
"odometer": 5010,
"fuel_qty": flt(50),
"price": flt(500)
})
if with_services:
vehicle_log.append("service_detail", {
"service_item": "Oil Change",
"type": "Inspection",
"frequency": "Mileage",
"expense_amount": flt(500)
})
vehicle_log.append("service_detail", {
"service_item": "Wheels",
"type": "Change",
"frequency": "Half Yearly",
"expense_amount": flt(1500)
})
vehicle_log.save()
vehicle_log.submit()
return vehicle_log

View File

@ -1,4 +1,5 @@
{
"actions": [],
"autoname": "naming_series:",
"creation": "2016-09-03 14:14:51.788550",
"doctype": "DocType",
@ -10,7 +11,6 @@
"naming_series",
"license_plate",
"employee",
"column_break_4",
"column_break_7",
"model",
"make",
@ -65,10 +65,6 @@
"options": "Employee",
"reqd": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
@ -142,7 +138,6 @@
{
"fieldname": "service_detail",
"fieldtype": "Table",
"label": "Service Detail",
"options": "Vehicle Service"
},
{
@ -158,7 +153,7 @@
"fetch_from": "license_plate.last_odometer",
"fieldname": "last_odometer",
"fieldtype": "Int",
"label": "last Odometer Value ",
"label": "Last Odometer Value ",
"read_only": 1,
"reqd": 1
},
@ -168,7 +163,8 @@
}
],
"is_submittable": 1,
"modified": "2020-03-18 16:45:45.060761",
"links": [],
"modified": "2021-05-17 00:10:21.188352",
"modified_by": "Administrator",
"module": "HR",
"name": "Vehicle Log",

View File

@ -37,5 +37,22 @@ frappe.query_reports["Employee Leave Balance"] = {
"fieldtype": "Link",
"options": "Employee",
}
]
],
onload: () => {
frappe.call({
type: "GET",
method: "erpnext.hr.utils.get_leave_period",
args: {
"from_date": frappe.defaults.get_default("year_start_date"),
"to_date": frappe.defaults.get_default("year_end_date"),
"company": frappe.defaults.get_user_default("Company")
},
freeze: true,
callback: (data) => {
frappe.query_report.set_filter_value("from_date", data.message[0].from_date);
frappe.query_report.set_filter_value("to_date", data.message[0].to_date);
}
});
}
}

View File

@ -6,15 +6,16 @@ import frappe
from frappe.utils import flt, add_days
from frappe import _
from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period, get_leave_balance_on
from itertools import groupby
def execute(filters=None):
if filters.to_date <= filters.from_date:
frappe.throw(_('"From date" can not be greater than or equal to "To date"'))
frappe.throw(_('"From Date" can not be greater than or equal to "To Date"'))
columns = get_columns()
data = get_data(filters)
return columns, data
charts = get_chart_data(data)
return columns, data, None, charts
def get_columns():
columns = [{
@ -31,9 +32,10 @@ def get_columns():
'options': 'Employee'
}, {
'label': _('Employee Name'),
'fieldtype': 'Data',
'fieldtype': 'Dynamic Link',
'fieldname': 'employee_name',
'width': 100,
'options': 'employee'
}, {
'label': _('Opening Balance'),
'fieldtype': 'float',
@ -64,8 +66,7 @@ def get_columns():
return columns
def get_data(filters):
leave_types = frappe.db.sql_list("SELECT `name` FROM `tabLeave Type` ORDER BY `name` ASC")
leave_types = frappe.db.get_list('Leave Type', pluck='name', order_by='name')
conditions = get_conditions(filters)
user = frappe.session.user
@ -113,12 +114,8 @@ def get_data(filters):
# not be shown on the basis of days left it create in user mind for carry_forward leave
row.closing_balance = (new_allocation + opening - (row.leaves_expired + leaves_taken))
row.indent = 1
data.append(row)
new_leaves_allocated = 0
return data
@ -129,27 +126,37 @@ def get_conditions(filters):
if filters.get('employee'):
conditions['name'] = filters.get('employee')
if filters.get('employee'):
conditions['name'] = filters.get('employee')
if filters.get('company'):
conditions['company'] = filters.get('company')
if filters.get('department'):
conditions['department'] = filters.get('department')
return conditions
def get_department_leave_approver_map(department=None):
conditions=''
if department:
conditions="and (department_name = '%(department)s' or parent_department = '%(department)s')"%{'department': department}
# get current department and all its child
department_list = frappe.db.sql_list(""" SELECT name FROM `tabDepartment` WHERE disabled=0 {0}""".format(conditions)) #nosec
department_list = frappe.get_list('Department',
filters={
'disabled': 0
},
or_filters={
'name': department,
'parent_department': department
},
fields=['name'],
pluck='name'
)
# retrieve approvers list from current department and from its subsequent child departments
approver_list = frappe.get_all('Department Approver', filters={
'parentfield': 'leave_approvers',
'parent': ('in', department_list)
}, fields=['parent', 'approver'], as_list=1)
approver_list = frappe.get_all('Department Approver',
filters={
'parentfield': 'leave_approvers',
'parent': ('in', department_list)
},
fields=['parent', 'approver'],
as_list=1
)
approvers = {}
@ -190,3 +197,40 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type):
new_allocation += record.leaves
return new_allocation, expired_leaves
def get_chart_data(data):
labels = []
datasets = []
employee_data = data
if data and data[0].get('employee_name'):
get_dataset_for_chart(employee_data, datasets, labels)
chart = {
'data': {
'labels': labels,
'datasets': datasets
},
'type': 'bar',
'colors': ['#456789', '#EE8888', '#7E77BF']
}
return chart
def get_dataset_for_chart(employee_data, datasets, labels):
leaves = []
employee_data = sorted(employee_data, key=lambda k: k['employee_name'])
for key, group in groupby(employee_data, lambda x: x['employee_name']):
for grp in group:
if grp.closing_balance:
leaves.append(frappe._dict({
'leave_type': grp.leave_type,
'closing_balance': grp.closing_balance
}))
if leaves:
labels.append(key)
for leave in leaves:
datasets.append({'name': leave.leave_type, 'values': [leave.closing_balance]})

View File

@ -0,0 +1,73 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils import getdate
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.vehicle_log.vehicle_log import make_expense_claim
from erpnext.hr.doctype.vehicle_log.test_vehicle_log import get_vehicle, make_vehicle_log
from erpnext.hr.report.vehicle_expenses.vehicle_expenses import execute
from erpnext.accounts.utils import get_fiscal_year
class TestVehicleExpenses(unittest.TestCase):
@classmethod
def setUpClass(self):
frappe.db.sql('delete from `tabVehicle Log`')
employee_id = frappe.db.sql('''select name from `tabEmployee` where name="testdriver@example.com"''')
self.employee_id = employee_id[0][0] if employee_id else None
if not self.employee_id:
self.employee_id = make_employee('testdriver@example.com', company='_Test Company')
self.license_plate = get_vehicle(self.employee_id)
def test_vehicle_expenses_based_on_fiscal_year(self):
vehicle_log = make_vehicle_log(self.license_plate, self.employee_id, with_services=True)
expense_claim = make_expense_claim(vehicle_log.name)
# Based on Fiscal Year
filters = {
'filter_based_on': 'Fiscal Year',
'fiscal_year': get_fiscal_year(getdate())[0]
}
report = execute(filters)
expected_data = [{
'vehicle': self.license_plate,
'make': 'Maruti',
'model': 'PCM',
'location': 'Mumbai',
'log_name': vehicle_log.name,
'odometer': 5010,
'date': getdate(),
'fuel_qty': 50.0,
'fuel_price': 500.0,
'fuel_expense': 25000.0,
'service_expense': 2000.0,
'employee': self.employee_id
}]
self.assertEqual(report[1], expected_data)
# Based on Date Range
fiscal_year = get_fiscal_year(getdate(), as_dict=True)
filters = {
'filter_based_on': 'Date Range',
'from_date': fiscal_year.year_start_date,
'to_date': fiscal_year.year_end_date
}
report = execute(filters)
self.assertEqual(report[1], expected_data)
# clean up
vehicle_log.cancel()
frappe.delete_doc('Expense Claim', expense_claim.name)
frappe.delete_doc('Vehicle Log', vehicle_log.name)
def tearDown(self):
frappe.delete_doc('Vehicle', self.license_plate, force=1)
frappe.delete_doc('Employee', self.employee_id, force=1)

View File

@ -1,31 +1,52 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.require("assets/erpnext/js/financial_statements.js", function() {
frappe.query_reports["Vehicle Expenses"] = {
"filters": [
{
"fieldname": "fiscal_year",
"label": __("Fiscal Year"),
"fieldtype": "Link",
"options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"),
"reqd": 1,
"on_change": function(query_report) {
var fiscal_year = query_report.get_values().fiscal_year;
if (!fiscal_year) {
return;
}
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
frappe.query_report.set_filter({
from_date: fy.year_start_date,
to_date: fy.year_end_date
});
});
}
}
]
}
});
frappe.query_reports["Vehicle Expenses"] = {
"filters": [
{
"fieldname": "filter_based_on",
"label": __("Filter Based On"),
"fieldtype": "Select",
"options": ["Fiscal Year", "Date Range"],
"default": ["Fiscal Year"],
"reqd": 1
},
{
"fieldname": "fiscal_year",
"label": __("Fiscal Year"),
"fieldtype": "Link",
"options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"),
"depends_on": "eval: doc.filter_based_on == 'Fiscal Year'",
"reqd": 1
},
{
"fieldname": "from_date",
"label": __("From Date"),
"fieldtype": "Date",
"reqd": 1,
"depends_on": "eval: doc.filter_based_on == 'Date Range'",
"default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12)
},
{
"fieldname": "to_date",
"label": __("To Date"),
"fieldtype": "Date",
"reqd": 1,
"depends_on": "eval: doc.filter_based_on == 'Date Range'",
"default": frappe.datetime.nowdate()
},
{
"fieldname": "vehicle",
"label": __("Vehicle"),
"fieldtype": "Link",
"options": "Vehicle"
},
{
"fieldname": "employee",
"label": __("Employee"),
"fieldtype": "Link",
"options": "Employee"
}
]
};

View File

@ -1,20 +1,23 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2016-09-09 03:33:40.605734",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 2,
"is_standard": "Yes",
"modified": "2017-02-24 19:59:18.641284",
"modified_by": "Administrator",
"module": "HR",
"name": "Vehicle Expenses",
"owner": "Administrator",
"ref_doctype": "Vehicle",
"report_name": "Vehicle Expenses",
"report_type": "Script Report",
"add_total_row": 1,
"columns": [],
"creation": "2016-09-09 03:33:40.605734",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 2,
"is_standard": "Yes",
"modified": "2021-05-16 22:48:22.767535",
"modified_by": "Administrator",
"module": "HR",
"name": "Vehicle Expenses",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Vehicle",
"report_name": "Vehicle Expenses",
"report_type": "Script Report",
"roles": [
{
"role": "Fleet Manager"

View File

@ -5,86 +5,209 @@ from __future__ import unicode_literals
import frappe
import erpnext
from frappe import _
from frappe.utils import flt,cstr
from frappe.utils import flt
from erpnext.accounts.report.financial_statements import get_period_list
def execute(filters=None):
columns, data, chart = [], [], []
if filters.get('fiscal_year'):
company = erpnext.get_default_company()
period_list = get_period_list(filters.get('fiscal_year'), filters.get('fiscal_year'),
'', '', 'Fiscal Year', 'Monthly', company=company)
columns=get_columns()
data=get_log_data(filters)
chart=get_chart_data(data,period_list)
filters = frappe._dict(filters or {})
columns = get_columns()
data = get_vehicle_log_data(filters)
chart = get_chart_data(data, filters)
return columns, data, None, chart
def get_columns():
columns = [_("License") + ":Link/Vehicle:100", _('Create') + ":data:50",
_("Model") + ":data:50", _("Location") + ":data:100",
_("Log") + ":Link/Vehicle Log:100", _("Odometer") + ":Int:80",
_("Date") + ":Date:100", _("Fuel Qty") + ":Float:80",
_("Fuel Price") + ":Float:100",_("Fuel Expense") + ":Float:100",
_("Service Expense") + ":Float:100"
return [
{
'fieldname': 'vehicle',
'fieldtype': 'Link',
'label': _('Vehicle'),
'options': 'Vehicle',
'width': 150
},
{
'fieldname': 'make',
'fieldtype': 'Data',
'label': _('Make'),
'width': 100
},
{
'fieldname': 'model',
'fieldtype': 'Data',
'label': _('Model'),
'width': 80
},
{
'fieldname': 'location',
'fieldtype': 'Data',
'label': _('Location'),
'width': 100
},
{
'fieldname': 'log_name',
'fieldtype': 'Link',
'label': _('Vehicle Log'),
'options': 'Vehicle Log',
'width': 100
},
{
'fieldname': 'odometer',
'fieldtype': 'Int',
'label': _('Odometer Value'),
'width': 120
},
{
'fieldname': 'date',
'fieldtype': 'Date',
'label': _('Date'),
'width': 100
},
{
'fieldname': 'fuel_qty',
'fieldtype': 'Float',
'label': _('Fuel Qty'),
'width': 80
},
{
'fieldname': 'fuel_price',
'fieldtype': 'Float',
'label': _('Fuel Price'),
'width': 100
},
{
'fieldname': 'fuel_expense',
'fieldtype': 'Currency',
'label': _('Fuel Expense'),
'width': 150
},
{
'fieldname': 'service_expense',
'fieldtype': 'Currency',
'label': _('Service Expense'),
'width': 150
},
{
'fieldname': 'employee',
'fieldtype': 'Link',
'label': _('Employee'),
'options': 'Employee',
'width': 150
}
]
return columns
def get_log_data(filters):
fy = frappe.db.get_value('Fiscal Year', filters.get('fiscal_year'), ['year_start_date', 'year_end_date'], as_dict=True)
data = frappe.db.sql("""select
vhcl.license_plate as "License", vhcl.make as "Make", vhcl.model as "Model",
vhcl.location as "Location", log.name as "Log", log.odometer as "Odometer",
log.date as "Date", log.fuel_qty as "Fuel Qty", log.price as "Fuel Price",
log.fuel_qty * log.price as "Fuel Expense"
from
def get_vehicle_log_data(filters):
start_date, end_date = get_period_dates(filters)
conditions, values = get_conditions(filters)
data = frappe.db.sql("""
SELECT
vhcl.license_plate as vehicle, vhcl.make, vhcl.model,
vhcl.location, log.name as log_name, log.odometer,
log.date, log.employee, log.fuel_qty,
log.price as fuel_price,
log.fuel_qty * log.price as fuel_expense
FROM
`tabVehicle` vhcl,`tabVehicle Log` log
where
vhcl.license_plate = log.license_plate and log.docstatus = 1 and date between %s and %s
order by date""" ,(fy.year_start_date, fy.year_end_date), as_dict=1)
dl=list(data)
for row in dl:
row["Service Expense"]= get_service_expense(row["Log"])
return dl
WHERE
vhcl.license_plate = log.license_plate
and log.docstatus = 1
and date between %(start_date)s and %(end_date)s
{0}
ORDER BY date""".format(conditions), values, as_dict=1)
for row in data:
row['service_expense'] = get_service_expense(row.log_name)
return data
def get_conditions(filters):
conditions = ''
start_date, end_date = get_period_dates(filters)
values = {
'start_date': start_date,
'end_date': end_date
}
if filters.employee:
conditions += ' and log.employee = %(employee)s'
values['employee'] = filters.employee
if filters.vehicle:
conditions += ' and vhcl.license_plate = %(vehicle)s'
values['vehicle'] = filters.vehicle
return conditions, values
def get_period_dates(filters):
if filters.filter_based_on == 'Fiscal Year' and filters.fiscal_year:
fy = frappe.db.get_value('Fiscal Year', filters.fiscal_year,
['year_start_date', 'year_end_date'], as_dict=True)
return fy.year_start_date, fy.year_end_date
else:
return filters.from_date, filters.to_date
def get_service_expense(logname):
expense_amount = frappe.db.sql("""select sum(expense_amount)
from `tabVehicle Log` log,`tabVehicle Service` ser
where ser.parent=log.name and log.name=%s""",logname)
return flt(expense_amount[0][0]) if expense_amount else 0
expense_amount = frappe.db.sql("""
SELECT sum(expense_amount)
FROM
`tabVehicle Log` log, `tabVehicle Service` service
WHERE
service.parent=log.name and log.name=%s
""", logname)
return flt(expense_amount[0][0]) if expense_amount else 0.0
def get_chart_data(data, filters):
period_list = get_period_list(filters.fiscal_year, filters.fiscal_year,
filters.from_date, filters.to_date, filters.filter_based_on, 'Monthly')
fuel_data, service_data = [], []
def get_chart_data(data,period_list):
fuel_exp_data,service_exp_data,fueldata,servicedata = [],[],[],[]
service_exp_data = []
fueldata = []
for period in period_list:
total_fuel_exp=0
total_ser_exp=0
for row in data:
if row["Date"] <= period.to_date and row["Date"] >= period.from_date:
total_fuel_exp+=flt(row["Fuel Expense"])
total_ser_exp+=flt(row["Service Expense"])
fueldata.append([period.key,total_fuel_exp])
servicedata.append([period.key,total_ser_exp])
total_fuel_exp = 0
total_service_exp = 0
for row in data:
if row.date <= period.to_date and row.date >= period.from_date:
total_fuel_exp += flt(row.fuel_expense)
total_service_exp += flt(row.service_expense)
fuel_data.append([period.key, total_fuel_exp])
service_data.append([period.key, total_service_exp])
labels = [period.label for period in period_list]
fuel_exp_data= [row[1] for row in fuel_data]
service_exp_data= [row[1] for row in service_data]
labels = [period.key for period in period_list]
fuel_exp_data= [row[1] for row in fueldata]
service_exp_data= [row[1] for row in servicedata]
datasets = []
if fuel_exp_data:
datasets.append({
'name': 'Fuel Expenses',
'name': _('Fuel Expenses'),
'values': fuel_exp_data
})
if service_exp_data:
datasets.append({
'name': 'Service Expenses',
'name': _('Service Expenses'),
'values': service_exp_data
})
chart = {
"data": {
'data': {
'labels': labels,
'datasets': datasets
}
},
'type': 'line',
'fieldtype': 'Currency'
}
chart["type"] = "line"
return chart

View File

@ -269,6 +269,7 @@ def get_total_exemption_amount(declarations):
total_exemption_amount = sum([flt(d.total_exemption_amount) for d in exemptions.values()])
return total_exemption_amount
@frappe.whitelist()
def get_leave_period(from_date, to_date, company):
leave_period = frappe.db.sql("""
select name, from_date, to_date

View File

@ -12,7 +12,7 @@
"idx": 0,
"is_default": 0,
"is_standard": 1,
"label": "Loan Management",
"label": "Loans",
"links": [
{
"hidden": 0,
@ -220,10 +220,10 @@
"type": "Link"
}
],
"modified": "2021-02-18 17:31:53.586508",
"modified": "2021-05-25 17:31:53.586508",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Management",
"name": "Loans",
"owner": "Administrator",
"pin_to_bottom": 0,
"pin_to_top": 0,

View File

@ -2,40 +2,36 @@
// License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.maintenance");
frappe.ui.form.on('Maintenance Schedule', {
setup: function(frm) {
setup: function (frm) {
frm.set_query('contact_person', erpnext.queries.contact_query);
frm.set_query('customer_address', erpnext.queries.address_query);
frm.set_query('customer', erpnext.queries.customer);
frm.add_fetch('item_code', 'item_name', 'item_name');
frm.add_fetch('item_code', 'description', 'description');
},
onload: function(frm) {
onload: function (frm) {
if (!frm.doc.status) {
frm.set_value({status:'Draft'});
frm.set_value({ status: 'Draft' });
}
if (frm.doc.__islocal) {
frm.set_value({transaction_date: frappe.datetime.get_today()});
frm.set_value({ transaction_date: frappe.datetime.get_today() });
}
},
refresh: function(frm) {
refresh: function (frm) {
setTimeout(() => {
frm.toggle_display('generate_schedule', !(frm.is_new()));
frm.toggle_display('schedule', !(frm.is_new()));
},10);
}, 10);
},
customer: function(frm) {
customer: function (frm) {
erpnext.utils.get_party_details(frm)
},
customer_address: function(frm) {
customer_address: function (frm) {
erpnext.utils.get_address_display(frm, 'customer_address', 'address_display');
},
contact_person: function(frm) {
contact_person: function (frm) {
erpnext.utils.get_contact_details(frm);
},
generate_schedule: function(frm) {
generate_schedule: function (frm) {
if (frm.is_new()) {
frappe.msgprint(__('Please save first'));
} else {
@ -46,14 +42,14 @@ frappe.ui.form.on('Maintenance Schedule', {
// TODO commonify this code
erpnext.maintenance.MaintenanceSchedule = frappe.ui.form.Controller.extend({
refresh: function() {
frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'}
refresh: function () {
frappe.dynamic_link = { doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer' };
var me = this;
if (this.frm.doc.docstatus === 0) {
this.frm.add_custom_button(__('Sales Order'),
function() {
function () {
erpnext.utils.map_current_doc({
method: "erpnext.selling.doctype.sales_order.sales_order.make_maintenance_schedule",
source_doctype: "Sales Order",
@ -68,52 +64,107 @@ erpnext.maintenance.MaintenanceSchedule = frappe.ui.form.Controller.extend({
});
}, __("Get Items From"));
} else if (this.frm.doc.docstatus === 1) {
this.frm.add_custom_button(__('Create Maintenance Visit'), function() {
frappe.model.open_mapped_doc({
method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.make_maintenance_visit",
source_name: me.frm.doc.name,
frm: me.frm
let schedules = me.frm.doc.schedules;
let flag = schedules.some(schedule => schedule.completion_status === "Pending");
if (flag) {
this.frm.add_custom_button(__('Maintenance Visit'), function () {
let options = "";
me.frm.call('get_pending_data', {data_type: "items"}).then(r => {
options = r.message;
let schedule_id = "";
let d = new frappe.ui.Dialog({
title: __("Enter Visit Details"),
fields: [{
fieldtype: "Select",
fieldname: "item_name",
label: __("Item Name"),
options: options,
reqd: 1,
onchange: function () {
let field = d.get_field("scheduled_date");
me.frm.call('get_pending_data',
{
item_name: this.value,
data_type: "date"
}).then(r => {
field.df.options = r.message;
field.refresh();
});
}
},
{
label: __('Scheduled Date'),
fieldname: 'scheduled_date',
fieldtype: 'Select',
options: "",
reqd: 1,
onchange: function () {
let field = d.get_field('item_name');
me.frm.call(
'get_pending_data',
{
item_name: field.value,
s_date: this.value,
data_type: "id"
}).then(r => {
schedule_id = r.message;
});
}
},
],
primary_action_label: 'Create Visit',
primary_action(values) {
frappe.call({
method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.make_maintenance_visit",
args: {
item_name: values.item_name,
s_id: schedule_id,
source_name: me.frm.doc.name,
},
callback: function (r) {
if (!r.exc) {
frappe.model.sync(r.message);
frappe.set_route("Form", r.message.doctype, r.message.name);
}
}
});
d.hide();
}
});
d.show();
});
}, __('Create'));
}, __('Create'));
}
}
},
start_date: function(doc, cdt, cdn) {
start_date: function (doc, cdt, cdn) {
this.set_no_of_visits(doc, cdt, cdn);
},
end_date: function(doc, cdt, cdn) {
end_date: function (doc, cdt, cdn) {
this.set_no_of_visits(doc, cdt, cdn);
},
periodicity: function(doc, cdt, cdn) {
periodicity: function (doc, cdt, cdn) {
this.set_no_of_visits(doc, cdt, cdn);
},
no_of_visits: function (doc, cdt, cdn) {
this.set_no_of_visits(doc, cdt, cdn);
},
set_no_of_visits: function(doc, cdt, cdn) {
set_no_of_visits: function (doc, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
if (item.start_date && item.end_date && item.periodicity) {
if(item.start_date > item.end_date) {
frappe.msgprint(__("Row {0}:Start Date must be before End Date", [item.idx]));
return;
}
var date_diff = frappe.datetime.get_diff(item.end_date, item.start_date) + 1;
var days_in_period = {
"Weekly": 7,
"Monthly": 30,
"Quarterly": 91,
"Half Yearly": 182,
"Yearly": 365
}
var no_of_visits = cint(date_diff / days_in_period[item.periodicity]);
frappe.model.set_value(item.doctype, item.name, "no_of_visits", no_of_visits);
let me = this;
if (item.start_date && item.periodicity) {
me.frm.call('validate_end_date_visits');
}
},
});
$.extend(cur_frm.cscript, new erpnext.maintenance.MaintenanceSchedule({frm: cur_frm}));
$.extend(cur_frm.cscript, new erpnext.maintenance.MaintenanceSchedule({ frm: cur_frm }));

View File

@ -4,12 +4,13 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import add_days, getdate, cint, cstr
from frappe.utils import add_days, getdate, cint, cstr, date_diff, formatdate
from frappe import throw, _
from erpnext.utilities.transaction_base import TransactionBase, delete_events
from erpnext.stock.utils import get_valid_serial_nos
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
class MaintenanceSchedule(TransactionBase):
@frappe.whitelist()
@ -32,8 +33,40 @@ class MaintenanceSchedule(TransactionBase):
child.idx = count
count = count + 1
child.sales_person = d.sales_person
child.completion_status = "Pending"
child.item_reference = d.name
@frappe.whitelist()
def validate_end_date_visits(self):
days_in_period = {
"Weekly": 7,
"Monthly": 30,
"Quarterly": 91,
"Half Yearly": 182,
"Yearly": 365
}
for item in self.items:
if item.periodicity and item.start_date:
if not item.end_date:
if item.no_of_visits:
item.end_date = add_days(item.start_date, item.no_of_visits * days_in_period[item.periodicity])
else:
item.end_date = add_days(item.start_date, days_in_period[item.periodicity])
diff = date_diff(item.end_date, item.start_date) + 1
no_of_visits = cint(diff / days_in_period[item.periodicity])
if not item.no_of_visits or item.no_of_visits == 0:
item.end_date = add_days(item.start_date, days_in_period[item.periodicity])
diff = date_diff(item.end_date, item.start_date) + 1
item.no_of_visits = cint(diff / days_in_period[item.periodicity])
elif item.no_of_visits > no_of_visits:
item.end_date = add_days(item.start_date, item.no_of_visits * days_in_period[item.periodicity])
elif item.no_of_visits < no_of_visits:
item.end_date = add_days(item.start_date, item.no_of_visits * days_in_period[item.periodicity])
self.save()
def on_submit(self):
if not self.get('schedules'):
@ -58,9 +91,10 @@ class MaintenanceSchedule(TransactionBase):
if no_email_sp:
frappe.msgprint(
frappe._("Setting Events to {0}, since the Employee attached to the below Sales Persons does not have a User ID{1}").format(
_("Setting Events to {0}, since the Employee attached to the below Sales Persons does not have a User ID{1}").format(
self.owner, "<br>" + "<br>".join(no_email_sp)
))
)
)
scheduled_date = frappe.db.sql("""select scheduled_date from
`tabMaintenance Schedule Detail` where sales_person=%s and item_code=%s and
@ -106,7 +140,7 @@ class MaintenanceSchedule(TransactionBase):
if employee:
holiday_list = get_holiday_list_for_employee(employee)
else:
holiday_list = frappe.get_cached_value('Company', self.company, "default_holiday_list")
holiday_list = frappe.get_cached_value('Company', self.company, "default_holiday_list")
holidays = frappe.db.sql_list('''select holiday_date from `tabHoliday` where parent=%s''', holiday_list)
@ -135,8 +169,7 @@ class MaintenanceSchedule(TransactionBase):
}
if date_diff < days_in_period[d.periodicity]:
throw(_("Row {0}: To set {1} periodicity, difference between from and to date \
must be greater than or equal to {2}")
throw(_("Row {0}: To set {1} periodicity, difference between from and to date must be greater than or equal to {2}")
.format(d.idx, d.periodicity, days_in_period[d.periodicity]))
def validate_maintenance_detail(self):
@ -166,13 +199,15 @@ class MaintenanceSchedule(TransactionBase):
throw(_("Maintenance Schedule {0} exists against {1}").format(chk[0][0], d.sales_order))
def validate(self):
self.validate_end_date_visits()
self.validate_maintenance_detail()
self.validate_dates_with_periodicity()
self.validate_sales_order()
self.generate_schedule()
def on_update(self):
frappe.db.set(self, 'status', 'Draft')
def update_amc_date(self, serial_nos, amc_expiry_date=None):
for serial_no in serial_nos:
serial_no_doc = frappe.get_doc("Serial No", serial_no)
@ -202,8 +237,8 @@ class MaintenanceSchedule(TransactionBase):
if not sr_details.warehouse and sr_details.delivery_date and \
getdate(sr_details.delivery_date) >= getdate(amc_start_date):
throw(_("Maintenance start date can not be before delivery date for Serial No {0}")
.format(serial_no))
throw(_("Maintenance start date can not be before delivery date for Serial No {0}")
.format(serial_no))
def validate_schedule(self):
item_lst1 =[]
@ -245,13 +280,50 @@ class MaintenanceSchedule(TransactionBase):
def on_trash(self):
delete_events(self.doctype, self.name)
@frappe.whitelist()
def get_pending_data(self, data_type, s_date=None, item_name=None):
if data_type == "date":
dates = ""
for schedule in self.schedules:
if schedule.item_name == item_name and schedule.completion_status == "Pending":
dates = dates + "\n" + formatdate(schedule.scheduled_date, "dd-MM-yyyy")
return dates
elif data_type == "items":
items = ""
for item in self.items:
for schedule in self.schedules:
if item.item_name == schedule.item_name and schedule.completion_status == "Pending":
items = items + "\n" + item.item_name
break
return items
elif data_type == "id":
for schedule in self.schedules:
if schedule.item_name == item_name and s_date == formatdate(schedule.scheduled_date, "dd-mm-yyyy"):
return schedule.name
@frappe.whitelist()
def make_maintenance_visit(source_name, target_doc=None):
def update_serial_nos(s_id):
serial_nos = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'serial_no')
if serial_nos:
serial_nos = get_serial_nos(serial_nos)
return serial_nos
else:
return False
@frappe.whitelist()
def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=None):
from frappe.model.mapper import get_mapped_doc
def update_status(source, target, parent):
def update_status_and_detail(source, target, parent):
target.maintenance_type = "Scheduled"
target.maintenance_schedule = source.name
target.maintenance_schedule_detail = s_id
def update_sales(source, target, parent):
sales_person = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'sales_person')
target.service_person = sales_person
target.serial_no = ''
doclist = get_mapped_doc("Maintenance Schedule", source_name, {
"Maintenance Schedule": {
"doctype": "Maintenance Visit",
@ -261,15 +333,12 @@ def make_maintenance_visit(source_name, target_doc=None):
"validation": {
"docstatus": ["=", 1]
},
"postprocess": update_status
"postprocess": update_status_and_detail
},
"Maintenance Schedule Item": {
"doctype": "Maintenance Visit Purpose",
"field_map": {
"parent": "prevdoc_docname",
"parenttype": "prevdoc_doctype",
"sales_person": "service_person"
}
"condition": lambda doc: doc.item_name == item_name,
"postprocess": update_sales
}
}, target_doc)

View File

@ -2,7 +2,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
from frappe.utils.data import get_datetime, add_days
from frappe.utils.data import add_days, today, formatdate
from erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule import make_maintenance_visit
import frappe
import unittest
@ -21,7 +22,57 @@ class TestMaintenanceSchedule(unittest.TestCase):
ms.cancel()
events_after_cancel = get_events(ms)
self.assertTrue(len(events_after_cancel) == 0)
def test_make_schedule(self):
ms = make_maintenance_schedule()
ms.save()
i = ms.items[0]
expected_dates = []
expected_end_date = add_days(i.start_date, i.no_of_visits * 7)
self.assertEqual(i.end_date, expected_end_date)
i.no_of_visits = 2
ms.save()
expected_end_date = add_days(i.start_date, i.no_of_visits * 7)
self.assertEqual(i.end_date, expected_end_date)
items = ms.get_pending_data(data_type = "items")
items = items.split('\n')
items.pop(0)
expected_items = ['_Test Item']
self.assertTrue(items, expected_items)
# "dates" contains all generated schedule dates
dates = ms.get_pending_data(data_type = "date", item_name = i.item_name)
dates = dates.split('\n')
dates.pop(0)
expected_dates.append(formatdate(add_days(i.start_date, 7), "dd-MM-yyyy"))
expected_dates.append(formatdate(add_days(i.start_date, 14), "dd-MM-yyyy"))
# test for generated schedule dates
self.assertEqual(dates, expected_dates)
ms.submit()
s_id = ms.get_pending_data(data_type = "id", item_name = i.item_name, s_date = expected_dates[1])
test = make_maintenance_visit(source_name = ms.name, item_name = "_Test Item", s_id = s_id)
visit = frappe.new_doc('Maintenance Visit')
visit = test
visit.maintenance_schedule = ms.name
visit.maintenance_schedule_detail = s_id
visit.completion_status = "Partially Completed"
visit.set('purposes', [{
'item_code': i.item_code,
'description': "test",
'work_done': "test",
'service_person': "Sales Team",
}])
visit.save()
visit.submit()
ms = frappe.get_doc('Maintenance Schedule', ms.name)
#checks if visit status is back updated in schedule
self.assertTrue(ms.schedules[1].completion_status, "Partially Completed")
def get_events(ms):
return frappe.get_all("Event Participants", filters={
"reference_doctype": ms.doctype,
@ -33,12 +84,11 @@ def make_maintenance_schedule():
ms = frappe.new_doc("Maintenance Schedule")
ms.company = "_Test Company"
ms.customer = "_Test Customer"
ms.transaction_date = get_datetime()
ms.transaction_date = today()
ms.append("items", {
"item_code": "_Test Item",
"start_date": get_datetime(),
"end_date": add_days(get_datetime(), 32),
"start_date": today(),
"periodicity": "Weekly",
"no_of_visits": 4,
"sales_person": "Sales Team",

View File

@ -1,222 +1,137 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "hash",
"beta": 0,
"creation": "2013-02-22 01:28:05",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"autoname": "hash",
"creation": "2013-02-22 01:28:05",
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item_code",
"item_name",
"column_break_3",
"scheduled_date",
"actual_date",
"section_break_6",
"sales_person",
"column_break_8",
"completion_status",
"section_break_10",
"serial_no",
"item_reference"
],
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "item_code",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Item Code",
"length": 0,
"no_copy": 0,
"oldfieldname": "item_code",
"oldfieldtype": "Link",
"options": "Item",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 1,
"set_only_once": 0,
"unique": 0
},
"columns": 2,
"fieldname": "item_code",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Code",
"oldfieldname": "item_code",
"oldfieldtype": "Link",
"options": "Item",
"read_only": 1,
"search_index": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "item_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Item Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "item_name",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "item_name",
"fieldtype": "Data",
"in_global_search": 1,
"label": "Item Name",
"oldfieldname": "item_name",
"oldfieldtype": "Data",
"read_only": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "scheduled_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Scheduled Date",
"length": 0,
"no_copy": 0,
"oldfieldname": "scheduled_date",
"oldfieldtype": "Date",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 1,
"set_only_once": 0,
"unique": 0
},
"columns": 2,
"fieldname": "scheduled_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Scheduled Date",
"oldfieldname": "scheduled_date",
"oldfieldtype": "Date",
"reqd": 1,
"search_index": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "actual_date",
"fieldtype": "Date",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Actual Date",
"length": 0,
"no_copy": 1,
"oldfieldname": "actual_date",
"oldfieldtype": "Date",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 1,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "actual_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Actual Date",
"no_copy": 1,
"oldfieldname": "actual_date",
"oldfieldtype": "Date",
"print_hide": 1,
"read_only": 1,
"report_hide": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sales_person",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Sales Person",
"length": 0,
"no_copy": 0,
"oldfieldname": "incharge_name",
"oldfieldtype": "Link",
"options": "Sales Person",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"allow_on_submit": 1,
"columns": 2,
"fieldname": "sales_person",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Sales Person",
"oldfieldname": "incharge_name",
"oldfieldtype": "Link",
"options": "Sales Person",
"read_only_depends_on": "eval:doc.completion_status != \"Pending\""
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "serial_no",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Serial No",
"length": 0,
"no_copy": 0,
"oldfieldname": "serial_no",
"oldfieldtype": "Small Text",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "160px",
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"fieldname": "serial_no",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Serial No",
"oldfieldname": "serial_no",
"oldfieldtype": "Small Text",
"print_width": "160px",
"read_only": 1,
"width": "160px"
},
{
"columns": 2,
"fieldname": "completion_status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Completion Status",
"options": "Pending\nPartially Completed\nFully Completed",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_10",
"fieldtype": "Section Break"
},
{
"fieldname": "item_reference",
"fieldtype": "Link",
"hidden": 1,
"label": "Item Reference",
"options": "Maintenance Schedule Item",
"read_only": 1
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2017-02-17 17:05:44.644663",
"modified_by": "Administrator",
"module": "Maintenance",
"name": "Maintenance Schedule Detail",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"track_changes": 1,
"track_seen": 0
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-05-27 16:07:25.905015",
"modified_by": "Administrator",
"module": "Maintenance",
"name": "Maintenance Schedule Detail",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -1,431 +1,160 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "hash",
"beta": 0,
"creation": "2013-02-22 01:28:05",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"autoname": "hash",
"creation": "2013-02-22 01:28:05",
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item_code",
"item_name",
"description",
"column_break_4",
"start_date",
"end_date",
"periodicity",
"schedule_details",
"no_of_visits",
"column_break_10",
"sales_person",
"reference",
"serial_no",
"sales_order"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "item_code",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Item Code",
"length": 0,
"no_copy": 0,
"oldfieldname": "item_code",
"oldfieldtype": "Link",
"options": "Item",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 1,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"columns": 2,
"fieldname": "item_code",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Code",
"oldfieldname": "item_code",
"oldfieldtype": "Link",
"options": "Item",
"reqd": 1,
"search_index": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"columns": 1,
"fetch_from": "item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Item Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "item_name",
"oldfieldtype": "Data",
"options": "",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "item_name",
"fieldtype": "Data",
"in_global_search": 1,
"in_list_view": 1,
"label": "Item Name",
"oldfieldname": "item_name",
"oldfieldtype": "Data",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "item_code.description",
"fieldname": "description",
"fieldtype": "Text Editor",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
"oldfieldname": "description",
"oldfieldtype": "Data",
"options": "",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "300px",
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"fieldname": "description",
"fieldtype": "Text Editor",
"label": "Description",
"oldfieldname": "description",
"oldfieldtype": "Data",
"print_width": "300px",
"read_only": 1,
"width": "300px"
},
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "schedule_details",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "schedule_details",
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "start_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Start Date",
"length": 0,
"no_copy": 0,
"oldfieldname": "start_date",
"oldfieldtype": "Date",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 1,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"columns": 2,
"fieldname": "start_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Start Date",
"oldfieldname": "start_date",
"oldfieldtype": "Date",
"reqd": 1,
"search_index": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "end_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "End Date",
"length": 0,
"no_copy": 0,
"oldfieldname": "end_date",
"oldfieldtype": "Date",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 1,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"columns": 2,
"fieldname": "end_date",
"fieldtype": "Date",
"label": "End Date",
"oldfieldname": "end_date",
"oldfieldtype": "Date",
"reqd": 1,
"search_index": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "periodicity",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Periodicity",
"length": 0,
"no_copy": 0,
"oldfieldname": "periodicity",
"oldfieldtype": "Select",
"options": "\nWeekly\nMonthly\nQuarterly\nHalf Yearly\nYearly\nRandom",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"columns": 1,
"fieldname": "periodicity",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Periodicity",
"oldfieldname": "periodicity",
"oldfieldtype": "Select",
"options": "\nWeekly\nMonthly\nQuarterly\nHalf Yearly\nYearly\nRandom"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "no_of_visits",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "No of Visits",
"length": 0,
"no_copy": 0,
"oldfieldname": "no_of_visits",
"oldfieldtype": "Int",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"columns": 1,
"fieldname": "no_of_visits",
"fieldtype": "Int",
"in_list_view": 1,
"label": "No of Visits",
"oldfieldname": "no_of_visits",
"oldfieldtype": "Int",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sales_person",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Sales Person",
"length": 0,
"no_copy": 0,
"oldfieldname": "incharge_name",
"oldfieldtype": "Link",
"options": "Sales Person",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "sales_person",
"fieldtype": "Link",
"label": "Sales Person",
"oldfieldname": "incharge_name",
"oldfieldtype": "Link",
"options": "Sales Person"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "reference",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Reference",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "reference",
"fieldtype": "Section Break",
"label": "Reference"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "serial_no",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Serial No",
"length": 0,
"no_copy": 0,
"oldfieldname": "serial_no",
"oldfieldtype": "Small Text",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "serial_no",
"fieldtype": "Small Text",
"label": "Serial No",
"oldfieldname": "serial_no",
"oldfieldtype": "Small Text"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sales_order",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Sales Order",
"length": 0,
"no_copy": 1,
"oldfieldname": "prevdoc_docname",
"oldfieldtype": "Data",
"options": "Sales Order",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": "150px",
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 1,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"fieldname": "sales_order",
"fieldtype": "Link",
"label": "Sales Order",
"no_copy": 1,
"oldfieldname": "prevdoc_docname",
"oldfieldtype": "Data",
"options": "Sales Order",
"print_hide": 1,
"print_width": "150px",
"read_only": 1,
"search_index": 1,
"width": "150px"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-05-16 22:43:14.260729",
"modified_by": "Administrator",
"module": "Maintenance",
"name": "Maintenance Schedule Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"track_changes": 0,
"track_seen": 0
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-04-15 16:09:47.311994",
"modified_by": "Administrator",
"module": "Maintenance",
"name": "Maintenance Schedule Item",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@ -2,39 +2,62 @@
// License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.maintenance");
var serial_nos = [];
frappe.ui.form.on('Maintenance Visit', {
refresh: function(frm) {
refresh: function (frm) {
//filters for serial_no based on item_code
frm.set_query('serial_no', 'purposes', function(frm, cdt, cdn) {
frm.set_query('serial_no', 'purposes', function (frm, cdt, cdn) {
let item = locals[cdt][cdn];
return {
filters: {
'item_code': item.item_code
}
};
if (serial_nos) {
return {
filters: {
'item_code': item.item_code,
'name': ["in", serial_nos]
}
};
} else {
return {
filters: {
'item_code': item.item_code
}
};
}
});
},
setup: function(frm) {
setup: function (frm) {
frm.set_query('contact_person', erpnext.queries.contact_query);
frm.set_query('customer_address', erpnext.queries.address_query);
frm.set_query('customer', erpnext.queries.customer);
},
onload: function(frm) {
onload: function (frm, cdt, cdn) {
let item = locals[cdt][cdn];
if (frm.maintenance_type == 'Scheduled') {
let schedule_id = item.purposes[0].prevdoc_detail_docname;
frappe.call({
method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.update_serial_nos",
args: {
s_id: schedule_id
},
callback: function (r) {
serial_nos = r.message;
}
});
}
if (!frm.doc.status) {
frm.set_value({status:'Draft'});
frm.set_value({ status: 'Draft' });
}
if (frm.doc.__islocal) {
frm.set_value({mntc_date: frappe.datetime.get_today()});
frm.set_value({ mntc_date: frappe.datetime.get_today() });
}
},
customer: function(frm) {
customer: function (frm) {
erpnext.utils.get_party_details(frm);
},
customer_address: function(frm) {
customer_address: function (frm) {
erpnext.utils.get_address_display(frm, 'customer_address', 'address_display');
},
contact_person: function(frm) {
contact_person: function (frm) {
erpnext.utils.get_contact_details(frm);
}
@ -42,14 +65,14 @@ frappe.ui.form.on('Maintenance Visit', {
// TODO commonify this code
erpnext.maintenance.MaintenanceVisit = frappe.ui.form.Controller.extend({
refresh: function() {
frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'}
refresh: function () {
frappe.dynamic_link = { doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer' };
var me = this;
if (this.frm.doc.docstatus===0) {
if (this.frm.doc.docstatus === 0) {
this.frm.add_custom_button(__('Maintenance Schedule'),
function() {
function () {
erpnext.utils.map_current_doc({
method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.make_maintenance_visit",
source_doctype: "Maintenance Schedule",
@ -64,7 +87,7 @@ erpnext.maintenance.MaintenanceVisit = frappe.ui.form.Controller.extend({
})
}, __("Get Items From"));
this.frm.add_custom_button(__('Warranty Claim'),
function() {
function () {
erpnext.utils.map_current_doc({
method: "erpnext.support.doctype.warranty_claim.warranty_claim.make_maintenance_visit",
source_doctype: "Warranty Claim",
@ -80,7 +103,7 @@ erpnext.maintenance.MaintenanceVisit = frappe.ui.form.Controller.extend({
})
}, __("Get Items From"));
this.frm.add_custom_button(__('Sales Order'),
function() {
function () {
erpnext.utils.map_current_doc({
method: "erpnext.selling.doctype.sales_order.sales_order.make_maintenance_visit",
source_doctype: "Sales Order",
@ -99,4 +122,4 @@ erpnext.maintenance.MaintenanceVisit = frappe.ui.form.Controller.extend({
},
});
$.extend(cur_frm.cscript, new erpnext.maintenance.MaintenanceVisit({frm: cur_frm}));
$.extend(cur_frm.cscript, new erpnext.maintenance.MaintenanceVisit({ frm: cur_frm }));

View File

@ -4,6 +4,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import get_datetime
from erpnext.utilities.transaction_base import TransactionBase
@ -16,44 +17,62 @@ class MaintenanceVisit(TransactionBase):
if d.serial_no and not frappe.db.exists("Serial No", d.serial_no):
frappe.throw(_("Serial No {0} does not exist").format(d.serial_no))
def validate_maintenance_date(self):
if self.maintenance_type == "Scheduled" and self.maintenance_schedule_detail:
item_ref = frappe.db.get_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'item_reference')
if item_ref:
start_date, end_date = frappe.db.get_value('Maintenance Schedule Item', item_ref, ['start_date', 'end_date'])
if get_datetime(self.mntc_date) < get_datetime(start_date) or get_datetime(self.mntc_date) > get_datetime(end_date):
frappe.throw(_("Date must be between {0} and {1}").format(start_date, end_date))
def validate(self):
self.validate_serial_no()
self.validate_maintenance_date()
def update_completion_status(self):
if self.maintenance_schedule_detail:
frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'completion_status', self.completion_status)
def update_actual_date(self):
if self.maintenance_schedule_detail:
frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'actual_date', self.mntc_date)
def update_customer_issue(self, flag):
for d in self.get('purposes'):
if d.prevdoc_docname and d.prevdoc_doctype == 'Warranty Claim' :
if flag==1:
mntc_date = self.mntc_date
service_person = d.service_person
work_done = d.work_done
status = "Open"
if self.completion_status == 'Fully Completed':
status = 'Closed'
elif self.completion_status == 'Partially Completed':
status = 'Work In Progress'
else:
nm = frappe.db.sql("select t1.name, t1.mntc_date, t2.service_person, t2.work_done from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent = t1.name and t1.completion_status = 'Partially Completed' and t2.prevdoc_docname = %s and t1.name!=%s and t1.docstatus = 1 order by t1.name desc limit 1", (d.prevdoc_docname, self.name))
if nm:
status = 'Work In Progress'
mntc_date = nm and nm[0][1] or ''
service_person = nm and nm[0][2] or ''
work_done = nm and nm[0][3] or ''
if not self.maintenance_schedule:
for d in self.get('purposes'):
if d.prevdoc_docname and d.prevdoc_doctype == 'Warranty Claim' :
if flag==1:
mntc_date = self.mntc_date
service_person = d.service_person
work_done = d.work_done
status = "Open"
if self.completion_status == 'Fully Completed':
status = 'Closed'
elif self.completion_status == 'Partially Completed':
status = 'Work In Progress'
else:
status = 'Open'
mntc_date = None
service_person = None
work_done = None
nm = frappe.db.sql("select t1.name, t1.mntc_date, t2.service_person, t2.work_done from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent = t1.name and t1.completion_status = 'Partially Completed' and t2.prevdoc_docname = %s and t1.name!=%s and t1.docstatus = 1 order by t1.name desc limit 1", (d.prevdoc_docname, self.name))
wc_doc = frappe.get_doc('Warranty Claim', d.prevdoc_docname)
wc_doc.update({
'resolution_date': mntc_date,
'resolved_by': service_person,
'resolution_details': work_done,
'status': status
})
if nm:
status = 'Work In Progress'
mntc_date = nm and nm[0][1] or ''
service_person = nm and nm[0][2] or ''
work_done = nm and nm[0][3] or ''
else:
status = 'Open'
mntc_date = None
service_person = None
work_done = None
wc_doc.db_update()
wc_doc = frappe.get_doc('Warranty Claim', d.prevdoc_docname)
wc_doc.update({
'resolution_date': mntc_date,
'resolved_by': service_person,
'resolution_details': work_done,
'status': status
})
wc_doc.db_update()
def check_if_last_visit(self):
"""check if last maintenance visit against same sales order/ Warranty Claim"""
@ -77,6 +96,8 @@ class MaintenanceVisit(TransactionBase):
def on_submit(self):
self.update_customer_issue(1)
frappe.db.set(self, 'status', 'Submitted')
self.update_completion_status()
self.update_actual_date()
def on_cancel(self):
self.check_if_last_visit()

View File

@ -1,4 +1,5 @@
{
"actions": [],
"autoname": "hash",
"creation": "2013-02-22 01:28:06",
"doctype": "DocType",
@ -8,14 +9,15 @@
"field_order": [
"item_code",
"item_name",
"column_break_3",
"service_person",
"serial_no",
"section_break_6",
"description",
"work_details",
"service_person",
"work_done",
"prevdoc_doctype",
"prevdoc_docname",
"prevdoc_detail_docname"
"prevdoc_docname"
],
"fields": [
{
@ -62,6 +64,8 @@
"fieldtype": "Section Break"
},
{
"fetch_from": "prevdoc_detail_docname.sales_person",
"fetch_if_empty": 1,
"fieldname": "service_person",
"fieldtype": "Link",
"in_list_view": 1,
@ -83,49 +87,30 @@
{
"fieldname": "prevdoc_doctype",
"fieldtype": "Link",
"hidden": 1,
"label": "Document Type",
"no_copy": 1,
"oldfieldname": "prevdoc_doctype",
"oldfieldtype": "Data",
"options": "DocType",
"print_hide": 1,
"print_width": "150px",
"read_only": 1,
"report_hide": 1,
"width": "150px"
"options": "DocType"
},
{
"fieldname": "prevdoc_docname",
"fieldtype": "Dynamic Link",
"hidden": 1,
"label": "Against Document No",
"no_copy": 1,
"oldfieldname": "prevdoc_docname",
"oldfieldtype": "Data",
"options": "prevdoc_doctype",
"print_hide": 1,
"print_width": "160px",
"read_only": 1,
"report_hide": 1,
"width": "160px"
"options": "prevdoc_doctype"
},
{
"fieldname": "prevdoc_detail_docname",
"fieldtype": "Data",
"hidden": 1,
"label": "Against Document Detail No",
"no_copy": 1,
"oldfieldname": "prevdoc_detail_docname",
"oldfieldtype": "Data",
"print_hide": 1,
"print_width": "160px",
"read_only": 1,
"report_hide": 1,
"width": "160px"
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
}
],
"idx": 1,
"istable": 1,
"modified": "2020-09-18 17:26:09.703215",
"links": [],
"modified": "2021-05-27 17:47:21.474282",
"modified_by": "Administrator",
"module": "Maintenance",
"name": "Maintenance Visit Purpose",

View File

@ -76,9 +76,9 @@ frappe.ui.form.on("Work Order", {
frm.set_query("production_item", function() {
return {
query: "erpnext.controllers.queries.item_query",
filters:[
['is_stock_item', '=',1]
]
filters: {
"is_stock_item": 1,
}
};
});

View File

@ -779,5 +779,7 @@ erpnext.patches.v12_0.add_ewaybill_validity_field
erpnext.patches.v13_0.germany_make_custom_fields
erpnext.patches.v13_0.germany_fill_debtor_creditor_number
erpnext.patches.v13_0.set_pos_closing_as_failed
execute:frappe.rename_doc("Workspace", "Loan Management", "Loans", force=True)
erpnext.patches.v13_0.update_timesheet_changes
erpnext.patches.v13_0.set_training_event_attendance
erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold

View File

@ -19,6 +19,9 @@ def execute():
logger.info("purchase_receipt_status: begin patch, PR count: {}"
.format(len(affected_purchase_receipts)))
frappe.reload_doc("stock", "doctype", "Purchase Receipt")
frappe.reload_doc("stock", "doctype", "Purchase Receipt Item")
for pr in affected_purchase_receipts:
pr_name = pr[0]

View File

@ -0,0 +1,20 @@
# Copyright (c) 2020, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
if frappe.db.exists('DocType', 'Issue'):
frappe.reload_doc("support", "doctype", "issue")
rename_status()
def rename_status():
frappe.db.sql("""
UPDATE
`tabIssue`
SET
status = 'On Hold'
WHERE
status = 'Hold'
""")

View File

@ -15,7 +15,13 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_
from erpnext.loan_management.doctype.loan.test_loan import create_loan, make_loan_disbursement_entry, create_loan_type, create_loan_accounts
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans
test_dependencies = ['Holiday List']
class TestPayrollEntry(unittest.TestCase):
@classmethod
def setUpClass(cls):
frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", '_Test Holiday List')
def setUp(self):
for dt in ["Salary Slip", "Salary Component", "Salary Component Account",
"Payroll Entry", "Salary Structure", "Salary Structure Assignment", "Payroll Employee Detail", "Additional Salary"]:

View File

@ -644,14 +644,14 @@ frappe.help.help_links["List/Payment Request"] = [
frappe.help.help_links["List/Asset"] = [
{
label: "Managing Fixed Assets",
url: docsUrl + "user/manual/en/accounts/managing-fixed-assets",
url: docsUrl + "user/manual/en/accounts/opening-balance/fixed_assets",
},
];
frappe.help.help_links["List/Asset Category"] = [
{
label: "Asset Category",
url: docsUrl + "user/manual/en/accounts/managing-fixed-assets",
url: docsUrl + "user/manual/en/asset/asset-category",
},
];
@ -663,7 +663,7 @@ frappe.help.help_links["List/Item"] = [
{ label: "Item", url: docsUrl + "user/manual/en/stock/item" },
{
label: "Item Price",
url: docsUrl + "user/manual/en/stock/item/item-price",
url: docsUrl + "user/manual/en/stock/item-price",
},
{
label: "Barcode",
@ -672,25 +672,25 @@ frappe.help.help_links["List/Item"] = [
},
{
label: "Item Wise Taxation",
url: docsUrl + "user/manual/en/accounts/item-wise-taxation",
url: docsUrl + "user/manual/en/accounts/item-tax-template",
},
{
label: "Managing Fixed Assets",
url: docsUrl + "user/manual/en/accounts/managing-fixed-assets",
url: docsUrl + "user/manual/en/accounts/opening-balance/fixed_assets",
},
{
label: "Item Codification",
url: docsUrl + "user/manual/en/stock/item/item-codification",
url: docsUrl + "user/manual/en/stock/articles/item-codification",
},
{
label: "Item Variants",
url: docsUrl + "user/manual/en/stock/item/item-variants",
url: docsUrl + "user/manual/en/stock/item-variants",
},
{
label: "Item Valuation",
url:
docsUrl +
"user/manual/en/stock/item/item-valuation-fifo-and-moving-average",
"user/manual/en/stock/articles/item-valuation-fifo-and-moving-average",
},
];
@ -698,7 +698,7 @@ frappe.help.help_links["Form/Item"] = [
{ label: "Item", url: docsUrl + "user/manual/en/stock/item" },
{
label: "Item Price",
url: docsUrl + "user/manual/en/stock/item/item-price",
url: docsUrl + "user/manual/en/stock/item-price",
},
{
label: "Barcode",
@ -707,19 +707,19 @@ frappe.help.help_links["Form/Item"] = [
},
{
label: "Item Wise Taxation",
url: docsUrl + "user/manual/en/accounts/item-wise-taxation",
url: docsUrl + "user/manual/en/accounts/item-tax-template",
},
{
label: "Managing Fixed Assets",
url: docsUrl + "user/manual/en/accounts/managing-fixed-assets",
url: docsUrl + "user/manual/en/accounts/opening-balance/fixed_assets",
},
{
label: "Item Codification",
url: docsUrl + "user/manual/en/stock/item/item-codification",
url: docsUrl + "user/manual/en/stock/articles/item-codification",
},
{
label: "Item Variants",
url: docsUrl + "user/manual/en/stock/item/item-variants",
url: docsUrl + "user/manual/en/stock/item-variants",
},
{
label: "Item Valuation",

View File

@ -1,4 +1,3 @@
@import "frappe/public/scss/desk/variables";
@import "frappe/public/scss/common/mixins";
body.product-page {
@ -74,15 +73,6 @@ body.product-page {
}
}
// .card-body {
// text-align: center;
// }
// .featured-item {
// .card-body {
// text-align: left;
// }
// }
.card-img-container {
height: 210px;
width: 100%;
@ -217,12 +207,12 @@ body.product-page {
border-color: var(--table-border-color) !important;
padding: 15px;
@include media-breakpoint-between(xs, md) {
@media (max-width: var(--md-width)) {
height: 300px;
width: 300px;
}
@include media-breakpoint-up(lg) {
@media (min-width: var(--lg-width)) {
height: 350px;
width: 350px;
}
@ -233,11 +223,12 @@ body.product-page {
}
.item-slideshow {
@include media-breakpoint-between(xs, md) {
@media (max-width: var(--md-width)) {
max-height: 320px;
}
@include media-breakpoint-up(lg) {
@media (min-width: var(--lg-width)) {
max-height: 430px;
}
@ -254,7 +245,7 @@ body.product-page {
cursor: pointer;
&:hover, &.active {
border-color: $primary;
border-color: var(--primary);
}
}
@ -316,12 +307,9 @@ body.product-page {
}
.item-group-slideshow {
.item-group-description {
// max-width: 900px;
}
.carousel-inner.rounded-carousel {
border-radius: $card-border-radius;
border-radius: var(--card-border-radius);
}
}

View File

@ -1,4 +1,3 @@
@import "frappe/public/scss/website/variables";
.filter-options {
max-height: 300px;
@ -14,7 +13,7 @@
}
&.active {
border-color: $primary;
border-color: var(--primary);
.check {
display: inline-flex;
@ -25,7 +24,7 @@
.check {
display: inline-flex;
padding: 0.25rem;
background: $primary;
background: var(--primary);
color: white;
border-radius: 50%;
font-size: 12px;
@ -38,12 +37,12 @@
}
.result {
border-bottom: 1px solid $border-color;
border-bottom: 1px solid var(--border-color);
}
.transaction-list-item {
padding: 1rem 0;
border-top: 1px solid $border-color;
border-top: 1px solid var(--border-color);
position: relative;
a.transaction-item-link {

View File

@ -310,7 +310,7 @@ class GSTR3BReport(Document):
self.report_dict['sup_details']['osup_det']['txval'] += taxable_value
gst_category = self.invoice_detail_map.get(inv, {}).get('gst_category')
place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply', '00-Other Territory')
place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply') or '00-Other Territory'
if gst_category in ['Unregistered', 'Registered Composition', 'UIN Holders'] and \
self.gst_details.get("gst_state") != place_of_supply.split("-")[1]:

View File

@ -500,7 +500,7 @@ def download_ewb_json():
if not isinstance(docname, list):
# removes characters not allowed in a filename (https://stackoverflow.com/a/38766141/4767738)
filename_prefix = re.sub('[^\w_.)( -]', '', docname)
filename_prefix = re.sub(r'[^\w_.)( -]', '', docname)
frappe.local.response.filename = '{0}_e-WayBill_Data_{1}.json'.format(filename_prefix, frappe.utils.random_string(5))

View File

@ -15,7 +15,6 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_va
data = dict()
result = []
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
warehouse, hide_unavailable_items = frappe.db.get_value('POS Profile', pos_profile, ['warehouse', 'hide_unavailable_items'])
if not frappe.db.exists('Item Group', item_group):
@ -62,7 +61,6 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_va
`tabItem` item {bin_join_selection}
WHERE
item.disabled = 0
AND item.is_stock_item = 1
AND item.has_variants = 0
AND item.is_sales_item = 1
AND item.is_fixed_asset = 0
@ -84,6 +82,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_va
), {'warehouse': warehouse}, as_dict=1)
if items_data:
items_data = filter_service_items(items_data)
items = [d.item_code for d in items_data]
item_prices_data = frappe.get_all("Item Price",
fields = ["item_code", "price_list_rate", "currency"],
@ -96,10 +95,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_va
for item in items_data:
item_code = item.item_code
item_price = item_prices.get(item_code) or {}
if allow_negative_stock:
item_stock_qty = frappe.db.sql("""select ifnull(sum(actual_qty), 0) from `tabBin` where item_code = %s""", item_code)[0][0]
else:
item_stock_qty = get_stock_availability(item_code, warehouse)
item_stock_qty = get_stock_availability(item_code, warehouse)
row = {}
row.update(item)
@ -135,6 +131,14 @@ def search_serial_or_batch_or_barcode_number(search_value):
return {}
def filter_service_items(items):
for item in items:
if not item['is_stock_item']:
if not frappe.db.exists('Product Bundle', item['item_code']):
items.remove(item)
return items
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))

View File

@ -241,10 +241,8 @@ erpnext.PointOfSale.Controller = class {
events: {
get_frm: () => this.frm,
cart_item_clicked: (item_code, batch_no, uom) => {
const search_field = batch_no ? 'batch_no' : 'item_code';
const search_value = batch_no || item_code;
const item_row = this.frm.doc.items.find(i => i[search_field] === search_value && i.uom === uom);
cart_item_clicked: (item_code, batch_no, uom, rate) => {
const item_row = this.get_item_from_frm(item_code, batch_no, uom, rate);
this.item_details.toggle_item_details_section(item_row);
},
@ -275,18 +273,25 @@ erpnext.PointOfSale.Controller = class {
this.cart.toggle_numpad(minimize);
},
form_updated: async (cdt, cdn, fieldname, value) => {
form_updated: (cdt, cdn, fieldname, value) => {
const item_row = frappe.model.get_doc(cdt, cdn);
if (item_row && item_row[fieldname] != value) {
const { item_code, batch_no, uom } = this.item_details.current_item;
const { item_code, batch_no, uom, rate } = this.item_details.current_item;
const event = {
field: fieldname,
value,
item: { item_code, batch_no, uom }
item: { item_code, batch_no, uom, rate }
}
return this.on_cart_update(event)
}
return Promise.resolve();
},
highlight_cart_item: (item) => {
const cart_item = this.cart.get_cart_item(item);
this.cart.toggle_item_highlight(cart_item);
},
item_field_focused: (fieldname) => {
@ -501,8 +506,8 @@ erpnext.PointOfSale.Controller = class {
let item_row = undefined;
try {
let { field, value, item } = args;
const { item_code, batch_no, serial_no, uom } = item;
item_row = this.get_item_from_frm(item_code, batch_no, uom);
const { item_code, batch_no, serial_no, uom, rate } = item;
item_row = this.get_item_from_frm(item_code, batch_no, uom, rate);
const item_selected_from_selector = field === 'qty' && value === "+1"
@ -535,7 +540,7 @@ erpnext.PointOfSale.Controller = class {
item_selected_from_selector && (value = flt(value))
const args = { item_code, batch_no, [field]: value };
const args = { item_code, batch_no, rate, [field]: value };
if (serial_no) {
await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no);
@ -550,9 +555,11 @@ erpnext.PointOfSale.Controller = class {
await this.check_stock_availability(item_row, value, this.frm.doc.set_warehouse);
await this.trigger_new_item_events(item_row);
this.check_serial_batch_selection_needed(item_row) && this.edit_item_details_of(item_row);
this.update_cart_html(item_row);
this.item_details.$component.is(':visible') && this.edit_item_details_of(item_row);
this.check_serial_batch_selection_needed(item_row) && this.edit_item_details_of(item_row);
}
} catch (error) {
@ -563,12 +570,13 @@ erpnext.PointOfSale.Controller = class {
}
}
get_item_from_frm(item_code, batch_no, uom) {
get_item_from_frm(item_code, batch_no, uom, rate) {
const has_batch_no = batch_no;
return this.frm.doc.items.find(
i => i.item_code === item_code
&& (!has_batch_no || (has_batch_no && i.batch_no === batch_no))
&& (i.uom === uom)
&& (i.rate == rate)
);
}

View File

@ -184,7 +184,8 @@ erpnext.PointOfSale.ItemCart = class {
const item_code = unescape($cart_item.attr('data-item-code'));
const batch_no = unescape($cart_item.attr('data-batch-no'));
const uom = unescape($cart_item.attr('data-uom'));
me.events.cart_item_clicked(item_code, batch_no, uom);
const rate = unescape($cart_item.attr('data-rate'));
me.events.cart_item_clicked(item_code, batch_no, uom, rate);
this.numpad_value = '';
});
@ -520,28 +521,34 @@ erpnext.PointOfSale.ItemCart = class {
}
}
get_cart_item({ item_code, batch_no, uom }) {
get_cart_item({ item_code, batch_no, uom, rate }) {
const batch_attr = `[data-batch-no="${escape(batch_no)}"]`;
const item_code_attr = `[data-item-code="${escape(item_code)}"]`;
const uom_attr = `[data-uom="${escape(uom)}"]`;
const rate_attr = `[data-rate="${escape(rate)}"]`;
const item_selector = batch_no ?
`.cart-item-wrapper${batch_attr}${uom_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}`;
`.cart-item-wrapper${batch_attr}${uom_attr}${rate_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}${rate_attr}`;
return this.$cart_items_wrapper.find(item_selector);
}
get_item_from_frm(item) {
const doc = this.events.get_frm().doc;
const { item_code, batch_no, uom, rate } = item;
const search_field = batch_no ? 'batch_no' : 'item_code';
const search_value = batch_no || item_code;
return doc.items.find(i => i[search_field] === search_value && i.uom === uom && i.rate === rate);
}
update_item_html(item, remove_item) {
const $item = this.get_cart_item(item);
if (remove_item) {
$item && $item.next().remove() && $item.remove();
} else {
const { item_code, batch_no, uom } = item;
const search_field = batch_no ? 'batch_no' : 'item_code';
const search_value = batch_no || item_code;
const item_row = this.events.get_frm().doc.items.find(i => i[search_field] === search_value && i.uom === uom);
const item_row = this.get_item_from_frm(item);
this.render_cart_item(item_row, $item);
}
@ -559,7 +566,7 @@ erpnext.PointOfSale.ItemCart = class {
this.$cart_items_wrapper.append(
`<div class="cart-item-wrapper"
data-item-code="${escape(item_data.item_code)}" data-uom="${escape(item_data.uom)}"
data-batch-no="${escape(item_data.batch_no || '')}">
data-batch-no="${escape(item_data.batch_no || '')}" data-rate="${escape(item_data.rate)}">
</div>
<div class="seperator"></div>`
)
@ -636,13 +643,23 @@ erpnext.PointOfSale.ItemCart = class {
function get_item_image_html() {
const { image, item_name } = item_data;
if (image) {
return `<div class="item-image"><img src="${image}" alt="${image}""></div>`;
return `
<div class="item-image">
<img
onerror="cur_pos.cart.handle_broken_image(this)"
src="${image}" alt="${frappe.get_abbr(item_name)}"">
</div>`;
} else {
return `<div class="item-image item-abbr">${frappe.get_abbr(item_name)}</div>`;
}
}
}
handle_broken_image($img) {
const item_abbr = $($img).attr('alt');
$($img).parent().replaceWith(`<div class="item-image item-abbr">${item_abbr}</div>`);
}
scroll_to_item($item) {
if ($item.length === 0) return;
const scrollTop = $item.offset().top - this.$cart_items_wrapper.offset().top + this.$cart_items_wrapper.scrollTop();

View File

@ -54,13 +54,24 @@ erpnext.PointOfSale.ItemDetails = class {
this.$dicount_section = this.$component.find('.discount-section');
}
toggle_item_details_section(item) {
const { item_code, batch_no, uom } = this.current_item;
has_item_has_changed(item) {
const { item_code, batch_no, uom, rate } = this.current_item;
const item_code_is_same = item && item_code === item.item_code;
const batch_is_same = item && batch_no == item.batch_no;
const uom_is_same = item && uom === item.uom;
const rate_is_same = item && rate === item.rate;
if (!item)
return false;
this.item_has_changed = !item ? false : item_code_is_same && batch_is_same && uom_is_same ? false : true;
if (item_code_is_same && batch_is_same && uom_is_same && rate_is_same)
return false;
return true;
}
toggle_item_details_section(item) {
this.item_has_changed = this.has_item_has_changed(item);
this.events.toggle_item_selector(this.item_has_changed);
this.toggle_component(this.item_has_changed);
@ -72,11 +83,12 @@ erpnext.PointOfSale.ItemDetails = class {
this.item_row = item;
this.currency = this.events.get_frm().doc.currency;
this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom };
this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom, rate: item.rate };
this.render_dom(item);
this.render_discount_dom(item);
this.render_form(item);
this.events.highlight_cart_item(item);
} else {
this.validate_serial_batch_item();
this.current_item = {};
@ -198,12 +210,14 @@ erpnext.PointOfSale.ItemDetails = class {
if (this.allow_rate_change) {
this.rate_control.df.onchange = function() {
if (this.value || flt(this.value) === 0) {
me.events.set_value_in_current_cart_item('rate', this.value);
me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => {
const item_row = frappe.get_doc(me.doctype, me.name);
const doc = me.events.get_frm().doc;
me.$item_price.html(format_currency(item_row.rate, doc.currency));
me.render_discount_dom(item_row);
});
me.current_item.rate = this.value;
}
};
} else {
@ -292,11 +306,7 @@ erpnext.PointOfSale.ItemDetails = class {
frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => {
const field_control = this[`${fieldname}_control`];
const { item_code, batch_no, uom } = this.current_item;
const item_code_is_same = item_code === item_row.item_code;
const batch_is_same = batch_no == item_row.batch_no;
const uom_is_same = uom === item_row.uom;
const item_is_same = item_code_is_same && batch_is_same && uom_is_same ? true : false;
const item_is_same = !this.has_item_has_changed(item_row);
if (item_is_same && field_control && field_control.get_value() !== value) {
field_control.set_value(value);

View File

@ -78,7 +78,7 @@ erpnext.PointOfSale.ItemSelector = class {
get_item_html(item) {
const me = this;
// eslint-disable-next-line no-unused-vars
const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom } = item;
const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom, price_list_rate } = item;
const indicator_color = actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange";
let qty_to_display = actual_qty;
@ -94,7 +94,11 @@ erpnext.PointOfSale.ItemSelector = class {
<span class="indicator-pill whitespace-nowrap ${indicator_color}">${qty_to_display}</span>
</div>
<div class="flex items-center justify-center h-32 border-b-grey text-6xl text-grey-100">
<img class="h-full" src="${item_image}" alt="${frappe.get_abbr(item.item_name)}" style="object-fit: cover;">
<img
onerror="cur_pos.item_selector.handle_broken_image(this)"
class="h-full" src="${item_image}"
alt="${frappe.get_abbr(item.item_name)}"
style="object-fit: cover;">
</div>`;
} else {
return `<div class="item-qty-pill">
@ -108,6 +112,7 @@ erpnext.PointOfSale.ItemSelector = class {
`<div class="item-wrapper"
data-item-code="${escape(item.item_code)}" data-serial-no="${escape(serial_no)}"
data-batch-no="${escape(batch_no)}" data-uom="${escape(stock_uom)}"
data-rate="${escape(price_list_rate)}"
title="${item.item_name}">
${get_item_image_html()}
@ -116,12 +121,17 @@ erpnext.PointOfSale.ItemSelector = class {
<div class="item-name">
${frappe.ellipsis(item.item_name, 18)}
</div>
<div class="item-rate">${format_currency(item.price_list_rate, item.currency, 0) || 0}</div>
<div class="item-rate">${format_currency(price_list_rate, item.currency, 0) || 0}</div>
</div>
</div>`
);
}
handle_broken_image($img) {
const item_abbr = $($img).attr('alt');
$($img).parent().replaceWith(`<div class="item-display abbr">${item_abbr}</div>`);
}
make_search_bar() {
const me = this;
const doc = me.events.get_frm().doc;
@ -213,13 +223,15 @@ erpnext.PointOfSale.ItemSelector = class {
let batch_no = unescape($item.attr('data-batch-no'));
let serial_no = unescape($item.attr('data-serial-no'));
let uom = unescape($item.attr('data-uom'));
let rate = unescape($item.attr('data-rate'));
// escape(undefined) returns "undefined" then unescape returns "undefined"
batch_no = batch_no === "undefined" ? undefined : batch_no;
serial_no = serial_no === "undefined" ? undefined : serial_no;
uom = uom === "undefined" ? undefined : uom;
rate = rate === "undefined" ? undefined : rate;
me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom }});
me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom, rate }});
me.set_search_value('');
});

View File

@ -249,7 +249,7 @@ class EmailDigest(Document):
card = cache.get(cache_key)
if card:
card = eval(card)
card = frappe.safe_eval(card)
else:
card = frappe._dict(getattr(self, "get_" + key)())
@ -808,7 +808,6 @@ def get_incomes_expenses_for_period(account, from_date, to_date):
val = balance_on_to_date - balance_before_from_date
else:
last_year_closing_balance = get_balance_on(account, date=fy_start_date - timedelta(days=1))
print(fy_start_date - timedelta(days=1), last_year_closing_balance)
val = balance_on_to_date + (last_year_closing_balance - balance_before_from_date)
return val
@ -837,4 +836,4 @@ def get_future_date_for_calendaer_event(frequency):
elif frequency == "Monthly":
to_date = add_to_date(from_date, months=1)
return from_date, to_date
return from_date, to_date

View File

@ -112,6 +112,9 @@ def make_taxes_and_charges_template(company_name, doctype, template):
'charge_type': 'On Net Total'
}
if doctype == 'Purchase Taxes and Charges Template':
tax_row_defaults['add_deduct_tax'] = 'Add'
# if account_head is a dict, search or create the account and get it's name
if isinstance(account_data, dict):
tax_row_defaults['description'] = '{0} @ {1}'.format(account_data.get('account_name'), account_data.get('tax_rate'))

View File

@ -7,7 +7,7 @@ import frappe
from frappe.utils import nowdate, add_months
from erpnext.shopping_cart.cart import _get_cart_quotation, update_cart, get_party
from erpnext.tests.utils import create_test_contact_and_address
from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule
# test_dependencies = ['Payment Terms Template']
@ -125,7 +125,7 @@ class TestShoppingCart(unittest.TestCase):
tax_rule = frappe.get_test_records("Tax Rule")[0]
try:
frappe.get_doc(tax_rule).insert()
except frappe.DuplicateEntryError:
except (frappe.DuplicateEntryError, ConflictingTaxRule):
pass
def create_quotation(self):

View File

@ -13,6 +13,7 @@
"section_break_6",
"warehouse",
"target_warehouse",
"conversion_factor",
"column_break_9",
"qty",
"uom",
@ -209,13 +210,18 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "conversion_factor",
"fieldtype": "Float",
"label": "Conversion Factor"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-09-24 09:25:13.050151",
"modified": "2021-05-26 07:08:05.111385",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packed Item",

View File

@ -53,6 +53,7 @@ def update_packing_list_item(doc, packing_item_code, qty, main_item_row, descrip
pi.parent_detail_docname = main_item_row.name
pi.uom = item.stock_uom
pi.qty = flt(qty)
pi.conversion_factor = main_item_row.conversion_factor
if description and not pi.description:
pi.description = description
if not pi.warehouse and not doc.amended_from:

View File

@ -297,6 +297,8 @@ class TestPurchaseReceipt(unittest.TestCase):
item_code = "Test Extra Item 1", qty=10, basic_rate=100)
se2 = make_stock_entry(target="_Test Warehouse - _TC",
item_code = "_Test FG Item", qty=1, basic_rate=100)
se3 = make_stock_entry(target="_Test Warehouse - _TC",
item_code = "Test Extra Item 2", qty=1, basic_rate=100)
rm_items = [
{
"item_code": item_code,
@ -331,6 +333,7 @@ class TestPurchaseReceipt(unittest.TestCase):
se.cancel()
se1.cancel()
se2.cancel()
se3.cancel()
po.reload()
po.cancel()

View File

@ -15,10 +15,12 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import create_landed_cost_voucher
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import BackDatedStockTransaction
from frappe.core.page.permission_manager.permission_manager import reset
class TestStockLedgerEntry(unittest.TestCase):
def setUp(self):
items = create_items()
reset('Stock Entry')
# delete SLE and BINs for all items
frappe.db.sql("delete from `tabStock Ledger Entry` where item_code in (%s)" % (', '.join(['%s']*len(items))), items)
@ -314,10 +316,11 @@ class TestStockLedgerEntry(unittest.TestCase):
# Set User with Stock User role but not Stock Manager
try:
user = frappe.get_doc("User", "test@example.com")
frappe.set_user(user.name)
user.add_roles("Stock User")
user.remove_roles("Stock Manager")
frappe.set_user(user.name)
stock_entry_on_today = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100)
back_dated_se_1 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100,
posting_date=add_days(today(), -1), do_not_submit=True)

View File

@ -477,19 +477,19 @@ class StockReconciliation(StockController):
def get_items(warehouse, posting_date, posting_time, company):
lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
items = frappe.db.sql("""
select i.name, i.item_name, bin.warehouse
select i.name, i.item_name, bin.warehouse, i.has_serial_no
from tabBin bin, tabItem i
where i.name=bin.item_code and i.disabled=0 and i.is_stock_item = 1
and i.has_variants = 0 and i.has_serial_no = 0 and i.has_batch_no = 0
and i.has_variants = 0 and i.has_batch_no = 0
and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=bin.warehouse)
""", (lft, rgt))
items += frappe.db.sql("""
select i.name, i.item_name, id.default_warehouse
select i.name, i.item_name, id.default_warehouse, i.has_serial_no
from tabItem i, `tabItem Default` id
where i.name = id.parent
and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse)
and i.is_stock_item = 1 and i.has_serial_no = 0 and i.has_batch_no = 0
and i.is_stock_item = 1 and i.has_batch_no = 0
and i.has_variants = 0 and i.disabled = 0 and id.company=%s
group by i.name
""", (lft, rgt, company))
@ -497,7 +497,7 @@ def get_items(warehouse, posting_date, posting_time, company):
res = []
for d in set(items):
stock_bal = get_stock_balance(d[0], d[2], posting_date, posting_time,
with_valuation_rate=True)
with_valuation_rate=True , with_serial_no=cint(d[3]))
if frappe.db.get_value("Item", d[0], "disabled") == 0:
res.append({
@ -507,7 +507,9 @@ def get_items(warehouse, posting_date, posting_time, company):
"item_name": d[1],
"valuation_rate": stock_bal[1],
"current_qty": stock_bal[0],
"current_valuation_rate": stock_bal[1]
"current_valuation_rate": stock_bal[1],
"current_serial_no": stock_bal[2] if cint(d[3]) else '',
"serial_no": stock_bal[2] if cint(d[3]) else ''
})
return res

View File

@ -194,9 +194,6 @@ def set_stock_balance_as_per_serial_no(item_code=None, posting_date=None, postin
serial_nos = frappe.db.sql("""select count(name) from `tabSerial No`
where item_code=%s and warehouse=%s and docstatus < 2""", (d[0], d[1]))
if serial_nos and flt(serial_nos[0][0]) != flt(d[2]):
print(d[0], d[1], d[2], serial_nos[0][0])
sle = frappe.db.sql("""select valuation_rate, company from `tabStock Ledger Entry`
where item_code = %s and warehouse = %s and is_cancelled = 0
order by posting_date desc limit 1""", (d[0], d[1]))
@ -230,7 +227,7 @@ def set_stock_balance_as_per_serial_no(item_code=None, posting_date=None, postin
})
update_bin(args)
create_repost_item_valuation_entry({
"item_code": d[0],
"warehouse": d[1],

View File

@ -119,7 +119,7 @@
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
"options": "Open\nReplied\nHold\nResolved\nClosed",
"options": "Open\nReplied\nOn Hold\nResolved\nClosed",
"search_index": 1
},
{
@ -410,7 +410,7 @@
"icon": "fa fa-ticket",
"idx": 7,
"links": [],
"modified": "2020-08-11 18:49:07.574769",
"modified": "2021-05-26 10:49:07.574769",
"modified_by": "Administrator",
"module": "Support",
"name": "Issue",

View File

@ -42,6 +42,7 @@ frappe.query_reports["Issue Summary"] = {
"",
{label: __('Open'), value: 'Open'},
{label: __('Replied'), value: 'Replied'},
{label: __('On Hold'), value: 'On Hold'},
{label: __('Resolved'), value: 'Resolved'},
{label: __('Closed'), value: 'Closed'}
]

View File

@ -62,7 +62,7 @@ class IssueSummary(object):
'width': 200
})
self.statuses = ['Open', 'Replied', 'Resolved', 'Closed']
self.statuses = ['Open', 'Replied', 'On Hold', 'Resolved', 'Closed']
for status in self.statuses:
self.columns.append({
'label': _(status),
@ -265,6 +265,7 @@ class IssueSummary(object):
labels = []
open_issues = []
replied_issues = []
on_hold_issues = []
resolved_issues = []
closed_issues = []
@ -277,6 +278,7 @@ class IssueSummary(object):
labels.append(entry.get(entity_field))
open_issues.append(entry.get('open'))
replied_issues.append(entry.get('replied'))
on_hold_issues.append(entry.get('on_hold'))
resolved_issues.append(entry.get('resolved'))
closed_issues.append(entry.get('closed'))
@ -292,6 +294,10 @@ class IssueSummary(object):
'name': 'Replied',
'values': replied_issues[:30]
},
{
'name': 'On Hold',
'values': on_hold_issues[:30]
},
{
'name': 'Resolved',
'values': resolved_issues[:30]
@ -313,12 +319,14 @@ class IssueSummary(object):
open_issues = 0
replied = 0
on_hold = 0
resolved = 0
closed = 0
for entry in self.data:
open_issues += entry.get('open')
replied += entry.get('replied')
on_hold += entry.get('on_hold')
resolved += entry.get('resolved')
closed += entry.get('closed')
@ -335,6 +343,12 @@ class IssueSummary(object):
'label': _('Replied'),
'datatype': 'Int',
},
{
'value': on_hold,
'indicator': 'Grey',
'label': _('On Hold'),
'datatype': 'Int',
},
{
'value': resolved,
'indicator': 'Green',

View File

@ -0,0 +1 @@
global_test_dependencies = ['User', 'Company', 'Item']

View File

@ -12,7 +12,6 @@ def update_doctypes():
for f in dt.fields:
if f.fieldname == d.fieldname and f.fieldtype in ("Text", "Small Text"):
print(f.parent, f.fieldname)
f.fieldtype = "Text Editor"
dt.save()
break