Merge branch 'frappe:develop' into develop
This commit is contained in:
commit
2b8b97fcc9
114 changed files with 2794 additions and 1053 deletions
|
|
@ -5,7 +5,7 @@
|
|||
"es6": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 9,
|
||||
"ecmaVersion": 11,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
|
|
|
|||
7
.github/helper/install_dependencies.sh
vendored
7
.github/helper/install_dependencies.sh
vendored
|
|
@ -2,6 +2,13 @@
|
|||
|
||||
set -e
|
||||
|
||||
# Check for merge conflicts before proceeding
|
||||
python -m compileall -f "${GITHUB_WORKSPACE}"
|
||||
if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}"
|
||||
then echo "Found merge conflicts"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# install wkhtmltopdf
|
||||
wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
|
||||
tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
|
||||
|
|
|
|||
4
.github/try-on-f-cloud-button.svg
vendored
4
.github/try-on-f-cloud-button.svg
vendored
|
|
@ -1,4 +1,4 @@
|
|||
<svg width="201" height="60" viewBox="0 0 201 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="4 2 193 52">
|
||||
<g filter="url(#filter0_dd)">
|
||||
<rect x="4" y="2" width="193" height="52" rx="6" fill="#2490EF"/>
|
||||
<path d="M28 22.2891H32.8786V35.5H36.2088V22.2891H41.0874V19.5H28V22.2891Z" fill="white"/>
|
||||
|
|
@ -29,4 +29,4 @@
|
|||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.3 KiB |
1
.github/workflows/release.yml
vendored
1
.github/workflows/release.yml
vendored
|
|
@ -22,7 +22,6 @@ jobs:
|
|||
npm install @semantic-release/git @semantic-release/exec --no-save
|
||||
- name: Create Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
GIT_AUTHOR_NAME: "Frappe PR Bot"
|
||||
|
|
|
|||
38
README.md
38
README.md
|
|
@ -1,16 +1,16 @@
|
|||
<div align="center">
|
||||
<h1>
|
||||
<br>
|
||||
<a href="https://frappeframework.com">
|
||||
<img src=".github/frappe-framework-logo.svg" height="50">
|
||||
</a>
|
||||
</h1>
|
||||
<h3>
|
||||
a web framework with <a href="https://www.youtube.com/watch?v=LOjk3m0wTwg">"batteries included"</a>
|
||||
</h3>
|
||||
<h5>
|
||||
it's pronounced - <em>fra-pay</em>
|
||||
</h5>
|
||||
<h1>
|
||||
<br>
|
||||
<a href="https://frappeframework.com">
|
||||
<img src=".github/frappe-framework-logo.svg" height="50">
|
||||
</a>
|
||||
</h1>
|
||||
<h3>
|
||||
a web framework with <a href="https://www.youtube.com/watch?v=LOjk3m0wTwg">"batteries included"</a>
|
||||
</h3>
|
||||
<h5>
|
||||
it's pronounced - <em>fra-pay</em>
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
|
@ -27,20 +27,24 @@
|
|||
<img src='https://www.codetriage.com/frappe/frappe/badges/users.svg'>
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/frappe/frappe">
|
||||
<img src="https://codecov.io/gh/frappe/frappe/branch/develop/graph/badge.svg?token=XoTa679hIj"/>
|
||||
<img src="https://codecov.io/gh/frappe/frappe/branch/develop/graph/badge.svg?token=XoTa679hIj"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
Full-stack web application framework that uses Python and MariaDB on the server side and a tightly integrated client side library. Built for [ERPNext](https://erpnext.com)
|
||||
|
||||
<div align="center">
|
||||
<a href="https://frappecloud.com/deploy?apps=frappe&source=frappe_readme">
|
||||
<div align="center" style="max-height: 40px;">
|
||||
<a href="https://frappecloud.com/frappe/signup">
|
||||
<img src=".github/try-on-f-cloud-button.svg" height="40">
|
||||
</a>
|
||||
<a href="https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/gavindsouza/install-scripts/main/frappe/pwd.yml">
|
||||
<img src="https://raw.githubusercontent.com/play-with-docker/stacks/master/assets/images/button.png" alt="Try in PWD" height="37"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
> Login for the PWD site: (username: Administrator, password: admin)
|
||||
|
||||
## Table of Contents
|
||||
* [Installation](#installation)
|
||||
* [Contributing](#contributing)
|
||||
|
|
@ -52,7 +56,7 @@ Full-stack web application framework that uses Python and MariaDB on the server
|
|||
* [Install via Docker](https://github.com/frappe/frappe_docker)
|
||||
* [Install via Frappe Bench](https://github.com/frappe/bench)
|
||||
* [Offical Documentation](https://frappeframework.com/docs/user/en/installation)
|
||||
* [Managed Hosting on Frappe Cloud](https://frappecloud.com/deploy?apps=frappe&source=frappe_readme)
|
||||
* [Managed Hosting on Frappe Cloud](https://frappecloud.com/frappe/signup)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
|||
47
cypress/fixtures/doctype_with_phone.js
Normal file
47
cypress/fixtures/doctype_with_phone.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
export default {
|
||||
name: "Doctype With Phone",
|
||||
actions: [],
|
||||
custom: 1,
|
||||
is_submittable: 1,
|
||||
autoname: "field:title",
|
||||
creation: '2022-03-30 06:29:07.215072',
|
||||
doctype: 'DocType',
|
||||
engine: 'InnoDB',
|
||||
fields: [
|
||||
|
||||
{
|
||||
fieldname: 'title',
|
||||
fieldtype: 'Data',
|
||||
label: 'title',
|
||||
unique: 1,
|
||||
},
|
||||
{
|
||||
fieldname: 'phone',
|
||||
fieldtype: 'Phone',
|
||||
label: 'Phone'
|
||||
}
|
||||
],
|
||||
links: [],
|
||||
modified: '2019-03-30 14:40:53.127615',
|
||||
modified_by: 'Administrator',
|
||||
naming_rule: "By fieldname",
|
||||
module: 'Custom',
|
||||
owner: 'Administrator',
|
||||
permissions: [
|
||||
{
|
||||
create: 1,
|
||||
delete: 1,
|
||||
email: 1,
|
||||
print: 1,
|
||||
read: 1,
|
||||
role: 'System Manager',
|
||||
share: 1,
|
||||
write: 1,
|
||||
submit: 1,
|
||||
cancel: 1
|
||||
}
|
||||
],
|
||||
sort_field: 'modified',
|
||||
sort_order: 'ASC',
|
||||
track_changes: 1
|
||||
};
|
||||
129
cypress/integration/control_data.js
Normal file
129
cypress/integration/control_data.js
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
context('Data Control', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/doctype');
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.xcall('frappe.tests.ui_test_helpers.create_doctype', {
|
||||
name: 'Test Data Control',
|
||||
fields: [
|
||||
{
|
||||
"label": "Name",
|
||||
"fieldname": "name1",
|
||||
"fieldtype": "Data",
|
||||
"options": "Name",
|
||||
"in_list_view": 1,
|
||||
"reqd": 1,
|
||||
},
|
||||
{
|
||||
"label": "Email-ID",
|
||||
"fieldname": "email",
|
||||
"fieldtype": "Data",
|
||||
"options": "Email",
|
||||
"in_list_view": 1,
|
||||
"reqd": 1,
|
||||
},
|
||||
{
|
||||
"label": "Phone No.",
|
||||
"fieldname": "phone",
|
||||
"fieldtype": "Data",
|
||||
"options": "Phone",
|
||||
"in_list_view": 1,
|
||||
"reqd": 1,
|
||||
},
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
it('Verifying data control by inputting different patterns for "Name" field', () => {
|
||||
cy.new_form('Test Data Control');
|
||||
|
||||
//Checking the URL for the new form of the doctype
|
||||
cy.location("pathname").should('eq', '/app/test-data-control/new-test-data-control-1');
|
||||
cy.get('.title-text').should('have.text', 'New Test Data Control');
|
||||
cy.get('.frappe-control[data-fieldname="name1"]').find('label').should('have.class', 'reqd');
|
||||
cy.get('.frappe-control[data-fieldname="email"]').find('label').should('have.class', 'reqd');
|
||||
cy.get('.frappe-control[data-fieldname="phone"]').find('label').should('have.class', 'reqd');
|
||||
|
||||
//Checking if the status is "Not Saved" initially
|
||||
cy.get('.indicator-pill').should('have.text', 'Not Saved');
|
||||
|
||||
//Inputting data in the field
|
||||
cy.fill_field('name1', '@@###', 'Data');
|
||||
cy.fill_field('email', 'test@example.com', 'Data');
|
||||
cy.fill_field('phone', '9834280031', 'Data');
|
||||
|
||||
//Checking if the border color of the field changes to red
|
||||
cy.get('.frappe-control[data-fieldname="name1"]').should('have.class', 'has-error');
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
|
||||
//Checking for the error message
|
||||
cy.get('.modal-title').should('have.text', 'Message');
|
||||
cy.get('.msgprint').should('have.text', '@@### is not a valid Name');
|
||||
cy.get('.modal').type('{esc}');
|
||||
|
||||
cy.get_field('name1', 'Data').clear({force: true});
|
||||
cy.fill_field('name1', 'Komal{}/!', 'Data');
|
||||
cy.get('.frappe-control[data-fieldname="name1"]').should('have.class', 'has-error');
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
cy.get('.modal-title').should('have.text', 'Message');
|
||||
cy.get('.msgprint').should('have.text', 'Komal{}/! is not a valid Name');
|
||||
});
|
||||
|
||||
it('Verifying data control by inputting different patterns for "Email" field', () => {
|
||||
cy.get('.modal-actions > .btn-modal-close').trigger("click");
|
||||
cy.get_field('name1', 'Data').clear({force: true});
|
||||
cy.fill_field('name1', 'Komal', 'Data');
|
||||
cy.get_field('email', 'Data').clear({force: true});
|
||||
cy.fill_field('email', 'komal', 'Data');
|
||||
cy.get('.frappe-control[data-fieldname="email"]').should('have.class', 'has-error');
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
cy.get('.modal-title').should('have.text', 'Message');
|
||||
cy.get('.msgprint').should('have.text', 'komal is not a valid Email Address');
|
||||
cy.get('.modal-actions > .btn-modal-close').trigger("click");
|
||||
cy.get_field('email', 'Data').clear({force: true});
|
||||
cy.fill_field('email', 'komal@test', 'Data');
|
||||
cy.get('.frappe-control[data-fieldname="email"]').should('have.class', 'has-error');
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
cy.get('.modal-title').should('have.text', 'Message');
|
||||
cy.get('.msgprint').should('have.text', 'komal@test is not a valid Email Address');
|
||||
});
|
||||
|
||||
it('Verifying data control by inputting different patterns for "Phone" field', () => {
|
||||
cy.get('.modal-actions > .btn-modal-close').trigger("click");
|
||||
cy.get_field('email', 'Data').clear({force: true});
|
||||
cy.fill_field('email', 'komal@test.com', 'Data');
|
||||
cy.get_field('phone', 'Data').clear({force: true});
|
||||
cy.fill_field('phone', 'komal', 'Data');
|
||||
cy.get('.frappe-control[data-fieldname="phone"]').should('have.class', 'has-error');
|
||||
cy.findByRole('button', {name: 'Save'}).click({force: true});
|
||||
cy.get('.modal-title').should('have.text', 'Message');
|
||||
cy.get('.msgprint').should('have.text', 'komal is not a valid Phone Number');
|
||||
cy.get('.modal-actions > .btn-modal-close').trigger("click");
|
||||
});
|
||||
|
||||
it('Inputting correct data and saving the doc', () => {
|
||||
//Inputting the data as expected and saving the document
|
||||
cy.get_field('name1', 'Data').clear({force: true});
|
||||
cy.get_field('email', 'Data').clear({force: true});
|
||||
cy.get_field('phone', 'Data').clear({force: true});
|
||||
cy.fill_field('name1', 'Komal', 'Data');
|
||||
cy.fill_field('email', 'komal@test.com', 'Data');
|
||||
cy.fill_field('phone', '9432380001', 'Data');
|
||||
cy.findByRole('button', {name: 'Save'}).click({force: true});
|
||||
//Checking if the fields contains the data which has been filled in
|
||||
cy.location("pathname").should('not.be', '/app/test-data-control/new-test-data-control-1');
|
||||
cy.get_field('name1').should('have.value', 'Komal');
|
||||
cy.get_field('email').should('have.value', 'komal@test.com');
|
||||
cy.get_field('phone').should('have.value', '9432380001');
|
||||
});
|
||||
|
||||
it('Deleting the doc', () => {
|
||||
//Deleting the inserted document
|
||||
cy.go_to_list('Test Data Control');
|
||||
cy.get('.list-row-checkbox').eq(0).click({force: true});
|
||||
cy.get('.actions-btn-group > .btn').contains('Actions').click();
|
||||
cy.get('.actions-btn-group > .dropdown-menu [data-label="Delete"]').click();
|
||||
cy.click_modal_primary_button('Yes');
|
||||
cy.get('.btn-modal-close').click();
|
||||
});
|
||||
});
|
||||
|
|
@ -62,8 +62,8 @@ context('Dynamic Link', () => {
|
|||
"label": "Document ID",
|
||||
"fieldname": "doc_id",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"get_options": () => {
|
||||
return "User";
|
||||
"get_options": () => {
|
||||
return "User";
|
||||
},
|
||||
"in_list_view": 1,
|
||||
}]
|
||||
|
|
@ -118,11 +118,16 @@ context('Dynamic Link', () => {
|
|||
cy.get_field('doc_type').clear();
|
||||
|
||||
//Entering System Settings in the Doctype field
|
||||
cy.intercept('/api/method/frappe.desk.search.search_link').as('search_query');
|
||||
cy.fill_field('doc_type', 'System Settings', 'Link', {delay: 500});
|
||||
cy.wait('@search_query');
|
||||
cy.get(`[data-fieldname="doc_type"] ul:visible li:first-child`)
|
||||
.click({scrollBehavior: false});
|
||||
|
||||
cy.get_field('doc_id').click();
|
||||
|
||||
//Checking if the system throws error
|
||||
cy.get('.modal-title').should('have.text', 'Error');
|
||||
cy.get('.msgprint').should('have.text', 'System Settings is not a valid DocType for Dynamic Link');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ context("Control Markdown Editor", () => {
|
|||
cy.click_modal_primary_button("Upload");
|
||||
cy.get_field("main_section_md", "Markdown Editor").should(
|
||||
"contain",
|
||||
""
|
||||
";
|
||||
});
|
||||
});
|
||||
|
|
|
|||
90
cypress/integration/control_phone.js
Normal file
90
cypress/integration/control_phone.js
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import doctype_with_phone from '../fixtures/doctype_with_phone';
|
||||
|
||||
context("Control Phone", () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit("/app/website");
|
||||
});
|
||||
|
||||
function get_dialog_with_phone() {
|
||||
return cy.dialog({
|
||||
title: "Phone",
|
||||
fields: [{
|
||||
"fieldname": "phone",
|
||||
"fieldtype": "Phone",
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
it("should set flag and data", () => {
|
||||
get_dialog_with_phone().as("dialog");
|
||||
cy.get(".selected-phone").click();
|
||||
cy.get(".phone-picker .phone-wrapper[id='afghanistan']").click();
|
||||
cy.get(".selected-phone").click();
|
||||
cy.get(".phone-picker .phone-wrapper[id='india']").click();
|
||||
cy.get(".selected-phone .country").should("have.text", "+91");
|
||||
cy.get(".selected-phone > img").should("have.attr", "src").and("include", "/in.svg");
|
||||
|
||||
let phone_number = "9312672712";
|
||||
cy.get(".selected-phone > img").click().first();
|
||||
cy.get_field("phone")
|
||||
.first()
|
||||
.click({multiple: true});
|
||||
cy.get(".frappe-control[data-fieldname=phone]")
|
||||
.findByRole("textbox")
|
||||
.first()
|
||||
.type(phone_number, {force: true});
|
||||
|
||||
cy.get_field("phone").first().should("have.value", phone_number);
|
||||
cy.get_field("phone").first().blur({force: true});
|
||||
cy.wait(100);
|
||||
cy.get("@dialog").then(dialog => {
|
||||
let value = dialog.get_value("phone");
|
||||
expect(value).to.equal("+91-" + phone_number);
|
||||
});
|
||||
});
|
||||
|
||||
it("case insensitive search for country and clear search", () => {
|
||||
let search_text = "india";
|
||||
cy.get(".selected-phone").click().first();
|
||||
cy.get(".phone-picker").findByRole("searchbox").click().type(search_text);
|
||||
cy.get(".phone-section .phone-wrapper:not(.hidden)").then(i => {
|
||||
cy.get(`.phone-section .phone-wrapper[id*="${search_text.toLowerCase()}"]`).then(countries => {
|
||||
expect(i.length).to.equal(countries.length);
|
||||
});
|
||||
});
|
||||
|
||||
cy.get(".phone-picker").findByRole("searchbox").clear().blur();
|
||||
cy.get(".phone-section .phone-wrapper").should("not.have.class", "hidden");
|
||||
});
|
||||
|
||||
it("existing document should render phone field with data", () => {
|
||||
cy.visit("/app/doctype");
|
||||
cy.insert_doc("DocType", doctype_with_phone, true);
|
||||
cy.clear_cache();
|
||||
|
||||
// Creating custom doctype
|
||||
cy.insert_doc("DocType", doctype_with_phone, true);
|
||||
cy.visit("/app/doctype-with-phone");
|
||||
cy.click_listview_primary_button("Add Doctype With Phone");
|
||||
|
||||
// create a record
|
||||
cy.fill_field("title", "Test Phone 1");
|
||||
cy.fill_field("phone", "+91-9823341234");
|
||||
cy.get_field("phone").should("have.value", "9823341234");
|
||||
cy.click_doc_primary_button("Save");
|
||||
cy.get_doc("Doctype With Phone", "Test Phone 1").then((doc) => {
|
||||
let value = doc.data.phone;
|
||||
expect(value).to.equal("+91-9823341234");
|
||||
});
|
||||
|
||||
// open the doc from list view
|
||||
cy.go_to_list("Doctype With Phone");
|
||||
cy.clear_cache();
|
||||
cy.click_listview_row_item(0);
|
||||
cy.title().should("eq", "Test Phone 1");
|
||||
cy.get(".selected-phone .country").should("have.text", "+91");
|
||||
cy.get(".selected-phone > img").should("have.attr", "src").and("include", "/in.svg");
|
||||
cy.get_field("phone").should("have.value", "9823341234");
|
||||
});
|
||||
});
|
||||
22
cypress/integration/customize_form.js
Normal file
22
cypress/integration/customize_form.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
context('Customize Form', () => {
|
||||
before(() => {
|
||||
cy.visit('/app/customize-form');
|
||||
});
|
||||
it('Changing to naming rule should update autoname', () => {
|
||||
cy.fill_field("doc_type", "ToDo", "Link").blur();
|
||||
cy.click_form_section("Naming");
|
||||
const naming_rule_default_autoname_map = {
|
||||
"Set by user": "prompt",
|
||||
"By fieldname": "field:",
|
||||
'By "Naming Series" field': "naming_series:",
|
||||
"Expression": "format:",
|
||||
"Expression (old style)": "",
|
||||
"Random": "hash",
|
||||
"By script": ""
|
||||
};
|
||||
Cypress._.forOwn(naming_rule_default_autoname_map, (value, naming_rule) => {
|
||||
cy.fill_field("naming_rule", naming_rule, "Select");
|
||||
cy.get_field("autoname", "Data").should("have.value", value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -27,7 +27,7 @@ context('Form', () => {
|
|||
|
||||
cy.clear_filters();
|
||||
cy.get('.standard-filter-section [data-fieldname="name"] input').type('Test Form Contact 3').blur();
|
||||
cy.click_listview_row_item(0);
|
||||
cy.click_listview_row_item_with_text('Test Form Contact 3');
|
||||
|
||||
cy.get('#page-Contact .page-head').findByTitle('Test Form Contact 3').should('exist');
|
||||
cy.get('.prev-doc').should('be.visible').click();
|
||||
|
|
|
|||
|
|
@ -72,14 +72,16 @@ context('Kanban Board', () => {
|
|||
|
||||
});
|
||||
|
||||
it('Drag todo', () => {
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: 'api/method/frappe.desk.doctype.kanban_board.kanban_board.update_order_for_single_card'
|
||||
}).as('drag-completed');
|
||||
// it('Drag todo', () => {
|
||||
// cy.intercept({
|
||||
// method: 'POST',
|
||||
// url: 'api/method/frappe.desk.doctype.kanban_board.kanban_board.update_order_for_single_card'
|
||||
// }).as('drag-completed');
|
||||
|
||||
cy.get('.kanban-card-body:first').drag('[data-column-value="Closed"] .kanban-cards', {force: true});
|
||||
// cy.get('.kanban-card-body')
|
||||
// .contains('Test Kanban ToDo').first()
|
||||
// .drag('[data-column-value="Closed"] .kanban-cards', { force: true });
|
||||
|
||||
cy.wait('@drag-completed');
|
||||
});
|
||||
// cy.wait('@drag-completed');
|
||||
// });
|
||||
});
|
||||
|
|
@ -16,7 +16,7 @@ context('Timeline Email', () => {
|
|||
|
||||
it('Adding email and verifying timeline content for email attachment', () => {
|
||||
cy.visit('/app/todo');
|
||||
cy.get('.list-row > .level-left > .list-subject').eq(0).click();
|
||||
cy.click_listview_row_item_with_text('Test ToDo');
|
||||
|
||||
//Creating a new email
|
||||
cy.get('.timeline-actions > .timeline-item > .action-buttons > .action-btn').click();
|
||||
|
|
@ -47,7 +47,7 @@ context('Timeline Email', () => {
|
|||
|
||||
it('Deleting attachment and ToDo', () => {
|
||||
cy.visit('/app/todo');
|
||||
cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click();
|
||||
cy.click_listview_row_item_with_text('Test ToDo');
|
||||
|
||||
//Removing the added attachment
|
||||
cy.get('.attachment-row > .data-pill > .remove-btn > .icon').click();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ context('Workspace 2.0', () => {
|
|||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
it('Navigate to page from sidebar', () => {
|
||||
|
|
@ -13,6 +12,11 @@ context('Workspace 2.0', () => {
|
|||
});
|
||||
|
||||
it('Create Private Page', () => {
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: 'api/method/frappe.desk.doctype.workspace.workspace.new_page'
|
||||
}).as('new_page');
|
||||
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.custom-actions button[data-label="Create%20Workspace"]').click();
|
||||
cy.fill_field('title', 'Test Private Page', 'Data');
|
||||
|
|
@ -27,12 +31,100 @@ context('Workspace 2.0', () => {
|
|||
cy.wait(300);
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('have.attr', 'item-public', '0');
|
||||
|
||||
cy.wait(500);
|
||||
cy.wait('@new_page');
|
||||
});
|
||||
|
||||
it('Create Child Page', () => {
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: 'api/method/frappe.desk.doctype.workspace.workspace.new_page'
|
||||
}).as('new_page');
|
||||
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.custom-actions button[data-label="Create%20Workspace"]').click();
|
||||
cy.fill_field('title', 'Test Child Page', 'Data');
|
||||
cy.fill_field('parent', 'Test Private Page', 'Select');
|
||||
cy.fill_field('icon', 'edit', 'Icon');
|
||||
cy.get_open_dialog().find('.modal-header').click();
|
||||
cy.get_open_dialog().find('.btn-primary').click();
|
||||
|
||||
// check if sidebar item is added in pubic section
|
||||
cy.get('.sidebar-item-container[item-name="Test Child Page"]').should('have.attr', 'item-public', '0');
|
||||
|
||||
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
|
||||
cy.wait(300);
|
||||
cy.get('.sidebar-item-container[item-name="Test Child Page"]').should('have.attr', 'item-public', '0');
|
||||
|
||||
cy.wait('@new_page');
|
||||
});
|
||||
|
||||
it('Duplicate Page', () => {
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: 'api/method/frappe.desk.doctype.workspace.workspace.duplicate_page'
|
||||
}).as('page_duplicated');
|
||||
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.standard-actions .btn-secondary[data-label=Edit]').click();
|
||||
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').as('sidebar-item');
|
||||
|
||||
cy.get('@sidebar-item').find('.standard-sidebar-item').first().click();
|
||||
cy.get('@sidebar-item').find('.dropdown-btn').first().click();
|
||||
cy.get('@sidebar-item').find('.dropdown-list .dropdown-item').contains('Duplicate').first().click({force: true});
|
||||
|
||||
cy.get_open_dialog().fill_field('title', 'Duplicate Page', 'Data');
|
||||
cy.click_modal_primary_button('Duplicate');
|
||||
|
||||
cy.wait('@page_duplicated');
|
||||
});
|
||||
|
||||
it('Drag Sidebar Item', () => {
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: 'api/method/frappe.desk.doctype.workspace.workspace.sort_pages'
|
||||
}).as('page_sorted');
|
||||
|
||||
cy.get('.sidebar-item-container[item-name="Duplicate Page"]').as('sidebar-item');
|
||||
|
||||
cy.get('@sidebar-item').find('.standard-sidebar-item').first().click();
|
||||
cy.get('@sidebar-item').find('.drag-handle').first().move({ deltaX: 0, deltaY: 100 });
|
||||
|
||||
cy.get('.sidebar-item-container[item-name="Build"]').as('sidebar-item');
|
||||
|
||||
cy.get('@sidebar-item').find('.standard-sidebar-item').first().click();
|
||||
cy.get('@sidebar-item').find('.drag-handle').first().move({ deltaX: 0, deltaY: 100 });
|
||||
|
||||
cy.wait('@page_sorted');
|
||||
});
|
||||
|
||||
it('Edit Page Detail', () => {
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: 'api/method/frappe.desk.doctype.workspace.workspace.update_page'
|
||||
}).as('page_updated');
|
||||
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').as('sidebar-item');
|
||||
|
||||
cy.get('@sidebar-item').find('.standard-sidebar-item').first().click();
|
||||
cy.get('@sidebar-item').find('.dropdown-btn').first().click();
|
||||
cy.get('@sidebar-item').find('.dropdown-list .dropdown-item').contains('Edit').first().click({force: true});
|
||||
|
||||
cy.get_open_dialog().fill_field('title', ' 1', 'Data');
|
||||
cy.get_open_dialog().find('input[data-fieldname="is_public"]').check();
|
||||
cy.click_modal_primary_button('Update');
|
||||
|
||||
cy.get('.standard-sidebar-section:first .sidebar-item-container[item-name="Test Private Page"]').should('not.exist');
|
||||
cy.get('.standard-sidebar-section:last .sidebar-item-container[item-name="Test Private Page 1"]').should('exist');
|
||||
|
||||
cy.wait('@page_updated');
|
||||
});
|
||||
|
||||
it('Add New Block', () => {
|
||||
cy.get('.sidebar-item-container[item-name="Duplicate Page"]').as('sidebar-item');
|
||||
|
||||
cy.get('@sidebar-item').find('.standard-sidebar-item').first().click();
|
||||
|
||||
cy.get('.ce-block').click().type('{enter}');
|
||||
cy.get('.block-list-container .block-list-item').contains('Heading').click();
|
||||
cy.get(":focus").type('Header');
|
||||
|
|
@ -70,19 +162,24 @@ context('Workspace 2.0', () => {
|
|||
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
|
||||
});
|
||||
|
||||
it('Delete Private Page', () => {
|
||||
it('Delete Duplicate Page', () => {
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: 'api/method/frappe.desk.doctype.workspace.workspace.delete_page'
|
||||
}).as('page_deleted');
|
||||
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.standard-actions .btn-secondary[data-label=Edit]').click();
|
||||
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]')
|
||||
cy.get('.sidebar-item-container[item-name="Duplicate Page"]')
|
||||
.find('.sidebar-item-control .setting-btn').click();
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]')
|
||||
cy.get('.sidebar-item-container[item-name="Duplicate Page"]')
|
||||
.find('.dropdown-item[title="Delete Workspace"]').click({force: true});
|
||||
cy.wait(300);
|
||||
cy.get('.modal-footer > .standard-actions > .btn-modal-primary:visible').first().click();
|
||||
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('not.exist');
|
||||
cy.get('.sidebar-item-container[item-name="Duplicate Page"]').should('not.exist');
|
||||
|
||||
cy.wait('@page_deleted');
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -352,6 +352,13 @@ Cypress.Commands.add('click_listview_row_item', (row_no) => {
|
|||
cy.get('.list-row > .level-left > .list-subject > .level-item > .ellipsis').eq(row_no).click({force: true});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_listview_row_item_with_text', (text) => {
|
||||
cy.get('.list-row > .level-left > .list-subject > .level-item > .ellipsis')
|
||||
.contains(text)
|
||||
.first()
|
||||
.click({force: true});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_filter_button', () => {
|
||||
cy.get('.filter-selector > .btn').click();
|
||||
});
|
||||
|
|
@ -360,6 +367,10 @@ Cypress.Commands.add('click_listview_primary_button', (btn_name) => {
|
|||
cy.get('.primary-action').contains(btn_name).click({force: true});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_doc_primary_button', (btn_name) => {
|
||||
cy.get('.primary-action').contains(btn_name).click({force: true});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_timeline_action_btn', (btn_name) => {
|
||||
cy.get('.timeline-message-box .actions .action-btn').contains(btn_name).click();
|
||||
});
|
||||
|
|
@ -367,3 +378,7 @@ Cypress.Commands.add('click_timeline_action_btn', (btn_name) => {
|
|||
Cypress.Commands.add('select_listview_row_checkbox', (row_no) => {
|
||||
cy.get('.frappe-list .select-like > .list-row-checkbox').eq(row_no).click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_form_section', (section_name) => {
|
||||
cy.get('.section-head').contains(section_name).click();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -199,6 +199,7 @@ def init(site, sites_path=None, new_site=False):
|
|||
}
|
||||
)
|
||||
local.rollback_observers = []
|
||||
local.locked_documents = []
|
||||
local.before_commit = []
|
||||
local.test_objects = {}
|
||||
|
||||
|
|
@ -231,7 +232,6 @@ def init(site, sites_path=None, new_site=False):
|
|||
local.cache = {}
|
||||
local.document_cache = {}
|
||||
local.meta_cache = {}
|
||||
local.autoincremented_status_map = {site: -1}
|
||||
local.form_dict = _dict()
|
||||
local.session = _dict()
|
||||
local.dev_server = _dev_server
|
||||
|
|
@ -354,11 +354,11 @@ def cache() -> "RedisWrapper":
|
|||
return redis_server
|
||||
|
||||
|
||||
def get_traceback():
|
||||
def get_traceback(with_context=False):
|
||||
"""Returns error traceback."""
|
||||
from frappe.utils import get_traceback
|
||||
|
||||
return get_traceback()
|
||||
return get_traceback(with_context=with_context)
|
||||
|
||||
|
||||
def errprint(msg):
|
||||
|
|
@ -1210,18 +1210,35 @@ def reload_doc(module, dt=None, dn=None, force=False, reset_permissions=False):
|
|||
|
||||
|
||||
@whitelist()
|
||||
def rename_doc(*args, **kwargs):
|
||||
def rename_doc(
|
||||
doctype: str,
|
||||
old: str,
|
||||
new: str,
|
||||
force: bool = False,
|
||||
merge: bool = False,
|
||||
*,
|
||||
ignore_if_exists: bool = False,
|
||||
show_alert: bool = True,
|
||||
rebuild_search: bool = True,
|
||||
) -> str:
|
||||
"""
|
||||
Renames a doc(dt, old) to doc(dt, new) and updates all linked fields of type "Link"
|
||||
|
||||
Calls `frappe.model.rename_doc.rename_doc`
|
||||
"""
|
||||
kwargs.pop("ignore_permissions", None)
|
||||
kwargs.pop("cmd", None)
|
||||
|
||||
from frappe.model.rename_doc import rename_doc
|
||||
|
||||
return rename_doc(*args, **kwargs)
|
||||
return rename_doc(
|
||||
doctype=doctype,
|
||||
old=old,
|
||||
new=new,
|
||||
force=force,
|
||||
merge=merge,
|
||||
ignore_if_exists=ignore_if_exists,
|
||||
show_alert=show_alert,
|
||||
rebuild_search=rebuild_search,
|
||||
)
|
||||
|
||||
|
||||
def get_module(modulename):
|
||||
|
|
@ -1490,10 +1507,11 @@ def get_newargs(fn, kwargs):
|
|||
if hasattr(fn, "fnargs"):
|
||||
fnargs = fn.fnargs
|
||||
else:
|
||||
fullargspec = inspect.getfullargspec(fn)
|
||||
fnargs = fullargspec.args
|
||||
fnargs.extend(fullargspec.kwonlyargs)
|
||||
varkw = fullargspec.varkw
|
||||
signature = inspect.signature(fn)
|
||||
fnargs = list(signature.parameters)
|
||||
varkw = "kwargs" in fnargs
|
||||
if varkw:
|
||||
fnargs.pop(-1)
|
||||
|
||||
newargs = {}
|
||||
for a in kwargs:
|
||||
|
|
@ -1907,7 +1925,7 @@ def attach_print(
|
|||
|
||||
if not file_name:
|
||||
file_name = name
|
||||
file_name = file_name.replace(" ", "").replace("/", "-")
|
||||
file_name = cstr(file_name).replace(" ", "").replace("/", "-")
|
||||
|
||||
print_settings = db.get_singles_dict("Print Settings")
|
||||
|
||||
|
|
@ -2069,7 +2087,6 @@ def logger(
|
|||
|
||||
def log_error(title=None, message=None, reference_doctype=None, reference_name=None):
|
||||
"""Log error to Error Log"""
|
||||
|
||||
# Parameter ALERT:
|
||||
# the title and message may be swapped
|
||||
# the better API for this is log_error(title, message), and used in many cases this way
|
||||
|
|
@ -2082,20 +2099,15 @@ def log_error(title=None, message=None, reference_doctype=None, reference_name=N
|
|||
else:
|
||||
traceback = message
|
||||
|
||||
if not traceback:
|
||||
traceback = get_traceback()
|
||||
|
||||
if not title:
|
||||
title = "Error"
|
||||
title = title or "Error"
|
||||
traceback = as_unicode(traceback or get_traceback(with_context=True))
|
||||
|
||||
return get_doc(
|
||||
dict(
|
||||
doctype="Error Log",
|
||||
error=as_unicode(traceback),
|
||||
method=title,
|
||||
reference_doctype=reference_doctype,
|
||||
reference_name=reference_name,
|
||||
)
|
||||
doctype="Error Log",
|
||||
error=traceback,
|
||||
method=title,
|
||||
reference_doctype=reference_doctype,
|
||||
reference_name=reference_name,
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
|
||||
|
|
@ -2252,7 +2264,4 @@ def mock(type, size=1, locale="en"):
|
|||
return squashify(results)
|
||||
|
||||
|
||||
def validate_and_sanitize_search_inputs(fn):
|
||||
from frappe.desk.search import validate_and_sanitize_search_inputs as func
|
||||
|
||||
return func(fn)
|
||||
from frappe.desk.search import validate_and_sanitize_search_inputs # noqa
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from frappe.core.doctype.navbar_settings.navbar_settings import get_app_logo, ge
|
|||
from frappe.desk.doctype.route_history.route_history import frequently_visited_links
|
||||
from frappe.desk.form.load import get_meta_bundle
|
||||
from frappe.email.inbox import get_email_accounts
|
||||
from frappe.geo.country_info import get_all
|
||||
from frappe.model.base_document import get_controller
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.query_builder.functions import Count
|
||||
|
|
@ -67,6 +68,7 @@ def get_bootinfo():
|
|||
bootinfo.home_folder = frappe.db.get_value("File", {"is_home_folder": 1})
|
||||
bootinfo.navbar_settings = get_navbar_settings()
|
||||
bootinfo.notification_settings = get_notification_settings()
|
||||
get_country_codes(bootinfo)
|
||||
set_time_zone(bootinfo)
|
||||
|
||||
# ipinfo
|
||||
|
|
@ -384,6 +386,11 @@ def get_notification_settings():
|
|||
return frappe.get_cached_doc("Notification Settings", frappe.session.user)
|
||||
|
||||
|
||||
def get_country_codes(bootinfo):
|
||||
country_codes = get_all()
|
||||
bootinfo.country_codes = frappe._dict(country_codes)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_link_title_doctypes():
|
||||
dts = frappe.get_all("DocType", {"show_title_field_in_link": 1})
|
||||
|
|
|
|||
|
|
@ -189,7 +189,10 @@ def insert(doc=None):
|
|||
if isinstance(doc, str):
|
||||
doc = json.loads(doc)
|
||||
|
||||
if doc.get("parenttype"):
|
||||
doc = frappe._dict(doc)
|
||||
if frappe.is_table(doc.doctype):
|
||||
if not (doc.parenttype and doc.parent and doc.parentfield):
|
||||
frappe.throw(_("parenttype, parent and parentfield are required to insert a child record"))
|
||||
# inserting a child record
|
||||
parent = frappe.get_doc(doc.parenttype, doc.parent)
|
||||
parent.append(doc.parentfield, doc)
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@
|
|||
"label": "Type",
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nJSON\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
|
||||
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nJSON\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nPhone\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
|
|
@ -557,4 +557,4 @@
|
|||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ frappe.ui.form.on('DocType', {
|
|||
frm.get_docfield('fields', 'in_list_view').label = frm.doc.istable ?
|
||||
__('In Grid View') : __('In List View');
|
||||
|
||||
frm.events.autoname(frm);
|
||||
frm.events.set_naming_rule_description(frm);
|
||||
frm.cscript.autoname(frm);
|
||||
frm.cscript.set_naming_rule_description(frm);
|
||||
},
|
||||
|
||||
istable: (frm) => {
|
||||
|
|
@ -67,80 +67,6 @@ frappe.ui.form.on('DocType', {
|
|||
frm.set_value('allow_rename', 0);
|
||||
}
|
||||
},
|
||||
|
||||
naming_rule: function(frm) {
|
||||
// set the "autoname" property based on naming_rule
|
||||
if (frm.doc.naming_rule && !frm.__from_autoname) {
|
||||
|
||||
// flag to avoid recursion
|
||||
frm.__from_naming_rule = true;
|
||||
|
||||
if (frm.doc.naming_rule=='Set by user') {
|
||||
frm.set_value('autoname', 'Prompt');
|
||||
} else if (frm.doc.naming_rule === 'Autoincrement') {
|
||||
frm.set_value('autoname', 'autoincrement');
|
||||
// set allow rename to be false when using autoincrement
|
||||
frm.set_value('allow_rename', 0);
|
||||
} else if (frm.doc.naming_rule=='By fieldname') {
|
||||
frm.set_value('autoname', 'field:');
|
||||
} else if (frm.doc.naming_rule=='By "Naming Series" field') {
|
||||
frm.set_value('autoname', 'naming_series:');
|
||||
} else if (frm.doc.naming_rule=='Expression') {
|
||||
frm.set_value('autoname', 'format:');
|
||||
} else if (frm.doc.naming_rule=='Expression (old style)') {
|
||||
// pass
|
||||
} else if (frm.doc.naming_rule=='Random') {
|
||||
frm.set_value('autoname', 'hash');
|
||||
}
|
||||
setTimeout(() =>frm.__from_naming_rule = false, 500);
|
||||
|
||||
frm.events.set_naming_rule_description(frm);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
set_naming_rule_description(frm) {
|
||||
let naming_rule_description = {
|
||||
'Set by user': '',
|
||||
'Autoincrement': 'Uses Auto Increment feature of database.<br><b>WARNING: After using this option, any other naming option will not be accessible.</b>',
|
||||
'By fieldname': 'Format: <code>field:[fieldname]</code>. Valid fieldname must exist',
|
||||
'By "Naming Series" field': 'Format: <code>naming_series:[fieldname]</code>. Fieldname called <code>naming_series</code> must exist',
|
||||
'Expression': 'Format: <code>format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####}</code> - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.',
|
||||
'Expression (old style)': 'Format: <code>EXAMPLE-.#####</code> Series by prefix (separated by a dot)',
|
||||
'Random': '',
|
||||
'By script': ''
|
||||
};
|
||||
|
||||
if (frm.doc.naming_rule) {
|
||||
frm.get_field('autoname').set_description(naming_rule_description[frm.doc.naming_rule]);
|
||||
}
|
||||
},
|
||||
|
||||
autoname: function(frm) {
|
||||
// set naming_rule based on autoname (for old doctypes where its not been set)
|
||||
if (frm.doc.autoname && !frm.doc.naming_rule && !frm.__from_naming_rule) {
|
||||
// flag to avoid recursion
|
||||
frm.__from_autoname = true;
|
||||
if (frm.doc.autoname.toLowerCase() === 'prompt') {
|
||||
frm.set_value('naming_rule', 'Set by user');
|
||||
} else if (frm.doc.autoname.toLowerCase() === 'autoincrement') {
|
||||
frm.set_value('naming_rule', 'Autoincrement');
|
||||
} else if (frm.doc.autoname.startsWith('field:')) {
|
||||
frm.set_value('naming_rule', 'By fieldname');
|
||||
} else if (frm.doc.autoname.startsWith('naming_series:')) {
|
||||
frm.set_value('naming_rule', 'By "Naming Series" field');
|
||||
} else if (frm.doc.autoname.startsWith('format:')) {
|
||||
frm.set_value('naming_rule', 'Expression');
|
||||
} else if (frm.doc.autoname.toLowerCase() === 'hash') {
|
||||
frm.set_value('naming_rule', 'Random');
|
||||
} else {
|
||||
frm.set_value('naming_rule', 'Expression (old style)');
|
||||
}
|
||||
setTimeout(() => frm.__from_autoname = false, 500);
|
||||
}
|
||||
|
||||
frm.set_df_property('fields', 'reqd', frm.doc.autoname !== 'Prompt');
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("DocField", {
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@
|
|||
"label": "Naming"
|
||||
},
|
||||
{
|
||||
"description": "Naming Options:\n<ol><li><b>field:[fieldname]</b> - By Field</li><li><b>autoincrement</b> - Uses Databases' Auto Increment feature</li><li><b>naming_series:</b> - By Naming Series (field called naming_series must be present</li><li><b>Prompt</b> - Prompt user for a name</li><li><b>[series]</b> - Series by prefix (separated by a dot); for example PRE.#####</li>\n<li><b>format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####}</b> - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.</li></ol>",
|
||||
"description": "Naming Options:\n<ol><li><b>field:[fieldname]</b> - By Field</li><li><b>autoincrement</b> - Uses Databases' Auto Increment feature</li><li><b>naming_series:</b> - By Naming Series (field called naming_series must be present)</li><li><b>Prompt</b> - Prompt user for a name</li><li><b>[series]</b> - Series by prefix (separated by a dot); for example PRE.#####</li>\n<li><b>format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####}</b> - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.</li></ol>",
|
||||
"fieldname": "autoname",
|
||||
"fieldtype": "Data",
|
||||
"label": "Auto Name",
|
||||
|
|
|
|||
|
|
@ -92,10 +92,10 @@ class DocType(Document):
|
|||
|
||||
self.check_developer_mode()
|
||||
|
||||
self.validate_autoname()
|
||||
self.validate_name()
|
||||
|
||||
self.set_defaults_for_single_and_table()
|
||||
self.set_defaults_for_autoincremented()
|
||||
self.scrub_field_names()
|
||||
self.set_default_in_list_view()
|
||||
self.set_default_translatable()
|
||||
|
|
@ -124,6 +124,12 @@ class DocType(Document):
|
|||
if self.default_print_format and not self.custom:
|
||||
frappe.throw(_("Standard DocType cannot have default print format, use Customize Form"))
|
||||
|
||||
if check_if_can_change_name_type(self):
|
||||
change_name_column_type(
|
||||
self.name,
|
||||
"bigint" if self.autoname == "autoincrement" else f"varchar({frappe.db.VARCHAR_LEN})",
|
||||
)
|
||||
|
||||
def validate_field_name_conflicts(self):
|
||||
"""Check if field names dont conflict with controller properties and methods"""
|
||||
core_doctypes = [
|
||||
|
|
@ -184,6 +190,10 @@ class DocType(Document):
|
|||
self.allow_import = 0
|
||||
self.permissions = []
|
||||
|
||||
def set_defaults_for_autoincremented(self):
|
||||
if self.autoname and self.autoname == "autoincrement":
|
||||
self.allow_rename = 0
|
||||
|
||||
def set_default_in_list_view(self):
|
||||
"""Set default in-list-view for first 4 mandatory fields"""
|
||||
if not [d.fieldname for d in self.fields if d.in_list_view]:
|
||||
|
|
@ -809,19 +819,6 @@ class DocType(Document):
|
|||
max_idx = frappe.db.sql("""select max(idx) from `tabDocField` where parent = %s""", self.name)
|
||||
return max_idx and max_idx[0][0] or 0
|
||||
|
||||
def validate_autoname(self):
|
||||
if not self.is_new():
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
if doc_before_save:
|
||||
if (self.autoname == "autoincrement" and doc_before_save.autoname != "autoincrement") or (
|
||||
self.autoname != "autoincrement" and doc_before_save.autoname == "autoincrement"
|
||||
):
|
||||
frappe.throw(_("Cannot change to/from Autoincrement naming rule"))
|
||||
|
||||
else:
|
||||
if self.autoname == "autoincrement":
|
||||
self.allow_rename = 0
|
||||
|
||||
def validate_name(self, name=None):
|
||||
if not name:
|
||||
name = self.name
|
||||
|
|
@ -865,8 +862,13 @@ def validate_series(dt, autoname=None, name=None):
|
|||
|
||||
if not autoname and dt.get("fields", {"fieldname": "naming_series"}):
|
||||
dt.autoname = "naming_series:"
|
||||
elif dt.autoname == "naming_series:" and not dt.get("fields", {"fieldname": "naming_series"}):
|
||||
frappe.throw(_("Invalid fieldname '{0}' in autoname").format(dt.autoname))
|
||||
elif dt.autoname and dt.autoname.startswith("naming_series:"):
|
||||
fieldname = dt.autoname.split("naming_series:")[0] or "naming_series"
|
||||
if not dt.get("fields", {"fieldname": fieldname}):
|
||||
frappe.throw(
|
||||
_("Fieldname called {0} must exist to enable autonaming").format(frappe.bold(fieldname)),
|
||||
title=_("Field Missing"),
|
||||
)
|
||||
|
||||
# validate field name if autoname field:fieldname is used
|
||||
# Create unique index on autoname field automatically.
|
||||
|
|
@ -884,7 +886,7 @@ def validate_series(dt, autoname=None, name=None):
|
|||
autoname
|
||||
and (not autoname.startswith("field:"))
|
||||
and (not autoname.startswith("eval:"))
|
||||
and (not autoname.lower() in ("prompt", "hash"))
|
||||
and (autoname.lower() not in ("prompt", "hash"))
|
||||
and (not autoname.startswith("naming_series:"))
|
||||
and (not autoname.startswith("format:"))
|
||||
):
|
||||
|
|
@ -901,6 +903,51 @@ def validate_series(dt, autoname=None, name=None):
|
|||
frappe.throw(_("Series {0} already used in {1}").format(prefix, used_in[0][0]))
|
||||
|
||||
|
||||
def check_if_can_change_name_type(dt: DocType, raise_err: bool = True) -> bool:
|
||||
def get_autoname_before_save(doctype: str, to_be_customized_dt: str) -> str:
|
||||
if doctype == "Customize Form":
|
||||
property_value = frappe.db.get_value(
|
||||
"Property Setter", {"doc_type": to_be_customized_dt, "property": "autoname"}, "value"
|
||||
)
|
||||
|
||||
# initially no property setter is set,
|
||||
# hence getting autoname value from the doctype itself
|
||||
if not property_value:
|
||||
return frappe.db.get_value("DocType", to_be_customized_dt, "autoname") or ""
|
||||
|
||||
return property_value
|
||||
|
||||
return getattr(dt.get_doc_before_save(), "autoname", "")
|
||||
|
||||
doctype_name = dt.doc_type if dt.doctype == "Customize Form" else dt.name
|
||||
|
||||
if not dt.is_new():
|
||||
autoname_before_save = get_autoname_before_save(dt.doctype, doctype_name)
|
||||
is_autoname_autoincrement = dt.autoname == "autoincrement"
|
||||
|
||||
if (
|
||||
is_autoname_autoincrement
|
||||
and autoname_before_save != "autoincrement"
|
||||
or (not is_autoname_autoincrement and autoname_before_save == "autoincrement")
|
||||
):
|
||||
if not frappe.get_all(doctype_name, limit=1):
|
||||
# allow changing the column type if there is no data
|
||||
return True
|
||||
|
||||
if raise_err:
|
||||
frappe.throw(
|
||||
_("Can only change to/from Autoincrement naming rule when there is no data in the doctype")
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def change_name_column_type(doctype_name: str, type: str) -> None:
|
||||
return frappe.db.change_column_type(
|
||||
doctype_name, "name", type, True if frappe.db.db_type == "mariadb" else False
|
||||
)
|
||||
|
||||
|
||||
def validate_links_table_fieldnames(meta):
|
||||
"""Validate fieldnames in Links table"""
|
||||
if not meta.links or frappe.flags.in_patch or frappe.flags.in_fixtures:
|
||||
|
|
|
|||
|
|
@ -524,18 +524,33 @@ class TestDocType(unittest.TestCase):
|
|||
dt.delete()
|
||||
|
||||
def test_autoincremented_doctype_transition(self):
|
||||
frappe.delete_doc("testy_autoinc_dt")
|
||||
frappe.delete_doc_if_exists("DocType", "testy_autoinc_dt")
|
||||
dt = new_doctype("testy_autoinc_dt", autoname="autoincrement").insert(ignore_permissions=True)
|
||||
dt.autoname = "hash"
|
||||
|
||||
dt.save(ignore_permissions=True)
|
||||
|
||||
dt_data = frappe.get_doc({"doctype": dt.name, "some_fieldname": "test data"}).insert(
|
||||
ignore_permissions=True
|
||||
)
|
||||
|
||||
dt.autoname = "autoincrement"
|
||||
|
||||
try:
|
||||
dt.save(ignore_permissions=True)
|
||||
except frappe.ValidationError as e:
|
||||
self.assertEqual(e.args[0], "Cannot change to/from Autoincrement naming rule")
|
||||
self.assertEqual(
|
||||
e.args[0],
|
||||
"Can only change to/from Autoincrement naming rule when there is no data in the doctype",
|
||||
)
|
||||
else:
|
||||
self.fail("Shouldnt be possible to transition autoincremented doctype to any other naming rule")
|
||||
self.fail(
|
||||
"""Shouldn't be possible to transition to/from autoincremented doctype
|
||||
when data is present in doctype"""
|
||||
)
|
||||
finally:
|
||||
# cleanup
|
||||
dt_data.delete(ignore_permissions=True)
|
||||
dt.delete(ignore_permissions=True)
|
||||
|
||||
def test_json_field(self):
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@
|
|||
"fieldname": "doctype_event",
|
||||
"fieldtype": "Select",
|
||||
"label": "DocType Event",
|
||||
"options": "Before Insert\nBefore Validate\nBefore Save\nAfter Insert\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)"
|
||||
"options": "Before Insert\nBefore Validate\nBefore Save\nAfter Insert\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)\nOn Payment Authorization"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.script_type==='API'",
|
||||
|
|
@ -109,7 +109,7 @@
|
|||
"link_fieldname": "server_script"
|
||||
}
|
||||
],
|
||||
"modified": "2022-04-07 19:41:23.178772",
|
||||
"modified": "2022-04-27 11:42:52.032963",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Server Script",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ EVENT_MAP = {
|
|||
"after_delete": "After Delete",
|
||||
"before_update_after_submit": "Before Save (Submitted Document)",
|
||||
"on_update_after_submit": "After Save (Submitted Document)",
|
||||
"on_payment_authorized": "On Payment Authorization",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@
|
|||
"prepared_report_section",
|
||||
"enable_prepared_report_auto_deletion",
|
||||
"prepared_report_expiry_period",
|
||||
"column_break_64",
|
||||
"max_auto_email_report_per_user",
|
||||
"system_updates_section",
|
||||
"disable_system_update_notification"
|
||||
],
|
||||
|
|
@ -445,7 +447,7 @@
|
|||
"collapsible": 1,
|
||||
"fieldname": "prepared_report_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Prepared Report"
|
||||
"label": "Reports"
|
||||
},
|
||||
{
|
||||
"default": "Frappe",
|
||||
|
|
@ -485,12 +487,22 @@
|
|||
"fieldtype": "Select",
|
||||
"label": "First Day of the Week",
|
||||
"options": "Sunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_64",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "20",
|
||||
"fieldname": "max_auto_email_report_per_user",
|
||||
"fieldtype": "Int",
|
||||
"label": "Max auto email report per user"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-01-04 11:28:34.881192",
|
||||
"modified": "2022-04-21 09:11:35.218721",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "System Settings",
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@
|
|||
"label": "Field Type",
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nJSON\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
|
||||
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nJSON\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nPhone\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -152,6 +152,10 @@ frappe.ui.form.on("Customize Form", {
|
|||
},
|
||||
__("Actions")
|
||||
);
|
||||
|
||||
const is_autoname_autoincrement = frm.doc.autoname === 'autoincrement';
|
||||
frm.set_df_property("naming_rule", "hidden", is_autoname_autoincrement);
|
||||
frm.set_df_property("autoname", "read_only", is_autoname_autoincrement);
|
||||
}
|
||||
|
||||
frm.events.setup_export(frm);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
"fields_section_break",
|
||||
"fields",
|
||||
"naming_section",
|
||||
"naming_rule",
|
||||
"autoname",
|
||||
"view_settings_section",
|
||||
"title_field",
|
||||
|
|
@ -50,6 +51,13 @@
|
|||
"sort_order"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "naming_rule",
|
||||
"fieldtype": "Select",
|
||||
"label": "Naming Rule",
|
||||
"length": 40,
|
||||
"options": "\nSet by user\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script"
|
||||
},
|
||||
{
|
||||
"fieldname": "doc_type",
|
||||
"fieldtype": "Link",
|
||||
|
|
@ -279,7 +287,7 @@
|
|||
"label": "Naming"
|
||||
},
|
||||
{
|
||||
"description": "Naming Options:\n<ol><li><b>field:[fieldname]</b> - By Field</li><li><b>naming_series:</b> - By Naming Series (field called naming_series must be present</li><li><b>Prompt</b> - Prompt user for a name</li><li><b>[series]</b> - Series by prefix (separated by a dot); for example PRE.#####</li>\n<li><b>format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####}</b> - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.</li></ol>",
|
||||
"description": "Naming Options:\n<ol><li><b>field:[fieldname]</b> - By Field</li>\n<li><b>autoincrement</b> - Uses Databases' Auto Increment feature</li><li><b>naming_series:</b> - By Naming Series (field called naming_series must be present</li><li><b>Prompt</b> - Prompt user for a name</li><li><b>[series]</b> - Series by prefix (separated by a dot); for example PRE.#####</li>\n<li><b>format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####}</b> - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.</li></ol>",
|
||||
"fieldname": "autoname",
|
||||
"fieldtype": "Data",
|
||||
"label": "Auto Name"
|
||||
|
|
@ -311,7 +319,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-01-07 16:07:06.196534",
|
||||
"modified": "2022-04-21 15:36:16.772277",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form",
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ import frappe
|
|||
import frappe.translate
|
||||
from frappe import _
|
||||
from frappe.core.doctype.doctype.doctype import (
|
||||
change_name_column_type,
|
||||
check_email_append_to,
|
||||
check_if_can_change_name_type,
|
||||
validate_fields_for_doctype,
|
||||
validate_series,
|
||||
)
|
||||
|
|
@ -159,7 +161,9 @@ class CustomizeForm(Document):
|
|||
def save_customization(self):
|
||||
if not self.doc_type:
|
||||
return
|
||||
|
||||
validate_series(self, self.autoname, self.doc_type)
|
||||
can_change_name_type = check_if_can_change_name_type(self)
|
||||
self.flags.update_db = False
|
||||
self.flags.rebuild_doctype_for_global_search = False
|
||||
self.set_property_setters()
|
||||
|
|
@ -168,6 +172,12 @@ class CustomizeForm(Document):
|
|||
validate_fields_for_doctype(self.doc_type)
|
||||
check_email_append_to(self)
|
||||
|
||||
if can_change_name_type:
|
||||
change_name_column_type(
|
||||
self.doc_type,
|
||||
"bigint" if self.autoname == "autoincrement" else f"varchar({frappe.db.VARCHAR_LEN})",
|
||||
)
|
||||
|
||||
if self.flags.update_db:
|
||||
frappe.db.updatedb(self.doc_type)
|
||||
|
||||
|
|
@ -571,6 +581,7 @@ doctype_properties = {
|
|||
"email_append_to": "Check",
|
||||
"subject_field": "Data",
|
||||
"sender_field": "Data",
|
||||
"naming_rule": "Data",
|
||||
"autoname": "Data",
|
||||
"show_title_field_in_link": "Check",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@
|
|||
"label": "Type",
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
|
||||
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nPhone\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
|
|
@ -477,4 +477,4 @@
|
|||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1066,7 +1066,7 @@ class Database(object):
|
|||
now_datetime() - relativedelta(minutes=minutes),
|
||||
)[0][0]
|
||||
|
||||
def get_db_table_columns(self, table):
|
||||
def get_db_table_columns(self, table) -> List[str]:
|
||||
"""Returns list of column names from given table."""
|
||||
columns = frappe.cache().hget("table_columns", table)
|
||||
if columns is None:
|
||||
|
|
@ -1146,18 +1146,13 @@ class Database(object):
|
|||
return frappe.db.is_missing_column(e)
|
||||
|
||||
def get_descendants(self, doctype, name):
|
||||
"""Return descendants of the current record"""
|
||||
node_location_indexes = self.get_value(doctype, name, ("lft", "rgt"))
|
||||
if node_location_indexes:
|
||||
lft, rgt = node_location_indexes
|
||||
return self.sql_list(
|
||||
"""select name from `tab{doctype}`
|
||||
where lft > {lft} and rgt < {rgt}""".format(
|
||||
doctype=doctype, lft=lft, rgt=rgt
|
||||
)
|
||||
)
|
||||
else:
|
||||
# when document does not exist
|
||||
"""Return descendants of the group node in tree"""
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
|
||||
try:
|
||||
return get_descendants_of(doctype, name, ignore_permissions=True)
|
||||
except Exception:
|
||||
# Can only happen if document doesn't exists - kept for backward compatibility
|
||||
return []
|
||||
|
||||
def is_missing_table_or_column(self, e):
|
||||
|
|
@ -1251,6 +1246,21 @@ class Database(object):
|
|||
values_to_insert = values[start_index : start_index + chunk_size]
|
||||
query.columns(fields).insert(*values_to_insert).run()
|
||||
|
||||
def create_sequence(self, *args, **kwargs):
|
||||
from frappe.database.sequence import create_sequence
|
||||
|
||||
return create_sequence(*args, **kwargs)
|
||||
|
||||
def set_next_sequence_val(self, *args, **kwargs):
|
||||
from frappe.database.sequence import set_next_val
|
||||
|
||||
set_next_val(*args, **kwargs)
|
||||
|
||||
def get_next_sequence_val(self, *args, **kwargs):
|
||||
from frappe.database.sequence import get_next_val
|
||||
|
||||
return get_next_val(*args, **kwargs)
|
||||
|
||||
|
||||
def enqueue_jobs_after_commit():
|
||||
from frappe.utils.background_jobs import execute_job, get_queue
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ class MariaDBDatabase(Database):
|
|||
"Geolocation": ("longtext", ""),
|
||||
"Duration": ("decimal", "21,9"),
|
||||
"Icon": ("varchar", self.VARCHAR_LEN),
|
||||
"Phone": ("varchar", self.VARCHAR_LEN),
|
||||
"Autocomplete": ("varchar", self.VARCHAR_LEN),
|
||||
"JSON": ("json", ""),
|
||||
}
|
||||
|
|
@ -148,7 +149,7 @@ class MariaDBDatabase(Database):
|
|||
) -> Union[List, Tuple]:
|
||||
table_name = get_table_name(doctype)
|
||||
null_constraint = "NOT NULL" if not nullable else ""
|
||||
return self.sql(f"ALTER TABLE `{table_name}` MODIFY `{column}` {type} {null_constraint}")
|
||||
return self.sql_ddl(f"ALTER TABLE `{table_name}` MODIFY `{column}` {type} {null_constraint}")
|
||||
|
||||
# exception types
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import frappe
|
||||
from frappe import _
|
||||
from frappe.database.schema import DBTable
|
||||
from frappe.database.sequence import create_sequence
|
||||
from frappe.model import log_types
|
||||
|
||||
|
||||
|
|
@ -48,7 +47,7 @@ class MariaDBTable(DBTable):
|
|||
# By default the cache is 1000 which will mess up the sequence when
|
||||
# using the system after a restore.
|
||||
# issue link: https://jira.mariadb.org/browse/MDEV-21786
|
||||
create_sequence(self.doctype, check_not_exists=True, cache=50)
|
||||
frappe.db.create_sequence(self.doctype, check_not_exists=True, cache=50)
|
||||
|
||||
# NOTE: not used nextval func as default as the ability to restore
|
||||
# database with sequences has bugs in mariadb and gives a scary error.
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ class PostgresDatabase(Database):
|
|||
"Geolocation": ("text", ""),
|
||||
"Duration": ("decimal", "21,9"),
|
||||
"Icon": ("varchar", self.VARCHAR_LEN),
|
||||
"Phone": ("varchar", self.VARCHAR_LEN),
|
||||
"Autocomplete": ("varchar", self.VARCHAR_LEN),
|
||||
"JSON": ("json", ""),
|
||||
}
|
||||
|
|
@ -212,7 +213,11 @@ class PostgresDatabase(Database):
|
|||
) -> Union[List, Tuple]:
|
||||
table_name = get_table_name(doctype)
|
||||
null_constraint = "SET NOT NULL" if not nullable else "DROP NOT NULL"
|
||||
return self.sql(
|
||||
|
||||
# postgres allows ddl in transactions but since we've currently made
|
||||
# things same as mariadb (raising exception on ddl commands if the transaction has any writes),
|
||||
# hence using sql_ddl here for committing and then moving forward.
|
||||
return self.sql_ddl(
|
||||
f"""ALTER TABLE "{table_name}"
|
||||
ALTER COLUMN "{column}" TYPE {type},
|
||||
ALTER COLUMN "{column}" {null_constraint}"""
|
||||
|
|
@ -381,12 +386,10 @@ def modify_query(query):
|
|||
# drop .0 from decimals and add quotes around them
|
||||
#
|
||||
# >>> query = "c='abcd' , a >= 45, b = -45.0, c = 40, d=4500.0, e=3500.53, f=40psdfsd, g=9092094312, h=12.00023"
|
||||
# >>> re.sub(r"([=><]+)\s*(?!\d+[a-zA-Z])(?![+-]?\d+\.\d\d+)([+-]?\d+)(\.0)?", r"\1 '\2'", query)
|
||||
# >>> re.sub(r"([=><]+)\s*([+-]?\d+)(\.0)?(?![a-zA-Z\.\d])", r"\1 '\2'", query)
|
||||
# "c='abcd' , a >= '45', b = '-45', c = '40', d= '4500', e=3500.53, f=40psdfsd, g= '9092094312', h=12.00023
|
||||
|
||||
query = re.sub(
|
||||
r"([=><]+)\s*(?!\d+[a-zA-Z])(?![+-]?\d+\.\d\d+)([+-]?\d+)(\.0)?", r"\1 '\2'", query
|
||||
)
|
||||
query = re.sub(r"([=><]+)\s*([+-]?\d+)(\.0)?(?![a-zA-Z\.\d])", r"\1 '\2'", query)
|
||||
return query
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ CREATE TABLE "tabDocType" (
|
|||
|
||||
DROP TABLE IF EXISTS "tabSeries";
|
||||
CREATE TABLE "tabSeries" (
|
||||
"name" varchar(100) DEFAULT NULL,
|
||||
"name" varchar(100),
|
||||
"current" bigint NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY ("name")
|
||||
) ;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import frappe
|
||||
from frappe import _
|
||||
from frappe.database.schema import DBTable, get_definition
|
||||
from frappe.database.sequence import create_sequence
|
||||
from frappe.model import log_types
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
|
|
@ -39,7 +38,7 @@ class PostgresTable(DBTable):
|
|||
# Since we're opening and closing connections for every transaction this results in skipping the cache
|
||||
# to the next non-cached value hence not using cache in postgres.
|
||||
# ref: https://stackoverflow.com/questions/21356375/postgres-9-0-4-sequence-skipping-numbers
|
||||
create_sequence(self.doctype, check_not_exists=True)
|
||||
frappe.db.create_sequence(self.doctype, check_not_exists=True)
|
||||
name_column = "name bigint primary key"
|
||||
|
||||
# TODO: set docstatus length
|
||||
|
|
|
|||
|
|
@ -108,11 +108,14 @@ def change_orderby(order: str):
|
|||
tuple: field, order
|
||||
"""
|
||||
order = order.split()
|
||||
if order[1].lower() == "asc":
|
||||
orderby, order = order[0], Order.asc
|
||||
return orderby, order
|
||||
orderby, order = order[0], Order.desc
|
||||
return orderby, order
|
||||
|
||||
try:
|
||||
if order[1].lower() == "asc":
|
||||
return order[0], Order.asc
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
return order[0], Order.desc
|
||||
|
||||
|
||||
OPERATOR_MAP = {
|
||||
|
|
@ -175,10 +178,13 @@ class Query:
|
|||
"""
|
||||
if kwargs.get("orderby"):
|
||||
orderby = kwargs.get("orderby")
|
||||
order = kwargs.get("order") if kwargs.get("order") else Order.desc
|
||||
if isinstance(orderby, str) and len(orderby.split()) > 1:
|
||||
orderby, order = change_orderby(orderby)
|
||||
conditions = conditions.orderby(orderby, order=order)
|
||||
for ordby in orderby.split(","):
|
||||
if ordby := ordby.strip():
|
||||
orderby, order = change_orderby(ordby)
|
||||
conditions = conditions.orderby(orderby, order=order)
|
||||
else:
|
||||
conditions = conditions.orderby(orderby, order=kwargs.get("order") or Order.desc)
|
||||
|
||||
if kwargs.get("limit"):
|
||||
conditions = conditions.limit(kwargs.get("limit"))
|
||||
|
|
@ -288,7 +294,7 @@ class Query:
|
|||
table: str,
|
||||
fields: Union[List, Tuple],
|
||||
filters: Union[Dict[str, Union[str, int]], str, int] = None,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
):
|
||||
criterion = self.build_conditions(table, filters, **kwargs)
|
||||
if isinstance(fields, (list, tuple)):
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ def create_sequence(
|
|||
doctype_name: str,
|
||||
*,
|
||||
slug: str = "_id_seq",
|
||||
temporary=False,
|
||||
check_not_exists: bool = False,
|
||||
cycle: bool = False,
|
||||
cache: int = 0,
|
||||
|
|
@ -14,7 +15,7 @@ def create_sequence(
|
|||
max_value: int = 0,
|
||||
) -> str:
|
||||
|
||||
query = "create sequence"
|
||||
query = "create sequence" if not temporary else "create temporary sequence"
|
||||
sequence_name = scrub(doctype_name + slug)
|
||||
|
||||
if check_not_exists:
|
||||
|
|
@ -22,29 +23,29 @@ def create_sequence(
|
|||
|
||||
query += f" {sequence_name}"
|
||||
|
||||
if cache:
|
||||
query += f" cache {cache}"
|
||||
else:
|
||||
# in postgres, the default is cache 1
|
||||
if db.db_type == "mariadb":
|
||||
query += " nocache"
|
||||
|
||||
if start_value:
|
||||
# default is 1
|
||||
query += f" start with {start_value}"
|
||||
|
||||
if increment_by:
|
||||
# default is 1
|
||||
query += f" increment by {increment_by}"
|
||||
|
||||
if min_value:
|
||||
# default is 1
|
||||
query += f" min value {min_value}"
|
||||
query += f" minvalue {min_value}"
|
||||
|
||||
if max_value:
|
||||
query += f" max value {max_value}"
|
||||
query += f" maxvalue {max_value}"
|
||||
|
||||
if start_value:
|
||||
# default is 1
|
||||
query += f" start {start_value}"
|
||||
|
||||
# in postgres, the default is cache 1 / no cache
|
||||
if cache:
|
||||
query += f" cache {cache}"
|
||||
elif db.db_type == "mariadb":
|
||||
query += " nocache"
|
||||
|
||||
if not cycle:
|
||||
# in postgres, default is no cycle
|
||||
if db.db_type == "mariadb":
|
||||
query += " nocycle"
|
||||
else:
|
||||
|
|
@ -56,21 +57,23 @@ def create_sequence(
|
|||
|
||||
|
||||
def get_next_val(doctype_name: str, slug: str = "_id_seq") -> int:
|
||||
if db.db_type == "postgres":
|
||||
return db.sql(f"select nextval('\"{scrub(doctype_name + slug)}\"')")[0][0]
|
||||
return db.sql(f"select nextval(`{scrub(doctype_name + slug)}`)")[0][0]
|
||||
return db.multisql(
|
||||
{
|
||||
"postgres": f"select nextval('\"{scrub(doctype_name + slug)}\"')",
|
||||
"mariadb": f"select nextval(`{scrub(doctype_name + slug)}`)",
|
||||
}
|
||||
)[0][0]
|
||||
|
||||
|
||||
def set_next_val(
|
||||
doctype_name: str, next_val: int, *, slug: str = "_id_seq", is_val_used: bool = False
|
||||
) -> None:
|
||||
|
||||
if not is_val_used:
|
||||
is_val_used = 0 if db.db_type == "mariadb" else "f"
|
||||
else:
|
||||
is_val_used = 1 if db.db_type == "mariadb" else "t"
|
||||
is_val_used = "false" if not is_val_used else "true"
|
||||
|
||||
if db.db_type == "postgres":
|
||||
db.sql(f"SELECT SETVAL('\"{scrub(doctype_name + slug)}\"', {next_val}, '{is_val_used}')")
|
||||
else:
|
||||
db.sql(f"SELECT SETVAL(`{scrub(doctype_name + slug)}`, {next_val}, {is_val_used})")
|
||||
db.multisql(
|
||||
{
|
||||
"postgres": f"SELECT SETVAL('\"{scrub(doctype_name + slug)}\"', {next_val}, {is_val_used})",
|
||||
"mariadb": f"SELECT SETVAL(`{scrub(doctype_name + slug)}`, {next_val}, {is_val_used})",
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -27,8 +27,11 @@ frappe.ui.form.on('Number Card', {
|
|||
frm.trigger('set_method_description');
|
||||
frm.trigger('render_filters_table');
|
||||
}
|
||||
frm.trigger('create_add_to_dashboard_button');
|
||||
frm.trigger('set_parent_document_type');
|
||||
|
||||
if (!frm.is_new()) {
|
||||
frm.trigger('create_add_to_dashboard_button');
|
||||
}
|
||||
},
|
||||
|
||||
create_add_to_dashboard_button: function(frm) {
|
||||
|
|
|
|||
|
|
@ -20,15 +20,24 @@ class NumberCard(Document):
|
|||
self.name = append_number_if_name_exists("Number Card", self.name)
|
||||
|
||||
def validate(self):
|
||||
if not self.document_type:
|
||||
frappe.throw(_("Document type is required to create a number card"))
|
||||
if self.type == "Document Type":
|
||||
if not (self.document_type and self.function):
|
||||
frappe.throw(_("Document Type and Function are required to create a number card"))
|
||||
|
||||
if (
|
||||
self.document_type
|
||||
and frappe.get_meta(self.document_type).istable
|
||||
and not self.parent_document_type
|
||||
):
|
||||
frappe.throw(_("Parent document type is required to create a number card"))
|
||||
if (
|
||||
self.document_type
|
||||
and frappe.get_meta(self.document_type).istable
|
||||
and not self.parent_document_type
|
||||
):
|
||||
frappe.throw(_("Parent Document Type is required to create a number card"))
|
||||
|
||||
elif self.type == "Report":
|
||||
if not (self.report_name and self.report_field and self.function):
|
||||
frappe.throw(_("Report Name, Report Field and Fucntion are required to create a number card"))
|
||||
|
||||
elif self.type == "Custom":
|
||||
if not self.method:
|
||||
frappe.throw(_("Method is required to create a number card"))
|
||||
|
||||
def on_update(self):
|
||||
if frappe.conf.developer_mode and self.is_standard:
|
||||
|
|
|
|||
|
|
@ -80,7 +80,14 @@ class Workspace(Document):
|
|||
|
||||
# remove duplicate before adding
|
||||
for idx, link in enumerate(self.links):
|
||||
if link.label == card.get("label") and link.type == "Card Break":
|
||||
if link.get("label") == card.get("label") and link.get("type") == "Card Break":
|
||||
# count and set number of links for the card if link_count is 0
|
||||
if link.link_count == 0:
|
||||
for count, card_link in enumerate(self.links[idx + 1 :]):
|
||||
if card_link.get("type") == "Card Break":
|
||||
break
|
||||
link.link_count = count + 1
|
||||
|
||||
del self.links[idx : idx + link.link_count + 1]
|
||||
|
||||
self.append(
|
||||
|
|
@ -199,21 +206,31 @@ def update_page(name, title, icon, parent, public):
|
|||
doc.sequence_id = frappe.db.count("Workspace", {"public": public}, cache=True)
|
||||
doc.public = public
|
||||
doc.for_user = "" if public else doc.for_user or frappe.session.user
|
||||
doc.label = "{0}-{1}".format(title, doc.for_user) if doc.for_user else title
|
||||
doc.label = new_name = "{0}-{1}".format(title, doc.for_user) if doc.for_user else title
|
||||
doc.save(ignore_permissions=True)
|
||||
|
||||
if name != doc.label:
|
||||
rename_doc("Workspace", name, doc.label, force=True, ignore_permissions=True)
|
||||
if name != new_name:
|
||||
rename_doc("Workspace", name, new_name, force=True, ignore_permissions=True)
|
||||
|
||||
# update new name and public in child pages
|
||||
if child_docs:
|
||||
for child in child_docs:
|
||||
child_doc = frappe.get_doc("Workspace", child.name)
|
||||
child_doc.parent_page = doc.title
|
||||
child_doc.public = doc.public
|
||||
if child_doc.public != public:
|
||||
child_doc.public = public
|
||||
child_doc.for_user = "" if public else child_doc.for_user or frappe.session.user
|
||||
child_doc.label = new_child_name = (
|
||||
"{0}-{1}".format(child_doc.title, child_doc.for_user)
|
||||
if child_doc.for_user
|
||||
else child_doc.title
|
||||
)
|
||||
child_doc.save(ignore_permissions=True)
|
||||
|
||||
return {"name": doc.title, "public": doc.public, "label": doc.label}
|
||||
if child.name != new_child_name:
|
||||
rename_doc("Workspace", child.name, new_child_name, force=True, ignore_permissions=True)
|
||||
|
||||
return {"name": title, "public": public, "label": new_name}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ def get_report_doc(report_name):
|
|||
|
||||
|
||||
def get_report_result(report, filters):
|
||||
res = None
|
||||
|
||||
if report.report_type == "Query Report":
|
||||
res = report.execute_query_report(filters)
|
||||
|
||||
|
|
@ -84,7 +86,7 @@ def generate_report_result(
|
|||
res = get_report_result(report, filters) or []
|
||||
|
||||
columns, result, message, chart, report_summary, skip_total_row = ljust_list(res, 6)
|
||||
columns = [get_column_as_dict(col) for col in columns]
|
||||
columns = [get_column_as_dict(col) for col in (columns or [])]
|
||||
report_column_names = [col["fieldname"] for col in columns]
|
||||
|
||||
# convert to list of dicts
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import functools
|
||||
import json
|
||||
import re
|
||||
|
||||
import wrapt
|
||||
|
||||
# Search
|
||||
import frappe
|
||||
from frappe import _, is_whitelisted
|
||||
from frappe.permissions import has_permission
|
||||
|
|
@ -314,17 +312,20 @@ def relevance_sorter(key, query, as_dict):
|
|||
return (cstr(value).lower().startswith(query.lower()) is not True, value)
|
||||
|
||||
|
||||
@wrapt.decorator
|
||||
def validate_and_sanitize_search_inputs(fn, instance, args, kwargs):
|
||||
kwargs.update(dict(zip(fn.__code__.co_varnames, args)))
|
||||
sanitize_searchfield(kwargs["searchfield"])
|
||||
kwargs["start"] = cint(kwargs["start"])
|
||||
kwargs["page_len"] = cint(kwargs["page_len"])
|
||||
def validate_and_sanitize_search_inputs(fn):
|
||||
@functools.wraps(fn)
|
||||
def wrapper(*args, **kwargs):
|
||||
kwargs.update(dict(zip(fn.__code__.co_varnames, args)))
|
||||
sanitize_searchfield(kwargs["searchfield"])
|
||||
kwargs["start"] = cint(kwargs["start"])
|
||||
kwargs["page_len"] = cint(kwargs["page_len"])
|
||||
|
||||
if kwargs["doctype"] and not frappe.db.exists("DocType", kwargs["doctype"]):
|
||||
return []
|
||||
if kwargs["doctype"] and not frappe.db.exists("DocType", kwargs["doctype"]):
|
||||
return []
|
||||
|
||||
return fn(**kwargs)
|
||||
return fn(**kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from frappe.model.document import Document
|
|||
from frappe.model.naming import append_number_if_name_exists
|
||||
from frappe.utils import (
|
||||
add_to_date,
|
||||
cint,
|
||||
format_time,
|
||||
get_link_to_form,
|
||||
get_url_to_report,
|
||||
|
|
@ -51,14 +52,18 @@ class AutoEmailReport(Document):
|
|||
self.email_to = "\n".join(valid)
|
||||
|
||||
def validate_report_count(self):
|
||||
"""check that there are only 3 enabled reports per user"""
|
||||
count = frappe.db.sql(
|
||||
"select count(*) from `tabAuto Email Report` where user=%s and enabled=1", self.user
|
||||
)[0][0]
|
||||
max_reports_per_user = frappe.local.conf.max_reports_per_user or 3
|
||||
count = frappe.db.count("Auto Email Report", {"user": self.user, "enabled": 1})
|
||||
|
||||
max_reports_per_user = (
|
||||
cint(frappe.local.conf.max_reports_per_user) # kept for backward compatibilty
|
||||
or cint(frappe.db.get_single_value("System Settings", "max_auto_email_report_per_user"))
|
||||
or 20
|
||||
)
|
||||
|
||||
if count > max_reports_per_user + (-1 if self.flags.in_insert else 0):
|
||||
frappe.throw(_("Only {0} emailed reports are allowed per user").format(max_reports_per_user))
|
||||
msg = _("Only {0} emailed reports are allowed per user.").format(max_reports_per_user)
|
||||
msg += " " + _("To allow more reports update limit in System Settings.")
|
||||
frappe.throw(msg, title=_("Report limit reached"))
|
||||
|
||||
def validate_report_format(self):
|
||||
"""check if user has select correct report format"""
|
||||
|
|
|
|||
|
|
@ -134,10 +134,11 @@ frappe.ui.form.on("Email Account", {
|
|||
|
||||
show_gmail_message_for_less_secure_apps: function(frm) {
|
||||
frm.dashboard.clear_headline();
|
||||
let msg = __("GMail will only work if you enable 2-step authentication and use app-specific password.");
|
||||
let cta = __("Read the step by step guide here.");
|
||||
msg += ` <a target="_blank" href="https://docs.erpnext.com/docs/v13/user/manual/en/setting-up/email/email_account_setup_with_gmail">${cta}</a>`;
|
||||
if (frm.doc.service==="GMail") {
|
||||
frm.dashboard.set_headline_alert('Gmail will only work if you allow access for less secure \
|
||||
apps in Gmail settings. <a target="_blank" \
|
||||
href="https://support.google.com/accounts/answer/6010255?hl=en">Read this for details</a>');
|
||||
frm.dashboard.set_headline_alert(msg);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -622,11 +622,13 @@ class QueueBuilder:
|
|||
mail_to_string = cstr(mail.as_string())
|
||||
except frappe.InvalidEmailAddressError:
|
||||
# bad Email Address - don't add to queue
|
||||
self.log_error(
|
||||
frappe.log_error(
|
||||
title="Invalid email address",
|
||||
message="Invalid email address Sender: {0}, Recipients: {1}, \nTraceback: {2} ".format(
|
||||
self.sender, ", ".join(self.final_recipients()), traceback.format_exc()
|
||||
),
|
||||
reference_doctype=self.reference_doctype,
|
||||
reference_name=self.reference_name,
|
||||
)
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ class Newsletter(WebsiteGenerator):
|
|||
)
|
||||
|
||||
def get_success_recipients(self) -> List[str]:
|
||||
"""Recipients who have already recieved the newsletter.
|
||||
"""Recipients who have already received the newsletter.
|
||||
|
||||
Couldn't think of a better name ;)
|
||||
"""
|
||||
|
|
@ -132,7 +132,7 @@ class Newsletter(WebsiteGenerator):
|
|||
"Email Queue Recipient",
|
||||
filters={
|
||||
"status": ("in", ["Not Sent", "Sending", "Sent"]),
|
||||
"parentfield": ("in", self.get_linked_email_queue()),
|
||||
"parent": ("in", self.get_linked_email_queue()),
|
||||
},
|
||||
pluck="recipient",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -221,3 +221,24 @@ class TestNewsletter(TestNewsletterMixin, unittest.TestCase):
|
|||
|
||||
newsletter.reload()
|
||||
self.assertEqual(newsletter.email_sent, 0)
|
||||
|
||||
def test_retry_partially_sent_newsletter(self):
|
||||
frappe.db.delete("Email Queue")
|
||||
frappe.db.delete("Email Queue Recipient")
|
||||
frappe.db.delete("Newsletter")
|
||||
|
||||
newsletter = self.get_newsletter()
|
||||
newsletter.send_emails()
|
||||
email_queue_list = [frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue")]
|
||||
self.assertEqual(len(email_queue_list), 4)
|
||||
|
||||
# emulate partial send
|
||||
email_queue_list[0].status = "Error"
|
||||
email_queue_list[0].recipients[0].status = "Error"
|
||||
email_queue_list[0].save()
|
||||
newsletter.email_sent = False
|
||||
|
||||
# retry
|
||||
newsletter.send_emails()
|
||||
email_queue_list = [frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue")]
|
||||
self.assertEqual(len(email_queue_list), 5)
|
||||
|
|
|
|||
|
|
@ -268,9 +268,9 @@ class EMail:
|
|||
self.replace_sender()
|
||||
self.replace_sender_name()
|
||||
|
||||
self.recipients = [strip(r) for r in self.recipients]
|
||||
self.cc = [strip(r) for r in self.cc]
|
||||
self.bcc = [strip(r) for r in self.bcc]
|
||||
self.recipients = [strip(r) for r in self.recipients if r not in frappe.STANDARD_USERS]
|
||||
self.cc = [strip(r) for r in self.cc if r not in frappe.STANDARD_USERS]
|
||||
self.bcc = [strip(r) for r in self.bcc if r not in frappe.STANDARD_USERS]
|
||||
|
||||
for e in self.recipients + (self.cc or []) + (self.bcc or []):
|
||||
validate_email_address(e, True)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -226,7 +226,6 @@ scheduler_events = {
|
|||
"frappe.sessions.clear_expired_sessions",
|
||||
"frappe.email.doctype.notification.notification.trigger_daily_alerts",
|
||||
"frappe.utils.scheduler.restrict_scheduler_events_if_dormant",
|
||||
"frappe.email.doctype.auto_email_report.auto_email_report.send_daily",
|
||||
"frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.remove_unverified_record",
|
||||
"frappe.desk.form.document_follow.send_daily_updates",
|
||||
"frappe.social.doctype.energy_point_settings.energy_point_settings.allocate_review_points",
|
||||
|
|
@ -241,6 +240,7 @@ scheduler_events = {
|
|||
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily",
|
||||
"frappe.utils.change_log.check_for_update",
|
||||
"frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_daily",
|
||||
"frappe.email.doctype.auto_email_report.auto_email_report.send_daily",
|
||||
"frappe.integrations.doctype.google_drive.google_drive.daily_backup",
|
||||
],
|
||||
"weekly_long": [
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ class RazorpaySettings(Document):
|
|||
headers={"content-type": "application/json"},
|
||||
)
|
||||
if not resp.get("id"):
|
||||
frappe.log_error(str(resp), "Razorpay Failed while creating subscription")
|
||||
frappe.log_error(message=str(resp), title="Razorpay Failed while creating subscription")
|
||||
except:
|
||||
frappe.log_error(frappe.get_traceback())
|
||||
# failed
|
||||
|
|
@ -179,7 +179,7 @@ class RazorpaySettings(Document):
|
|||
frappe.flags.status = "created"
|
||||
return kwargs
|
||||
else:
|
||||
frappe.log_error(str(resp), "Razorpay Failed while creating subscription")
|
||||
frappe.log_error(message=str(resp), title="Razorpay Failed while creating subscription")
|
||||
|
||||
except:
|
||||
frappe.log_error(frappe.get_traceback())
|
||||
|
|
@ -281,7 +281,7 @@ class RazorpaySettings(Document):
|
|||
self.flags.status_changed_to = "Verified"
|
||||
|
||||
else:
|
||||
frappe.log_error(str(resp), "Razorpay Payment not authorized")
|
||||
frappe.log_error(message=str(resp), title="Razorpay Payment not authorized")
|
||||
|
||||
except:
|
||||
frappe.log_error(frappe.get_traceback())
|
||||
|
|
|
|||
|
|
@ -36,10 +36,14 @@ data_fieldtypes = (
|
|||
"Geolocation",
|
||||
"Duration",
|
||||
"Icon",
|
||||
"Phone",
|
||||
"Autocomplete",
|
||||
"JSON",
|
||||
)
|
||||
|
||||
float_like_fields = {"Float", "Currency", "Percent"}
|
||||
datetime_fields = {"Datetime", "Date", "Time"}
|
||||
|
||||
attachment_fieldtypes = (
|
||||
"Attach",
|
||||
"Attach Image",
|
||||
|
|
|
|||
|
|
@ -2,10 +2,18 @@
|
|||
# License: MIT. See LICENSE
|
||||
import datetime
|
||||
import json
|
||||
from typing import Dict, List
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model import child_table_fields, default_fields, display_fieldtypes, table_fields
|
||||
from frappe.model import (
|
||||
child_table_fields,
|
||||
datetime_fields,
|
||||
default_fields,
|
||||
display_fieldtypes,
|
||||
float_like_fields,
|
||||
table_fields,
|
||||
)
|
||||
from frappe.model.docstatus import DocStatus
|
||||
from frappe.model.naming import set_new_name
|
||||
from frappe.model.utils.link_count import notify_link_count
|
||||
|
|
@ -233,7 +241,6 @@ class BaseDocument(object):
|
|||
raise AttributeError(key)
|
||||
|
||||
value = get_controller(value["doctype"])(value)
|
||||
value.init_valid_columns()
|
||||
|
||||
value.parent = self.name
|
||||
value.parenttype = self.doctype
|
||||
|
|
@ -252,10 +259,11 @@ class BaseDocument(object):
|
|||
|
||||
def get_valid_dict(
|
||||
self, sanitize=True, convert_dates_to_str=False, ignore_nulls=False, ignore_virtual=False
|
||||
):
|
||||
) -> Dict:
|
||||
d = frappe._dict()
|
||||
for fieldname in self.meta.get_valid_columns():
|
||||
d[fieldname] = self.get(fieldname)
|
||||
# column is valid, we can use getattr
|
||||
d[fieldname] = getattr(self, fieldname, None)
|
||||
|
||||
# if no need for sanitization and value is None, continue
|
||||
if not sanitize and d[fieldname] is None:
|
||||
|
|
@ -263,25 +271,24 @@ class BaseDocument(object):
|
|||
|
||||
df = self.meta.get_field(fieldname)
|
||||
|
||||
if df and df.get("is_virtual"):
|
||||
if ignore_virtual:
|
||||
del d[fieldname]
|
||||
continue
|
||||
if df:
|
||||
if getattr(df, "is_virtual", False):
|
||||
if ignore_virtual:
|
||||
del d[fieldname]
|
||||
continue
|
||||
|
||||
from frappe.utils.safe_exec import get_safe_globals
|
||||
if d[fieldname] is None and (options := getattr(df, "options", None)):
|
||||
from frappe.utils.safe_exec import get_safe_globals
|
||||
|
||||
if d[fieldname] is None:
|
||||
if df.get("options"):
|
||||
d[fieldname] = frappe.safe_eval(
|
||||
code=df.get("options"),
|
||||
code=options,
|
||||
eval_globals=get_safe_globals(),
|
||||
eval_locals={"doc": self},
|
||||
)
|
||||
else:
|
||||
_val = getattr(self, fieldname, None)
|
||||
if _val and not callable(_val):
|
||||
d[fieldname] = _val
|
||||
elif df:
|
||||
|
||||
if isinstance(d[fieldname], list) and df.fieldtype not in table_fields:
|
||||
frappe.throw(_("Value for {0} cannot be a list").format(_(df.label)))
|
||||
|
||||
if df.fieldtype == "Check":
|
||||
d[fieldname] = 1 if cint(d[fieldname]) else 0
|
||||
|
||||
|
|
@ -291,25 +298,20 @@ class BaseDocument(object):
|
|||
elif df.fieldtype == "JSON" and isinstance(d[fieldname], dict):
|
||||
d[fieldname] = json.dumps(d[fieldname], sort_keys=True, indent=4, separators=(",", ": "))
|
||||
|
||||
elif df.fieldtype in ("Currency", "Float", "Percent") and not isinstance(d[fieldname], float):
|
||||
elif df.fieldtype in float_like_fields and not isinstance(d[fieldname], float):
|
||||
d[fieldname] = flt(d[fieldname])
|
||||
|
||||
elif df.fieldtype in ("Datetime", "Date", "Time") and d[fieldname] == "":
|
||||
elif (df.fieldtype in datetime_fields and d[fieldname] == "") or (
|
||||
getattr(df, "unique", False) and cstr(d[fieldname]).strip() == ""
|
||||
):
|
||||
d[fieldname] = None
|
||||
|
||||
elif df.get("unique") and cstr(d[fieldname]).strip() == "":
|
||||
# unique empty field should be set to None
|
||||
d[fieldname] = None
|
||||
|
||||
if isinstance(d[fieldname], list) and df.fieldtype not in table_fields:
|
||||
frappe.throw(_("Value for {0} cannot be a list").format(_(df.label)))
|
||||
|
||||
if convert_dates_to_str and isinstance(
|
||||
d[fieldname], (datetime.datetime, datetime.date, datetime.time, datetime.timedelta)
|
||||
):
|
||||
d[fieldname] = str(d[fieldname])
|
||||
|
||||
if d[fieldname] is None and ignore_nulls:
|
||||
if ignore_nulls and d[fieldname] is None:
|
||||
del d[fieldname]
|
||||
|
||||
return d
|
||||
|
|
@ -329,7 +331,7 @@ class BaseDocument(object):
|
|||
if key not in self.__dict__:
|
||||
self.__dict__[key] = None
|
||||
|
||||
def get_valid_columns(self):
|
||||
def get_valid_columns(self) -> List[str]:
|
||||
if self.doctype not in frappe.local.valid_columns:
|
||||
if self.doctype in DOCTYPES_FOR_DOCTYPE:
|
||||
from frappe.model.meta import get_table_columns
|
||||
|
|
@ -342,12 +344,12 @@ class BaseDocument(object):
|
|||
|
||||
return frappe.local.valid_columns[self.doctype]
|
||||
|
||||
def is_new(self):
|
||||
def is_new(self) -> bool:
|
||||
return self.get("__islocal")
|
||||
|
||||
@property
|
||||
def docstatus(self):
|
||||
return DocStatus(self.get("docstatus"))
|
||||
return DocStatus(cint(self.get("docstatus")))
|
||||
|
||||
@docstatus.setter
|
||||
def docstatus(self, value):
|
||||
|
|
@ -359,8 +361,8 @@ class BaseDocument(object):
|
|||
no_default_fields=False,
|
||||
convert_dates_to_str=False,
|
||||
no_child_table_fields=False,
|
||||
):
|
||||
doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str)
|
||||
) -> Dict:
|
||||
doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str, ignore_nulls=no_nulls)
|
||||
doc["doctype"] = self.doctype
|
||||
|
||||
for df in self.meta.get_table_fields():
|
||||
|
|
@ -375,20 +377,15 @@ class BaseDocument(object):
|
|||
for d in children
|
||||
]
|
||||
|
||||
if no_nulls:
|
||||
for k in list(doc):
|
||||
if doc[k] is None:
|
||||
del doc[k]
|
||||
|
||||
if no_default_fields:
|
||||
for k in list(doc):
|
||||
if k in default_fields:
|
||||
del doc[k]
|
||||
for key in default_fields:
|
||||
if key in doc:
|
||||
del doc[key]
|
||||
|
||||
if no_child_table_fields:
|
||||
for k in list(doc):
|
||||
if k in child_table_fields:
|
||||
del doc[k]
|
||||
for key in child_table_fields:
|
||||
if key in doc:
|
||||
del doc[key]
|
||||
|
||||
for key in (
|
||||
"_user_tags",
|
||||
|
|
@ -398,8 +395,8 @@ class BaseDocument(object):
|
|||
"__run_link_triggers",
|
||||
"__unsaved",
|
||||
):
|
||||
if self.get(key):
|
||||
doc[key] = self.get(key)
|
||||
if value := getattr(self, key, None):
|
||||
doc[key] = value
|
||||
|
||||
return doc
|
||||
|
||||
|
|
@ -771,6 +768,10 @@ class BaseDocument(object):
|
|||
|
||||
def _validate_data_fields(self):
|
||||
# data_field options defined in frappe.model.data_field_options
|
||||
for phone_field in self.meta.get_phone_fields():
|
||||
phone = self.get(phone_field.fieldname)
|
||||
frappe.utils.validate_phone_number_with_country_code(phone, phone_field.fieldname)
|
||||
|
||||
for data_field in self.meta.get_data_fields():
|
||||
data = self.get(data_field.fieldname)
|
||||
data_field_options = data_field.get("options")
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ class DatabaseQuery(object):
|
|||
|
||||
# left join parent, child tables
|
||||
for child in self.tables[1:]:
|
||||
parent_name = self.cast_name(f"{self.tables[0]}.name")
|
||||
parent_name = cast_name(f"{self.tables[0]}.name")
|
||||
args.tables += f" {self.join} {child} on ({child}.parent = {parent_name})"
|
||||
|
||||
if self.grouped_or_conditions:
|
||||
|
|
@ -225,6 +225,7 @@ class DatabaseQuery(object):
|
|||
args.conditions += (" or " if args.conditions else "") + " or ".join(self.or_conditions)
|
||||
|
||||
self.set_field_tables()
|
||||
self.cast_name_fields()
|
||||
|
||||
fields = []
|
||||
|
||||
|
|
@ -385,16 +386,8 @@ class DatabaseQuery(object):
|
|||
]
|
||||
# add tables from fields
|
||||
if self.fields:
|
||||
for i, field in enumerate(self.fields):
|
||||
# add cast in locate/strpos
|
||||
func_found = False
|
||||
for func in sql_functions:
|
||||
if func in field.lower():
|
||||
self.fields[i] = self.cast_name(field, func)
|
||||
func_found = True
|
||||
break
|
||||
|
||||
if func_found or not ("tab" in field and "." in field):
|
||||
for field in self.fields:
|
||||
if not ("tab" in field and "." in field) or any(x for x in sql_functions if x in field):
|
||||
continue
|
||||
|
||||
table_name = field.split(".")[0]
|
||||
|
|
@ -406,38 +399,6 @@ class DatabaseQuery(object):
|
|||
if table_name not in self.tables:
|
||||
self.append_table(table_name)
|
||||
|
||||
def cast_name(
|
||||
self,
|
||||
column: str,
|
||||
sql_function: str = "",
|
||||
) -> str:
|
||||
if frappe.db.db_type == "postgres":
|
||||
if "name" in column.lower():
|
||||
if "cast(" not in column.lower() or "::" not in column:
|
||||
if not sql_function:
|
||||
return f"cast({column} as varchar)"
|
||||
|
||||
elif sql_function == "locate(":
|
||||
return re.sub(
|
||||
r"locate\(([^,]+),([^)]+)\)",
|
||||
r"locate(\1, cast(\2 as varchar))",
|
||||
column,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
|
||||
elif sql_function == "strpos(":
|
||||
return re.sub(
|
||||
r"strpos\(([^,]+),([^)]+)\)",
|
||||
r"strpos(cast(\1 as varchar), \2)",
|
||||
column,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
|
||||
elif sql_function == "ifnull(":
|
||||
return re.sub(r"ifnull\(([^,]+)", r"ifnull(cast(\1 as varchar)", column, flags=re.IGNORECASE)
|
||||
|
||||
return column
|
||||
|
||||
def append_table(self, table_name):
|
||||
self.tables.append(table_name)
|
||||
doctype = table_name[4:-1]
|
||||
|
|
@ -462,6 +423,10 @@ class DatabaseQuery(object):
|
|||
if "." not in field and not _in_standard_sql_methods(field):
|
||||
self.fields[idx] = f"{self.tables[0]}.{field}"
|
||||
|
||||
def cast_name_fields(self):
|
||||
for i, field in enumerate(self.fields):
|
||||
self.fields[i] = cast_name(field)
|
||||
|
||||
def get_table_columns(self):
|
||||
try:
|
||||
return get_table_columns(self.doctype)
|
||||
|
|
@ -541,10 +506,7 @@ class DatabaseQuery(object):
|
|||
if tname not in self.tables:
|
||||
self.append_table(tname)
|
||||
|
||||
if "ifnull(" in f.fieldname:
|
||||
column_name = self.cast_name(f.fieldname, "ifnull(")
|
||||
else:
|
||||
column_name = self.cast_name(f"{tname}.`{f.fieldname}`")
|
||||
column_name = cast_name(f.fieldname if "ifnull(" in f.fieldname else f"{tname}.`{f.fieldname}`")
|
||||
|
||||
if f.operator.lower() in additional_filters_config:
|
||||
f.update(get_additional_filter_field(additional_filters_config, f, f.value))
|
||||
|
|
@ -766,7 +728,10 @@ class DatabaseQuery(object):
|
|||
return self.match_filters
|
||||
|
||||
def get_share_condition(self):
|
||||
return f"`tab{self.doctype}`.name in ({', '.join(frappe.db.escape(s, percent=False) for s in self.shared)})"
|
||||
return (
|
||||
cast_name(f"`tab{self.doctype}`.name")
|
||||
+ f" in ({', '.join(frappe.db.escape(s, percent=False) for s in self.shared)})"
|
||||
)
|
||||
|
||||
def add_user_permissions(self, user_permissions):
|
||||
meta = frappe.get_meta(self.doctype)
|
||||
|
|
@ -794,7 +759,9 @@ class DatabaseQuery(object):
|
|||
if frappe.get_system_settings("apply_strict_user_permissions"):
|
||||
condition = ""
|
||||
else:
|
||||
empty_value_condition = f"ifnull(`tab{self.doctype}`.`{df.get('fieldname')}`, '')=''"
|
||||
empty_value_condition = cast_name(
|
||||
f"ifnull(`tab{self.doctype}`.`{df.get('fieldname')}`, '')=''"
|
||||
)
|
||||
condition = empty_value_condition + " or "
|
||||
|
||||
for permission in user_permission_values:
|
||||
|
|
@ -815,7 +782,7 @@ class DatabaseQuery(object):
|
|||
|
||||
if docs:
|
||||
values = ", ".join(frappe.db.escape(doc, percent=False) for doc in docs)
|
||||
condition += f"`tab{self.doctype}`.`{df.get('fieldname')}` in ({values})"
|
||||
condition += cast_name(f"`tab{self.doctype}`.`{df.get('fieldname')}`") + f" in ({values})"
|
||||
match_conditions.append(f"({condition})")
|
||||
match_filters[df.get("options")] = docs
|
||||
|
||||
|
|
@ -933,6 +900,40 @@ class DatabaseQuery(object):
|
|||
update_user_settings(self.doctype, user_settings)
|
||||
|
||||
|
||||
def cast_name(column: str) -> str:
|
||||
"""Casts name field to varchar for postgres
|
||||
|
||||
Handles majorly 4 cases:
|
||||
1. locate
|
||||
2. strpos
|
||||
3. ifnull
|
||||
4. coalesce
|
||||
|
||||
Uses regex substitution.
|
||||
|
||||
Example:
|
||||
input - "ifnull(`tabBlog Post`.`name`, '')=''"
|
||||
output - "ifnull(cast(`tabBlog Post`.`name` as varchar), '')=''" """
|
||||
|
||||
if frappe.db.db_type == "mariadb":
|
||||
return column
|
||||
|
||||
kwargs = {"string": column, "flags": re.IGNORECASE}
|
||||
if "cast(" not in column.lower() and "::" not in column:
|
||||
if re.search(r"locate\(([^,]+),\s*([`\"]?name[`\"]?)\s*\)", **kwargs):
|
||||
return re.sub(
|
||||
r"locate\(([^,]+),\s*([`\"]?name[`\"]?)\)", r"locate(\1, cast(\2 as varchar))", **kwargs
|
||||
)
|
||||
|
||||
elif match := re.search(r"(strpos|ifnull|coalesce)\(\s*([`\"]?name[`\"]?)\s*,", **kwargs):
|
||||
func = match.groups()[0]
|
||||
return re.sub(rf"{func}\(\s*([`\"]?name[`\"]?)\s*,", rf"{func}(cast(\1 as varchar),", **kwargs)
|
||||
|
||||
return re.sub(r"([`\"]?tab[\w`\" -]+\.[`\"]?name[`\"]?)(?!\w)", r"cast(\1 as varchar)", **kwargs)
|
||||
|
||||
return column
|
||||
|
||||
|
||||
def check_parent_permission(parent, child_doctype):
|
||||
if parent:
|
||||
# User may pass fake parent and get the information from the child table
|
||||
|
|
|
|||
|
|
@ -989,6 +989,16 @@ class Document(BaseDocument):
|
|||
self.docstatus = DocStatus.cancelled()
|
||||
return self.save()
|
||||
|
||||
@whitelist.__func__
|
||||
def _rename(
|
||||
self, name: str, merge: bool = False, force: bool = False, validate_rename: bool = True
|
||||
):
|
||||
"""Rename the document. Triggers frappe.rename_doc, then reloads."""
|
||||
from frappe.model.rename_doc import rename_doc
|
||||
|
||||
self.name = rename_doc(doc=self, new=name, merge=merge, force=force, validate=validate_rename)
|
||||
self.reload()
|
||||
|
||||
@whitelist.__func__
|
||||
def submit(self):
|
||||
"""Submit the document. Sets `docstatus` = 1, then saves."""
|
||||
|
|
@ -999,6 +1009,13 @@ class Document(BaseDocument):
|
|||
"""Cancel the document. Sets `docstatus` = 2, then saves."""
|
||||
return self._cancel()
|
||||
|
||||
@whitelist.__func__
|
||||
def rename(
|
||||
self, name: str, merge: bool = False, force: bool = False, validate_rename: bool = True
|
||||
):
|
||||
"""Rename the document to `name`. This transforms the current object."""
|
||||
return self._rename(name=name, merge=merge, force=force, validate_rename=validate_rename)
|
||||
|
||||
def delete(self, ignore_permissions=False):
|
||||
"""Delete document."""
|
||||
frappe.delete_doc(
|
||||
|
|
@ -1398,21 +1415,22 @@ class Document(BaseDocument):
|
|||
# See: Stock Reconciliation
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
|
||||
if hasattr(self, "_" + action):
|
||||
action = "_" + action
|
||||
if hasattr(self, f"_{action}"):
|
||||
action = f"_{action}"
|
||||
|
||||
if file_lock.lock_exists(self.get_signature()):
|
||||
try:
|
||||
self.lock()
|
||||
except frappe.DocumentLockedError:
|
||||
frappe.throw(
|
||||
_("This document is currently queued for execution. Please try again"),
|
||||
title=_("Document Queued"),
|
||||
)
|
||||
|
||||
self.lock()
|
||||
enqueue(
|
||||
return enqueue(
|
||||
"frappe.model.document.execute_action",
|
||||
doctype=self.doctype,
|
||||
name=self.name,
|
||||
action=action,
|
||||
__doctype=self.doctype,
|
||||
__name=self.name,
|
||||
__action=action,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
|
@ -1433,10 +1451,13 @@ class Document(BaseDocument):
|
|||
if lock_exists:
|
||||
raise frappe.DocumentLockedError
|
||||
file_lock.create_lock(signature)
|
||||
frappe.local.locked_documents.append(self)
|
||||
|
||||
def unlock(self):
|
||||
"""Delete the lock file for this document"""
|
||||
file_lock.delete_lock(self.get_signature())
|
||||
if self in frappe.local.locked_documents:
|
||||
frappe.local.locked_documents.remove(self)
|
||||
|
||||
# validation helpers
|
||||
def validate_from_to_dates(self, from_date_field, to_date_field):
|
||||
|
|
@ -1495,12 +1516,12 @@ class Document(BaseDocument):
|
|||
return f"{doctype}({name})"
|
||||
|
||||
|
||||
def execute_action(doctype, name, action, **kwargs):
|
||||
def execute_action(__doctype, __name, __action, **kwargs):
|
||||
"""Execute an action on a document (called by background worker)"""
|
||||
doc = frappe.get_doc(doctype, name)
|
||||
doc = frappe.get_doc(__doctype, __name)
|
||||
doc.unlock()
|
||||
try:
|
||||
getattr(doc, action)(**kwargs)
|
||||
getattr(doc, __action)(**kwargs)
|
||||
except Exception:
|
||||
frappe.db.rollback()
|
||||
|
||||
|
|
@ -1511,4 +1532,4 @@ def execute_action(doctype, name, action, **kwargs):
|
|||
msg = "<pre><code>" + frappe.get_traceback() + "</pre></code>"
|
||||
|
||||
doc.add_comment("Comment", _("Action Failed") + "<br><br>" + msg)
|
||||
doc.notify_update()
|
||||
doc.notify_update()
|
||||
|
|
|
|||
|
|
@ -162,6 +162,9 @@ class Meta(Document):
|
|||
def get_data_fields(self):
|
||||
return self.get("fields", {"fieldtype": "Data"})
|
||||
|
||||
def get_phone_fields(self):
|
||||
return self.get("fields", {"fieldtype": "Phone"})
|
||||
|
||||
def get_dynamic_link_fields(self):
|
||||
if not hasattr(self, "_dynamic_link_fields"):
|
||||
self._dynamic_link_fields = self.get("fields", {"fieldtype": "Dynamic Link"})
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ if TYPE_CHECKING:
|
|||
from frappe.model.meta import Meta
|
||||
|
||||
|
||||
# NOTE: This is used to keep track of status of sites
|
||||
# whether `log_types` have autoincremented naming set for the site or not.
|
||||
autoincremented_site_status_map = {}
|
||||
|
||||
|
||||
def set_new_name(doc):
|
||||
"""
|
||||
Sets the `name` property for the document based on various rules.
|
||||
|
|
@ -35,9 +40,7 @@ def set_new_name(doc):
|
|||
doc.name = None
|
||||
|
||||
if is_autoincremented(doc.doctype, meta):
|
||||
from frappe.database.sequence import get_next_val
|
||||
|
||||
doc.name = get_next_val(doc.doctype)
|
||||
doc.name = frappe.db.get_next_sequence_val(doc.doctype)
|
||||
return
|
||||
|
||||
if getattr(doc, "amended_from", None):
|
||||
|
|
@ -72,12 +75,11 @@ def set_new_name(doc):
|
|||
doc.name = validate_name(doc.doctype, doc.name, meta.get_field("name_case"))
|
||||
|
||||
|
||||
def is_autoincremented(doctype: str, meta: "Meta" = None):
|
||||
def is_autoincremented(doctype: str, meta: Optional["Meta"] = None) -> bool:
|
||||
"""Checks if the doctype has autoincrement autoname set"""
|
||||
|
||||
if doctype in log_types:
|
||||
if (
|
||||
frappe.local.autoincremented_status_map.get(frappe.local.site) is None
|
||||
or frappe.local.autoincremented_status_map[frappe.local.site] == -1
|
||||
):
|
||||
if autoincremented_site_status_map.get(frappe.local.site) is None:
|
||||
if (
|
||||
frappe.db.sql(
|
||||
f"""select data_type FROM information_schema.columns
|
||||
|
|
@ -85,22 +87,19 @@ def is_autoincremented(doctype: str, meta: "Meta" = None):
|
|||
)[0][0]
|
||||
== "bigint"
|
||||
):
|
||||
frappe.local.autoincremented_status_map[frappe.local.site] = 1
|
||||
autoincremented_site_status_map[frappe.local.site] = 1
|
||||
return True
|
||||
else:
|
||||
frappe.local.autoincremented_status_map[frappe.local.site] = 0
|
||||
autoincremented_site_status_map[frappe.local.site] = 0
|
||||
|
||||
elif frappe.local.autoincremented_status_map[frappe.local.site]:
|
||||
elif autoincremented_site_status_map[frappe.local.site]:
|
||||
return True
|
||||
|
||||
else:
|
||||
if not meta:
|
||||
meta = frappe.get_meta(doctype)
|
||||
|
||||
if getattr(meta, "issingle", False):
|
||||
return False
|
||||
|
||||
if meta.autoname == "autoincrement":
|
||||
if not getattr(meta, "issingle", False) and meta.autoname == "autoincrement":
|
||||
return True
|
||||
|
||||
return False
|
||||
|
|
@ -329,11 +328,9 @@ def validate_name(doctype: str, name: Union[int, str], case: Optional[str] = Non
|
|||
|
||||
if isinstance(name, int):
|
||||
if is_autoincremented(doctype):
|
||||
from frappe.database.sequence import set_next_val
|
||||
|
||||
# this will set the sequence val to be the provided name and set it to be used
|
||||
# so that the sequence will start from the next val of the setted val(name)
|
||||
set_next_val(doctype, name, is_val_used=True)
|
||||
# this will set the sequence value to be the provided name/value and set it to be used
|
||||
# so that the sequence will start from the next value
|
||||
frappe.db.set_next_sequence_val(doctype, name, is_val_used=True)
|
||||
return name
|
||||
|
||||
frappe.throw(_("Invalid name type (integer) for varchar name column"), frappe.NameError)
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@ from typing import TYPE_CHECKING, Dict, List, Optional
|
|||
|
||||
import frappe
|
||||
from frappe import _, bold
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||
from frappe.model.naming import validate_name
|
||||
from frappe.model.utils.user_settings import sync_user_settings, update_user_settings_data
|
||||
from frappe.query_builder import Field
|
||||
from frappe.utils import cint
|
||||
from frappe.query_builder.utils import DocType, Table
|
||||
from frappe.utils.data import sbool
|
||||
from frappe.utils.password import rename_password
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.model.meta import Meta
|
||||
|
|
@ -23,10 +26,19 @@ def update_document_title(
|
|||
title: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
merge: bool = False,
|
||||
enqueue: bool = False,
|
||||
**kwargs,
|
||||
) -> str:
|
||||
"""
|
||||
Update title from header in form view
|
||||
Update the name or title of a document. Returns `name` if document was renamed,
|
||||
`docname` if renaming operation was queued.
|
||||
|
||||
:param doctype: DocType of the document
|
||||
:param docname: Name of the document
|
||||
:param title: New Title of the document
|
||||
:param name: New Name of the document
|
||||
:param merge: Merge the current Document with the existing one if exists
|
||||
:param enqueue: Enqueue the rename operation, title is updated in current process
|
||||
"""
|
||||
|
||||
# to maintain backwards API compatibility
|
||||
|
|
@ -38,6 +50,10 @@ def update_document_title(
|
|||
if not isinstance(obj, (str, type(None))):
|
||||
frappe.throw(f"{obj=} must be of type str or None")
|
||||
|
||||
# handle bad API usages
|
||||
merge = sbool(merge)
|
||||
enqueue = sbool(enqueue)
|
||||
|
||||
doc = frappe.get_doc(doctype, docname)
|
||||
doc.check_permission(permtype="write")
|
||||
|
||||
|
|
@ -49,11 +65,34 @@ def update_document_title(
|
|||
name_updated = updated_name and (updated_name != doc.name)
|
||||
|
||||
if name_updated:
|
||||
docname = rename_doc(doctype=doctype, old=docname, new=updated_name, merge=merge)
|
||||
if enqueue and not is_scheduler_inactive():
|
||||
current_name = doc.name
|
||||
|
||||
# before_name hook may have DocType specific validations or transformations
|
||||
transformed_name = doc.run_method("before_rename", current_name, updated_name, merge)
|
||||
if isinstance(transformed_name, dict):
|
||||
transformed_name = transformed_name.get("new")
|
||||
transformed_name = transformed_name or updated_name
|
||||
|
||||
# run rename validations before queueing
|
||||
# use savepoints to avoid partial renames / commits
|
||||
validate_rename(
|
||||
doctype=doctype,
|
||||
old=current_name,
|
||||
new=transformed_name,
|
||||
meta=doc.meta,
|
||||
merge=merge,
|
||||
save_point=True,
|
||||
)
|
||||
|
||||
doc.queue_action("rename", name=transformed_name, merge=merge)
|
||||
else:
|
||||
doc.rename(updated_name, merge=merge)
|
||||
|
||||
if title_updated:
|
||||
try:
|
||||
frappe.db.set_value(doctype, docname, title_field, updated_title)
|
||||
setattr(doc, title_field, updated_title)
|
||||
doc.save()
|
||||
frappe.msgprint(_("Saved"), alert=True, indicator="green")
|
||||
except Exception as e:
|
||||
if frappe.db.is_duplicate_entry(e):
|
||||
|
|
@ -64,44 +103,64 @@ def update_document_title(
|
|||
)
|
||||
raise
|
||||
|
||||
return docname
|
||||
return doc.name
|
||||
|
||||
|
||||
def rename_doc(
|
||||
doctype: str,
|
||||
old: str,
|
||||
new: str,
|
||||
doctype: Optional[str] = None,
|
||||
old: Optional[str] = None,
|
||||
new: str = None,
|
||||
force: bool = False,
|
||||
merge: bool = False,
|
||||
ignore_permissions: bool = False,
|
||||
ignore_if_exists: bool = False,
|
||||
show_alert: bool = True,
|
||||
rebuild_search: bool = True,
|
||||
doc: Optional[Document] = None,
|
||||
validate: bool = True,
|
||||
) -> str:
|
||||
"""Rename a doc(dt, old) to doc(dt, new) and update all linked fields of type "Link"."""
|
||||
if not frappe.db.exists(doctype, old):
|
||||
frappe.errprint(_("Failed: {0} to {1} because {0} doesn't exist.").format(old, new))
|
||||
return
|
||||
"""Rename a doc(dt, old) to doc(dt, new) and update all linked fields of type "Link".
|
||||
|
||||
if ignore_if_exists and frappe.db.exists(doctype, new):
|
||||
frappe.errprint(_("Failed: {0} to {1} because {1} already exists.").format(old, new))
|
||||
return
|
||||
doc: Document object to be renamed.
|
||||
new: New name for the record. If None, and doctype is specified, new name may be automatically generated via before_rename hooks.
|
||||
doctype: DocType of the document. Not required if doc is passed.
|
||||
old: Current name of the document. Not required if doc is passed.
|
||||
force: Allow even if document is not allowed to be renamed.
|
||||
merge: Merge with existing document of new name.
|
||||
ignore_permissions: Ignore user permissions while renaming.
|
||||
ignore_if_exists: Don't raise exception if document with new name already exists. This will quietely overwrite the existing document.
|
||||
show_alert: Display alert if document is renamed successfully.
|
||||
rebuild_search: Rebuild linked doctype search after renaming.
|
||||
validate: Validate before renaming. If False, it is assumed that the caller has already validated.
|
||||
"""
|
||||
old_usage_style = doctype and old and new
|
||||
new_usage_style = doc and new
|
||||
|
||||
if old == new:
|
||||
frappe.errprint(
|
||||
_("Ignored: {0} to {1} no changes made because old and new name are the same.").format(old, new)
|
||||
if not (new_usage_style or old_usage_style):
|
||||
raise TypeError(
|
||||
"{doctype, old, new} or {doc, new} are required arguments for frappe.model.rename_doc"
|
||||
)
|
||||
return
|
||||
|
||||
force = cint(force)
|
||||
merge = cint(merge)
|
||||
old = old or doc.name
|
||||
doctype = doctype or doc.doctype
|
||||
force = sbool(force)
|
||||
merge = sbool(merge)
|
||||
meta = frappe.get_meta(doctype)
|
||||
|
||||
# call before_rename
|
||||
old_doc = frappe.get_doc(doctype, old)
|
||||
out = old_doc.run_method("before_rename", old, new, merge) or {}
|
||||
new = (out.get("new") or new) if isinstance(out, dict) else (out or new)
|
||||
new = validate_rename(doctype, new, meta, merge, force, ignore_permissions)
|
||||
if validate:
|
||||
old_doc = doc or frappe.get_doc(doctype, old)
|
||||
out = old_doc.run_method("before_rename", old, new, merge) or {}
|
||||
new = (out.get("new") or new) if isinstance(out, dict) else (out or new)
|
||||
new = validate_rename(
|
||||
doctype=doctype,
|
||||
old=old,
|
||||
new=new,
|
||||
meta=meta,
|
||||
merge=merge,
|
||||
force=force,
|
||||
ignore_permissions=ignore_permissions,
|
||||
ignore_if_exists=ignore_if_exists,
|
||||
)
|
||||
|
||||
if not merge:
|
||||
rename_parent_and_child(doctype, old, new, meta)
|
||||
|
|
@ -139,11 +198,12 @@ def rename_doc(
|
|||
rename_password(doctype, old, new)
|
||||
|
||||
# update user_permissions
|
||||
frappe.db.sql(
|
||||
"""UPDATE `tabDefaultValue` SET `defvalue`=%s WHERE `parenttype`='User Permission'
|
||||
AND `defkey`=%s AND `defvalue`=%s""",
|
||||
(new, doctype, old),
|
||||
)
|
||||
DefaultValue = DocType("DefaultValue")
|
||||
frappe.qb.update(DefaultValue).set(DefaultValue.defvalue, new).where(
|
||||
(DefaultValue.parenttype == "User Permission")
|
||||
& (DefaultValue.defkey == doctype)
|
||||
& (DefaultValue.defvalue == old)
|
||||
).run()
|
||||
|
||||
if merge:
|
||||
new_doc.add_comment("Edit", _("merged {0} into {1}").format(frappe.bold(old), frappe.bold(new)))
|
||||
|
|
@ -207,15 +267,13 @@ def update_user_settings(old: str, new: str, link_fields: List[Dict]) -> None:
|
|||
|
||||
# find the user settings for the linked doctypes
|
||||
linked_doctypes = {d.parent for d in link_fields if not d.issingle}
|
||||
user_settings_details = frappe.db.sql(
|
||||
"""SELECT `user`, `doctype`, `data`
|
||||
FROM `__UserSettings`
|
||||
WHERE `data` like %s
|
||||
AND `doctype` IN ('{doctypes}')""".format(
|
||||
doctypes="', '".join(linked_doctypes)
|
||||
),
|
||||
(old),
|
||||
as_dict=1,
|
||||
UserSettings = Table("__UserSettings")
|
||||
|
||||
user_settings_details = (
|
||||
frappe.qb.from_(UserSettings)
|
||||
.select("user", "doctype", "data")
|
||||
.where(UserSettings.data.like(old) & UserSettings.doctype.isin(linked_doctypes))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
# create the dict using the doctype name as key and values as list of the user settings
|
||||
|
|
@ -240,37 +298,33 @@ def update_customizations(old: str, new: str) -> None:
|
|||
|
||||
|
||||
def update_attachments(doctype: str, old: str, new: str) -> None:
|
||||
try:
|
||||
if old != "File Data" and doctype != "DocType":
|
||||
frappe.db.sql(
|
||||
"""update `tabFile` set attached_to_name=%s
|
||||
where attached_to_name=%s and attached_to_doctype=%s""",
|
||||
(new, old, doctype),
|
||||
)
|
||||
except frappe.db.ProgrammingError as e:
|
||||
if not frappe.db.is_column_missing(e):
|
||||
raise
|
||||
if doctype != "DocType":
|
||||
File = DocType("File")
|
||||
|
||||
frappe.qb.update(File).set(File.attached_to_name, new).where(
|
||||
(File.attached_to_name == old) & (File.attached_to_doctype == doctype)
|
||||
).run()
|
||||
|
||||
|
||||
def rename_versions(doctype: str, old: str, new: str) -> None:
|
||||
frappe.db.sql(
|
||||
"""UPDATE `tabVersion` SET `docname`=%s WHERE `ref_doctype`=%s AND `docname`=%s""",
|
||||
(new, doctype, old),
|
||||
)
|
||||
Version = DocType("Version")
|
||||
|
||||
frappe.qb.update(Version).set(Version.docname, new).where(
|
||||
(Version.docname == old) & (Version.ref_doctype == doctype)
|
||||
).run()
|
||||
|
||||
|
||||
def rename_eps_records(doctype: str, old: str, new: str) -> None:
|
||||
epl = frappe.qb.DocType("Energy Point Log")
|
||||
(
|
||||
frappe.qb.update(epl)
|
||||
.set(epl.reference_name, new)
|
||||
.where((epl.reference_doctype == doctype) & (epl.reference_name == old))
|
||||
EPL = DocType("Energy Point Log")
|
||||
|
||||
frappe.qb.update(EPL).set(EPL.reference_name, new).where(
|
||||
(EPL.reference_doctype == doctype) & (EPL.reference_name == old)
|
||||
).run()
|
||||
|
||||
|
||||
def rename_parent_and_child(doctype: str, old: str, new: str, meta: "Meta") -> None:
|
||||
# rename the doc
|
||||
frappe.db.sql("UPDATE `tab{0}` SET `name`={1} WHERE `name`={1}".format(doctype, "%s"), (new, old))
|
||||
frappe.qb.update(doctype).set("name", new).where(Field("name") == old).run()
|
||||
|
||||
update_autoname_field(doctype, new, meta)
|
||||
update_child_docs(old, new, meta)
|
||||
|
||||
|
|
@ -280,20 +334,36 @@ def update_autoname_field(doctype: str, new: str, meta: "Meta") -> None:
|
|||
if meta.get("autoname"):
|
||||
field = meta.get("autoname").split(":")
|
||||
if field and field[0] == "field":
|
||||
frappe.db.sql(
|
||||
"UPDATE `tab{0}` SET `{1}`={2} WHERE `name`={2}".format(doctype, field[1], "%s"), (new, new)
|
||||
)
|
||||
frappe.qb.update(doctype).set(field[1], new).where(Field("name") == new).run()
|
||||
|
||||
|
||||
def validate_rename(
|
||||
doctype: str, new: str, meta: "Meta", merge: bool, force: bool, ignore_permissions: bool
|
||||
doctype: str,
|
||||
old: str,
|
||||
new: str,
|
||||
meta: "Meta",
|
||||
merge: bool,
|
||||
force: bool = False,
|
||||
ignore_permissions: bool = False,
|
||||
ignore_if_exists: bool = False,
|
||||
save_point=False,
|
||||
) -> str:
|
||||
# using for update so that it gets locked and someone else cannot edit it while this rename is going on!
|
||||
if save_point:
|
||||
_SAVE_POINT = f"validate_rename_{frappe.generate_hash(length=8)}"
|
||||
frappe.db.savepoint(_SAVE_POINT)
|
||||
|
||||
exists = (
|
||||
frappe.qb.from_(doctype).where(Field("name") == new).for_update().select("name").run(pluck=True)
|
||||
)
|
||||
exists = exists[0] if exists else None
|
||||
|
||||
if not frappe.db.exists(doctype, old):
|
||||
frappe.throw(_("Can't rename {0} to {1} because {0} doesn't exist.").format(old, new))
|
||||
|
||||
if old == new:
|
||||
frappe.throw(_("No changes made because old and new name are the same.").format(old, new))
|
||||
|
||||
if merge and not exists:
|
||||
frappe.throw(_("{0} {1} does not exist, select a new target to merge").format(doctype, new))
|
||||
|
||||
|
|
@ -301,7 +371,7 @@ def validate_rename(
|
|||
# for fixing case, accents
|
||||
exists = None
|
||||
|
||||
if (not merge) and exists:
|
||||
if not merge and exists and not ignore_if_exists:
|
||||
frappe.throw(_("Another {0} with name {1} exists, select another name").format(doctype, new))
|
||||
|
||||
if not (
|
||||
|
|
@ -315,6 +385,9 @@ def validate_rename(
|
|||
# validate naming like it's done in doc.py
|
||||
new = validate_name(doctype, new)
|
||||
|
||||
if save_point:
|
||||
frappe.db.rollback(save_point=_SAVE_POINT)
|
||||
|
||||
return new
|
||||
|
||||
|
||||
|
|
@ -337,9 +410,7 @@ def rename_doctype(doctype: str, old: str, new: str) -> None:
|
|||
def update_child_docs(old: str, new: str, meta: "Meta") -> None:
|
||||
# update "parent"
|
||||
for df in meta.get_table_fields():
|
||||
frappe.db.sql(
|
||||
"update `tab%s` set parent=%s where parent=%s" % (df.options, "%s", "%s"), (new, old)
|
||||
)
|
||||
frappe.qb.update(df.options).set("parent", new).where(Field("parent") == old).run()
|
||||
|
||||
|
||||
def update_link_field_values(link_fields: List[Dict], old: str, new: str, doctype: str) -> None:
|
||||
|
|
@ -384,57 +455,46 @@ def get_link_fields(doctype: str) -> List[Dict]:
|
|||
frappe.flags.link_fields = {}
|
||||
|
||||
if doctype not in frappe.flags.link_fields:
|
||||
link_fields = frappe.db.sql(
|
||||
"""\
|
||||
select parent, fieldname,
|
||||
(select issingle from tabDocType dt
|
||||
where dt.name = df.parent) as issingle
|
||||
from tabDocField df
|
||||
where
|
||||
df.options=%s and df.fieldtype='Link'""",
|
||||
(doctype,),
|
||||
as_dict=1,
|
||||
dt = DocType("DocType")
|
||||
df = DocType("DocField")
|
||||
cf = DocType("Custom Field")
|
||||
ps = DocType("Property Setter")
|
||||
|
||||
st_issingle = frappe.qb.from_(dt).select(dt.issingle).where(dt.name == df.parent).as_("issingle")
|
||||
standard_fields = (
|
||||
frappe.qb.from_(df)
|
||||
.select(df.parent, df.fieldname, st_issingle)
|
||||
.where((df.options == doctype) & (df.fieldtype == "Link"))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
# get link fields from tabCustom Field
|
||||
custom_link_fields = frappe.db.sql(
|
||||
"""\
|
||||
select dt as parent, fieldname,
|
||||
(select issingle from tabDocType dt
|
||||
where dt.name = df.dt) as issingle
|
||||
from `tabCustom Field` df
|
||||
where
|
||||
df.options=%s and df.fieldtype='Link'""",
|
||||
(doctype,),
|
||||
as_dict=1,
|
||||
cf_issingle = frappe.qb.from_(dt).select(dt.issingle).where(dt.name == cf.dt).as_("issingle")
|
||||
custom_fields = (
|
||||
frappe.qb.from_(cf)
|
||||
.select(cf.dt.as_("parent"), cf.fieldname, cf_issingle)
|
||||
.where((cf.options == doctype) & (cf.fieldtype == "Link"))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
# add custom link fields list to link fields list
|
||||
link_fields += custom_link_fields
|
||||
|
||||
# remove fields whose options have been changed using property setter
|
||||
property_setter_link_fields = frappe.db.sql(
|
||||
"""\
|
||||
select ps.doc_type as parent, ps.field_name as fieldname,
|
||||
(select issingle from tabDocType dt
|
||||
where dt.name = ps.doc_type) as issingle
|
||||
from `tabProperty Setter` ps
|
||||
where
|
||||
ps.property_type='options' and
|
||||
ps.field_name is not null and
|
||||
ps.value=%s""",
|
||||
(doctype,),
|
||||
as_dict=1,
|
||||
ps_issingle = (
|
||||
frappe.qb.from_(dt).select(dt.issingle).where(dt.name == ps.doc_type).as_("issingle")
|
||||
)
|
||||
property_setter_fields = (
|
||||
frappe.qb.from_(ps)
|
||||
.select(ps.doc_type.as_("parent"), ps.field_name.as_("fieldname"), ps_issingle)
|
||||
.where((ps.property == "options") & (ps.value == doctype) & (ps.field_name.notnull()))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
link_fields += property_setter_link_fields
|
||||
|
||||
frappe.flags.link_fields[doctype] = link_fields
|
||||
frappe.flags.link_fields[doctype] = standard_fields + custom_fields + property_setter_fields
|
||||
|
||||
return frappe.flags.link_fields[doctype]
|
||||
|
||||
|
||||
def update_options_for_fieldtype(fieldtype: str, old: str, new: str) -> None:
|
||||
CustomField = DocType("Custom Field")
|
||||
PropertySetter = DocType("Property Setter")
|
||||
|
||||
if frappe.conf.developer_mode:
|
||||
for name in frappe.get_all("DocField", filters={"options": old}, pluck="parent"):
|
||||
doctype = frappe.get_doc("DocType", name)
|
||||
|
|
@ -446,23 +506,18 @@ def update_options_for_fieldtype(fieldtype: str, old: str, new: str) -> None:
|
|||
if save:
|
||||
doctype.save()
|
||||
else:
|
||||
frappe.db.sql(
|
||||
"""update `tabDocField` set options=%s
|
||||
where fieldtype=%s and options=%s""",
|
||||
(new, fieldtype, old),
|
||||
)
|
||||
DocField = DocType("DocField")
|
||||
frappe.qb.update(DocField).set(DocField.options, new).where(
|
||||
(DocField.fieldtype == fieldtype) & (DocField.options == old)
|
||||
).run()
|
||||
|
||||
frappe.db.sql(
|
||||
"""update `tabCustom Field` set options=%s
|
||||
where fieldtype=%s and options=%s""",
|
||||
(new, fieldtype, old),
|
||||
)
|
||||
frappe.qb.update(CustomField).set(CustomField.options, new).where(
|
||||
(CustomField.fieldtype == fieldtype) & (CustomField.options == old)
|
||||
).run()
|
||||
|
||||
frappe.db.sql(
|
||||
"""update `tabProperty Setter` set value=%s
|
||||
where property='options' and value=%s""",
|
||||
(new, old),
|
||||
)
|
||||
frappe.qb.update(PropertySetter).set(PropertySetter.value, new).where(
|
||||
(PropertySetter.property == "options") & (PropertySetter.value == old)
|
||||
).run()
|
||||
|
||||
|
||||
def get_select_fields(old: str, new: str) -> List[Dict]:
|
||||
|
|
@ -470,108 +525,87 @@ def get_select_fields(old: str, new: str) -> List[Dict]:
|
|||
get select type fields where doctype's name is hardcoded as
|
||||
new line separated list
|
||||
"""
|
||||
df = DocType("DocField")
|
||||
dt = DocType("DocType")
|
||||
cf = DocType("Custom Field")
|
||||
ps = DocType("Property Setter")
|
||||
|
||||
# get link fields from tabDocField
|
||||
select_fields = frappe.db.sql(
|
||||
"""
|
||||
select parent, fieldname,
|
||||
(select issingle from tabDocType dt
|
||||
where dt.name = df.parent) as issingle
|
||||
from tabDocField df
|
||||
where
|
||||
df.parent != %s and df.fieldtype = 'Select' and
|
||||
df.options like {0} """.format(
|
||||
frappe.db.escape("%" + old + "%")
|
||||
),
|
||||
(new,),
|
||||
as_dict=1,
|
||||
st_issingle = frappe.qb.from_(dt).select(dt.issingle).where(dt.name == df.parent).as_("issingle")
|
||||
standard_fields = (
|
||||
frappe.qb.from_(df)
|
||||
.select(df.parent, df.fieldname, st_issingle)
|
||||
.where((df.parent != new) & (df.fieldtype == "Select") & (df.options.like(f"%{old}%")))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
# get link fields from tabCustom Field
|
||||
custom_select_fields = frappe.db.sql(
|
||||
"""
|
||||
select dt as parent, fieldname,
|
||||
(select issingle from tabDocType dt
|
||||
where dt.name = df.dt) as issingle
|
||||
from `tabCustom Field` df
|
||||
where
|
||||
df.dt != %s and df.fieldtype = 'Select' and
|
||||
df.options like {0} """.format(
|
||||
frappe.db.escape("%" + old + "%")
|
||||
),
|
||||
(new,),
|
||||
as_dict=1,
|
||||
cf_issingle = frappe.qb.from_(dt).select(dt.issingle).where(dt.name == cf.dt).as_("issingle")
|
||||
custom_select_fields = (
|
||||
frappe.qb.from_(cf)
|
||||
.select(cf.dt.as_("parent"), cf.fieldname, cf_issingle)
|
||||
.where((cf.dt != new) & (cf.fieldtype == "Select") & (cf.options.like(f"%{old}%")))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
# add custom link fields list to link fields list
|
||||
select_fields += custom_select_fields
|
||||
|
||||
# remove fields whose options have been changed using property setter
|
||||
property_setter_select_fields = frappe.db.sql(
|
||||
"""
|
||||
select ps.doc_type as parent, ps.field_name as fieldname,
|
||||
(select issingle from tabDocType dt
|
||||
where dt.name = ps.doc_type) as issingle
|
||||
from `tabProperty Setter` ps
|
||||
where
|
||||
ps.doc_type != %s and
|
||||
ps.property_type='options' and
|
||||
ps.field_name is not null and
|
||||
ps.value like {0} """.format(
|
||||
frappe.db.escape("%" + old + "%")
|
||||
),
|
||||
(new,),
|
||||
as_dict=1,
|
||||
ps_issingle = (
|
||||
frappe.qb.from_(dt).select(dt.issingle).where(dt.name == ps.doc_type).as_("issingle")
|
||||
)
|
||||
property_setter_select_fields = (
|
||||
frappe.qb.from_(ps)
|
||||
.select(ps.doc_type.as_("parent"), ps.field_name.as_("fieldname"), ps_issingle)
|
||||
.where(
|
||||
(ps.doc_type != new)
|
||||
& (ps.property == "options")
|
||||
& (ps.field_name.notnull())
|
||||
& (ps.value.like(f"%{old}%"))
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
select_fields += property_setter_select_fields
|
||||
|
||||
return select_fields
|
||||
return standard_fields + custom_select_fields + property_setter_select_fields
|
||||
|
||||
|
||||
def update_select_field_values(old: str, new: str):
|
||||
frappe.db.sql(
|
||||
"""
|
||||
update `tabDocField` set options=replace(options, %s, %s)
|
||||
where
|
||||
parent != %s and fieldtype = 'Select' and
|
||||
(options like {0} or options like {1})""".format(
|
||||
frappe.db.escape("%" + "\n" + old + "%"), frappe.db.escape("%" + old + "\n" + "%")
|
||||
),
|
||||
(old, new, new),
|
||||
)
|
||||
from frappe.query_builder.functions import Replace
|
||||
|
||||
frappe.db.sql(
|
||||
"""
|
||||
update `tabCustom Field` set options=replace(options, %s, %s)
|
||||
where
|
||||
dt != %s and fieldtype = 'Select' and
|
||||
(options like {0} or options like {1})""".format(
|
||||
frappe.db.escape("%" + "\n" + old + "%"), frappe.db.escape("%" + old + "\n" + "%")
|
||||
),
|
||||
(old, new, new),
|
||||
)
|
||||
DocField = DocType("DocField")
|
||||
CustomField = DocType("Custom Field")
|
||||
PropertySetter = DocType("Property Setter")
|
||||
|
||||
frappe.db.sql(
|
||||
"""
|
||||
update `tabProperty Setter` set value=replace(value, %s, %s)
|
||||
where
|
||||
doc_type != %s and field_name is not null and
|
||||
property='options' and
|
||||
(value like {0} or value like {1})""".format(
|
||||
frappe.db.escape("%" + "\n" + old + "%"), frappe.db.escape("%" + old + "\n" + "%")
|
||||
),
|
||||
(old, new, new),
|
||||
)
|
||||
frappe.qb.update(DocField).set(DocField.options, Replace(DocField.options, old, new)).where(
|
||||
(DocField.fieldtype == "Select")
|
||||
& (DocField.parent != new)
|
||||
& (DocField.options.like(f"%\n{old}%") | DocField.options.like(f"%{old}\n%"))
|
||||
).run()
|
||||
|
||||
frappe.qb.update(CustomField).set(
|
||||
CustomField.options, Replace(CustomField.options, old, new)
|
||||
).where(
|
||||
(CustomField.fieldtype == "Select")
|
||||
& (CustomField.dt != new)
|
||||
& (CustomField.options.like(f"%\n{old}%") | CustomField.options.like(f"%{old}\n%"))
|
||||
).run()
|
||||
|
||||
frappe.qb.update(PropertySetter).set(
|
||||
PropertySetter.value, Replace(PropertySetter.value, old, new)
|
||||
).where(
|
||||
(PropertySetter.property == "options")
|
||||
& (PropertySetter.field_name.notnull())
|
||||
& (PropertySetter.doc_type != new)
|
||||
& (PropertySetter.value.like(f"%\n{old}%") | PropertySetter.value.like(f"%{old}\n%"))
|
||||
).run()
|
||||
|
||||
|
||||
def update_parenttype_values(old: str, new: str):
|
||||
child_doctypes = frappe.db.get_all(
|
||||
child_doctypes = frappe.get_all(
|
||||
"DocField",
|
||||
fields=["options", "fieldname"],
|
||||
filters={"parent": new, "fieldtype": ["in", frappe.model.table_fields]},
|
||||
)
|
||||
|
||||
custom_child_doctypes = frappe.db.get_all(
|
||||
custom_child_doctypes = frappe.get_all(
|
||||
"Custom Field",
|
||||
fields=["options", "fieldname"],
|
||||
filters={"dt": new, "fieldtype": ["in", frappe.model.table_fields]},
|
||||
|
|
@ -586,35 +620,30 @@ def update_parenttype_values(old: str, new: str):
|
|||
pluck="value",
|
||||
)
|
||||
|
||||
child_doctypes = list(d["options"] for d in child_doctypes)
|
||||
child_doctypes += property_setter_child_doctypes
|
||||
child_doctypes = set(list(d["options"] for d in child_doctypes) + property_setter_child_doctypes)
|
||||
|
||||
for doctype in child_doctypes:
|
||||
frappe.db.sql(f"update `tab{doctype}` set parenttype=%s where parenttype=%s", (new, old))
|
||||
Table = DocType(doctype)
|
||||
frappe.qb.update(Table).set(Table.parenttype, new).where(Table.parenttype == old).run()
|
||||
|
||||
|
||||
def rename_dynamic_links(doctype: str, old: str, new: str):
|
||||
Singles = DocType("Singles")
|
||||
for df in get_dynamic_link_map().get(doctype, []):
|
||||
# dynamic link in single, just one value to check
|
||||
if frappe.get_meta(df.parent).issingle:
|
||||
refdoc = frappe.db.get_singles_dict(df.parent)
|
||||
if refdoc.get(df.options) == doctype and refdoc.get(df.fieldname) == old:
|
||||
|
||||
frappe.db.sql(
|
||||
"""update tabSingles set value=%s where
|
||||
field=%s and value=%s and doctype=%s""",
|
||||
(new, df.fieldname, old, df.parent),
|
||||
)
|
||||
frappe.qb.update(Singles).set(Singles.value, new).where(
|
||||
(Singles.field == df.fieldname) & (Singles.doctype == df.parent) & (Singles.value == old)
|
||||
).run()
|
||||
else:
|
||||
# because the table hasn't been renamed yet!
|
||||
parent = df.parent if df.parent != new else old
|
||||
frappe.db.sql(
|
||||
"""update `tab{parent}` set {fieldname}=%s
|
||||
where {options}=%s and {fieldname}=%s""".format(
|
||||
parent=parent, fieldname=df.fieldname, options=df.options
|
||||
),
|
||||
(new, doctype, old),
|
||||
)
|
||||
|
||||
frappe.qb.update(parent).set(df.fieldname, new).where(
|
||||
(Field(df.options) == doctype) & (Field(df.fieldname) == old)
|
||||
).run()
|
||||
|
||||
|
||||
def bulk_rename(
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class ParallelTestRunner:
|
|||
|
||||
if hasattr(test_module, "global_test_dependencies"):
|
||||
for doctype in test_module.global_test_dependencies:
|
||||
make_test_records(doctype)
|
||||
make_test_records(doctype, commit=True)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
elapsed = click.style(f" ({elapsed:.03}s)", fg="red")
|
||||
|
|
@ -76,7 +76,7 @@ class ParallelTestRunner:
|
|||
def create_test_dependency_records(self, module, path, filename):
|
||||
if hasattr(module, "test_dependencies"):
|
||||
for doctype in module.test_dependencies:
|
||||
make_test_records(doctype)
|
||||
make_test_records(doctype, commit=True)
|
||||
|
||||
if os.path.basename(os.path.dirname(path)) == "doctype":
|
||||
# test_data_migration_connector.py > data_migration_connector.json
|
||||
|
|
@ -86,7 +86,7 @@ class ParallelTestRunner:
|
|||
with open(test_record_file_path, "r") as f:
|
||||
doc = json.loads(f.read())
|
||||
doctype = doc["name"]
|
||||
make_test_records(doctype)
|
||||
make_test_records(doctype, commit=True)
|
||||
|
||||
def get_module(self, path, filename):
|
||||
app_path = frappe.get_pymodule_path(self.app)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ frappe.provide("frappe.model");
|
|||
apply to both DocType form and customize form.
|
||||
*/
|
||||
frappe.model.DocTypeController = class DocTypeController extends frappe.ui.form.Controller {
|
||||
|
||||
max_attachments() {
|
||||
if (!this.frm.doc.max_attachments) {
|
||||
return;
|
||||
|
|
@ -20,4 +19,74 @@ frappe.model.DocTypeController = class DocTypeController extends frappe.ui.form.
|
|||
__("Number of attachment fields are more than {}, limit updated to {}.", [label, no_of_attach_fields]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
naming_rule() {
|
||||
// set the "autoname" property based on naming_rule
|
||||
if (this.frm.doc.naming_rule && !this.frm.__from_autoname) {
|
||||
|
||||
// flag to avoid recursion
|
||||
this.frm.__from_naming_rule = true;
|
||||
|
||||
const naming_rule_default_autoname_map = {
|
||||
"Autoincrement": "autoincrement",
|
||||
"Set by user": "prompt",
|
||||
"By fieldname": "field:",
|
||||
'By "Naming Series" field': "naming_series:",
|
||||
"Expression": "format:",
|
||||
"Expression (sld style)": "",
|
||||
"Random": "hash",
|
||||
"By script": ""
|
||||
};
|
||||
this.frm.set_value("autoname", naming_rule_default_autoname_map[this.frm.doc.naming_rule] || "");
|
||||
setTimeout(() => (this.frm.__from_naming_rule = false), 500);
|
||||
|
||||
this.set_naming_rule_description();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
set_naming_rule_description() {
|
||||
let naming_rule_description = {
|
||||
'Set by user': '',
|
||||
'Autoincrement': 'Uses Auto Increment feature of database.<br><b>WARNING: After using this option, any other naming option will not be accessible.</b>',
|
||||
'By fieldname': 'Format: <code>field:[fieldname]</code>. Valid fieldname must exist',
|
||||
'By "Naming Series" field': 'Format: <code>naming_series:[fieldname]</code>. Default fieldname is <code>naming_series</code>',
|
||||
'Expression': 'Format: <code>format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####}</code> - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.',
|
||||
'Expression (old style)': 'Format: <code>EXAMPLE-.#####</code> Series by prefix (separated by a dot)',
|
||||
'Random': '',
|
||||
'By script': ''
|
||||
};
|
||||
|
||||
if (this.frm.doc.naming_rule) {
|
||||
this.frm.get_field('autoname').set_description(naming_rule_description[this.frm.doc.naming_rule]);
|
||||
}
|
||||
}
|
||||
|
||||
autoname() {
|
||||
// set naming_rule based on autoname (for old doctypes where its not been set)
|
||||
if (this.frm.doc.autoname && !this.frm.doc.naming_rule && !this.frm.__from_naming_rule) {
|
||||
// flag to avoid recursion
|
||||
this.frm.__from_autoname = true;
|
||||
const autoname = this.frm.doc.autoname.toLowerCase();
|
||||
|
||||
if (autoname === "prompt")
|
||||
this.frm.set_value("naming_rule", "Set by user");
|
||||
else if (autoname === "autoincrement")
|
||||
this.frm.set_value("naming_rule", "Autoincrement");
|
||||
else if (autoname.startsWith("field:"))
|
||||
this.frm.set_value("naming_rule", "By fieldname");
|
||||
else if (autoname.startsWith("naming_series:"))
|
||||
this.frm.set_value("naming_rule", 'By "Naming Series" field');
|
||||
else if (autoname.startsWith("format:"))
|
||||
this.frm.set_value("naming_rule", "Expression");
|
||||
else if (autoname === "hash")
|
||||
this.frm.set_value("naming_rule", "Random");
|
||||
else
|
||||
this.frm.set_value("naming_rule", "Expression (old style)");
|
||||
|
||||
setTimeout(() => (this.frm.__from_autoname = false), 500);
|
||||
}
|
||||
|
||||
this.frm.set_df_property('fields', 'reqd', this.frm.doc.autoname !== 'Prompt');
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -65,11 +65,7 @@ frappe.ui.form.ControlInput = class ControlInput extends frappe.ui.form.Control
|
|||
};
|
||||
|
||||
var update_input = function() {
|
||||
if (me.doctype && me.docname) {
|
||||
me.set_input(me.value);
|
||||
} else {
|
||||
me.set_input(me.value || null);
|
||||
}
|
||||
me.set_input(me.value);
|
||||
};
|
||||
|
||||
if (me.disp_status != "None") {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import './multiselect_list';
|
|||
import './rating';
|
||||
import './duration';
|
||||
import './icon';
|
||||
import './phone';
|
||||
import './json';
|
||||
|
||||
frappe.ui.form.make_control = function (opts) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ frappe.ui.form.ControlIcon = class ControlIcon extends frappe.ui.form.ControlDat
|
|||
get_all_icons() {
|
||||
frappe.symbols = [];
|
||||
$("#frappe-symbols > symbol[id]").each(function() {
|
||||
frappe.symbols.push(this.id.replace('icon-', ''));
|
||||
this.id.includes('icon-') && frappe.symbols.push(this.id.replace('icon-', ''));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
|
|||
no_spinner: true,
|
||||
args: args,
|
||||
callback: function(r) {
|
||||
if(!me.$input.is(":focus")) {
|
||||
if (!window.Cypress && !me.$input.is(":focus")) {
|
||||
return;
|
||||
}
|
||||
r.results = me.merge_duplicates(r.results);
|
||||
|
|
|
|||
197
frappe/public/js/frappe/form/controls/phone.js
Normal file
197
frappe/public/js/frappe/form/controls/phone.js
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
|
||||
import PhonePicker from '../../phone_picker/phone_picker';
|
||||
|
||||
frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlData {
|
||||
|
||||
make_input() {
|
||||
super.make_input();
|
||||
this.setup_country_code_picker();
|
||||
this.input_events();
|
||||
}
|
||||
|
||||
input_events() {
|
||||
this.$input.keydown((e) => {
|
||||
const key_code = e.keyCode;
|
||||
if ([frappe.ui.keyCode.BACKSPACE].includes(key_code)) {
|
||||
if (this.$input.val().length == 0) {
|
||||
this.country_code_picker.reset();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Replaces code when selected and removes previously selected.
|
||||
this.country_code_picker.on_change = (country) => {
|
||||
if (!country) {
|
||||
return this.reset_inputx();
|
||||
}
|
||||
const country_code = frappe.boot.country_codes[country].code;
|
||||
const country_isd = frappe.boot.country_codes[country].isd;
|
||||
this.set_flag(country_code);
|
||||
this.$icon = this.selected_icon.find('svg');
|
||||
this.$flag = this.selected_icon.find('img');
|
||||
|
||||
if (!this.$icon.hasClass('hide')) {
|
||||
this.$icon.toggleClass('hide');
|
||||
}
|
||||
if (!this.$flag.length) {
|
||||
this.selected_icon.prepend(this.get_country_flag(country));
|
||||
}
|
||||
if (!this.$isd.length) {
|
||||
this.selected_icon.append($(`<span class= "country"> ${country_isd}</span>`));
|
||||
} else {
|
||||
this.$isd.text(country_isd);
|
||||
}
|
||||
if (this.$input.val()) {
|
||||
this.set_value(this.get_country(country) +'-'+ this.$input.val());
|
||||
}
|
||||
this.update_padding();
|
||||
// hide popover and focus input
|
||||
this.$wrapper.popover('hide');
|
||||
this.$input.focus();
|
||||
};
|
||||
|
||||
this.$wrapper.find('.selected-phone').on('click', (e) => {
|
||||
this.$wrapper.popover('toggle');
|
||||
e.stopPropagation();
|
||||
|
||||
$('body').on('click.phone-popover', (ev) => {
|
||||
if (!$(ev.target).parents().is('.popover')) {
|
||||
this.$wrapper.popover('hide');
|
||||
}
|
||||
});
|
||||
$(window).on('hashchange.phone-popover', () => {
|
||||
this.$wrapper.popover('hide');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setup_country_code_picker() {
|
||||
let picker_wrapper = $('<div>');
|
||||
this.country_code_picker = new PhonePicker({
|
||||
parent: picker_wrapper,
|
||||
countries: frappe.boot.country_codes
|
||||
});
|
||||
|
||||
this.$wrapper.popover({
|
||||
trigger: 'manual',
|
||||
offset: `${-this.$wrapper.width() / 4.5}, 5`,
|
||||
boundary: 'viewport',
|
||||
placement: 'bottom',
|
||||
template: `
|
||||
<div class="popover phone-picker-popover">
|
||||
<div class="picker-arrow arrow"></div>
|
||||
<div class="popover-body popover-content"></div>
|
||||
</div>
|
||||
`,
|
||||
content: () => picker_wrapper,
|
||||
html: true
|
||||
}).on('show.bs.popover', () => {
|
||||
setTimeout(() => {
|
||||
this.country_code_picker.refresh();
|
||||
this.country_code_picker.search_input.focus();
|
||||
}, 10);
|
||||
}).on('hidden.bs.popover', () => {
|
||||
$('body').off('click.phone-popover');
|
||||
$(window).off('hashchange.phone-popover');
|
||||
});
|
||||
|
||||
// Default icon when nothing is selected.
|
||||
this.selected_icon = this.$wrapper.find('.selected-phone');
|
||||
let input_value = this.get_input_value();
|
||||
if (!this.selected_icon.length) {
|
||||
this.selected_icon = $(`<div class="selected-phone">${frappe.utils.icon("down", "sm")}</div>`);
|
||||
this.selected_icon.insertAfter(this.$input);
|
||||
this.selected_icon.append($(`<span class= "country"></span>`));
|
||||
this.$isd = this.selected_icon.find('.country');
|
||||
if (input_value && input_value.split("-").length == 2) {
|
||||
this.$isd.text(this.value.split("-")[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
super.refresh();
|
||||
// Previously opened doc values showing up on a new doc
|
||||
|
||||
if (this.frm && this.frm.doc.__islocal && !this.get_value()) {
|
||||
this.reset_input();
|
||||
}
|
||||
}
|
||||
|
||||
reset_input() {
|
||||
this.$input.val("");
|
||||
this.$wrapper.find('.country').text("");
|
||||
if (this.selected_icon.find('svg').hasClass('hide')) {
|
||||
this.selected_icon.find('svg').toggleClass('hide');
|
||||
this.selected_icon.find('img').addClass('hide');
|
||||
}
|
||||
this.$input.css("padding-left", 30);
|
||||
}
|
||||
|
||||
set_formatted_input(value) {
|
||||
if (value && value.includes('-') && value.split('-').length == 2) {
|
||||
let isd = this.value.split("-")[0];
|
||||
this.get_country_code_and_change_flag(isd);
|
||||
this.country_code_picker.set_country(isd);
|
||||
this.country_code_picker.refresh();
|
||||
if (this.country_code_picker.country && this.country_code_picker.country !== this.$isd.text()) {
|
||||
this.$isd.length && this.$isd.text(isd);
|
||||
}
|
||||
this.update_padding();
|
||||
this.$input.val(value.split('-').pop());
|
||||
|
||||
} else if (this.$isd.text().trim() && this.value) {
|
||||
let code_number = this.$isd.text() + '-' + value;
|
||||
this.set_value(code_number);
|
||||
}
|
||||
}
|
||||
|
||||
get_value() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
set_flag(country_code) {
|
||||
this.selected_icon.find('img').attr('src', `https://flagcdn.com/${country_code}.svg`);
|
||||
this.$icon = this.selected_icon.find('img');
|
||||
this.$icon.hasClass('hide') && this.$icon.toggleClass('hide');
|
||||
}
|
||||
|
||||
// country_code for India is 'in'
|
||||
get_country_code_and_change_flag(isd) {
|
||||
let country_data = frappe.boot.country_codes;
|
||||
let flag = this.selected_icon.find('img');
|
||||
for (const country in country_data) {
|
||||
if (Object.values(country_data[country]).includes(isd)) {
|
||||
let code = country_data[country].code;
|
||||
flag = this.selected_icon.find('img');
|
||||
if (!flag.length) {
|
||||
this.selected_icon.prepend(this.get_country_flag(country));
|
||||
this.selected_icon.find('svg').addClass('hide');
|
||||
} else {
|
||||
this.set_flag(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get_country(country) {
|
||||
const country_codes = frappe.boot.country_codes;
|
||||
return country_codes[country].isd;
|
||||
}
|
||||
|
||||
get_country_flag(country) {
|
||||
const country_codes = frappe.boot.country_codes;
|
||||
let code = country_codes[country].code;
|
||||
return frappe.utils.flag(code);
|
||||
}
|
||||
|
||||
update_padding() {
|
||||
let len = this.$isd.text().length;
|
||||
let diff = len - 2;
|
||||
if (len > 2) {
|
||||
this.$input.css("padding-left", 60 + (diff * 7));
|
||||
} else {
|
||||
this.$input.css("padding-left", 60);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -375,7 +375,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
}
|
||||
|
||||
set_open_count() {
|
||||
if (!this.data.transactions || !this.data.fieldname) {
|
||||
if (!this.data || (!this.data.transactions || !this.data.fieldname)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
grid_shortcut_keys.forEach(row => {
|
||||
frappe.ui.keys.add_shortcut({
|
||||
shortcut: row.shortcut,
|
||||
page: this,
|
||||
page: this.page,
|
||||
description: __(row.description),
|
||||
ignore_inputs: true,
|
||||
condition: () => !this.is_new()
|
||||
|
|
@ -248,7 +248,7 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
// on main doc
|
||||
frappe.model.on(me.doctype, "*", function(fieldname, value, doc, skip_dirty_trigger=false) {
|
||||
// set input
|
||||
if (cstr(doc.name) === me.docname) {
|
||||
if (doc.name == me.docname) {
|
||||
if (!skip_dirty_trigger) {
|
||||
me.dirty();
|
||||
}
|
||||
|
|
@ -273,7 +273,7 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
// using $.each to preserve df via closure
|
||||
$.each(table_fields, function(i, df) {
|
||||
frappe.model.on(df.options, "*", function(fieldname, value, doc) {
|
||||
if(doc.parent===me.docname && doc.parentfield===df.fieldname) {
|
||||
if (doc.parent == me.docname && doc.parentfield === df.fieldname) {
|
||||
me.dirty();
|
||||
me.fields_dict[df.fieldname].grid.set_value(fieldname, value, doc);
|
||||
return me.script_manager.trigger(fieldname, doc.doctype, doc.name);
|
||||
|
|
@ -356,7 +356,7 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
|
||||
// check permissions
|
||||
if (!this.has_read_permission()) {
|
||||
frappe.show_not_permitted(__(this.doctype) + " " + __(this.docname));
|
||||
frappe.show_not_permitted(__(this.doctype) + " " + __(cstr(this.docname)));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1765,12 +1765,15 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
// scroll to input
|
||||
frappe.utils.scroll_to($el, true, 15);
|
||||
|
||||
// highlight input
|
||||
$el.addClass('has-error');
|
||||
// focus if text field
|
||||
$el.find('input, select, textarea').focus();
|
||||
|
||||
// highlight control inside field
|
||||
let control_element = $el.find('.form-control')
|
||||
control_element.addClass('highlight');
|
||||
setTimeout(() => {
|
||||
$el.removeClass('has-error');
|
||||
$el.find('input, select, textarea').focus();
|
||||
}, 1000);
|
||||
control_element.removeClass('highlight');
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
setup_docinfo_change_listener() {
|
||||
|
|
|
|||
|
|
@ -289,19 +289,23 @@ export default class GridRow {
|
|||
var me = this;
|
||||
if(this.doc && !this.grid.df.in_place_edit) {
|
||||
// remove row
|
||||
if(!this.open_form_button) {
|
||||
this.open_form_button = $(`
|
||||
<div class="btn-open-row">
|
||||
<a>${frappe.utils.icon('edit', 'xs')}</a>
|
||||
<div class="hidden-xs edit-grid-row">${ __("Edit") }</div>
|
||||
</div>
|
||||
`)
|
||||
.appendTo($('<div class="col col-xs-1"></div>').appendTo(this.row))
|
||||
.on('click', function() {
|
||||
me.toggle_view(); return false;
|
||||
});
|
||||
if (!this.open_form_button) {
|
||||
this.open_form_button = $('<div class="col col-xs-1"></div>').appendTo(this.row);
|
||||
|
||||
if(this.is_too_small()) {
|
||||
if (!this.configure_columns) {
|
||||
this.open_form_button = $(`
|
||||
<div class="btn-open-row">
|
||||
<a>${frappe.utils.icon('edit', 'xs')}</a>
|
||||
<div class="hidden-xs edit-grid-row">${ __("Edit") }</div>
|
||||
</div>
|
||||
`)
|
||||
.appendTo(this.open_form_button)
|
||||
.on('click', function() {
|
||||
me.toggle_view(); return false;
|
||||
});
|
||||
}
|
||||
|
||||
if (this.is_too_small()) {
|
||||
// narrow
|
||||
this.open_form_button.css({'margin-right': '-2px'});
|
||||
}
|
||||
|
|
@ -310,7 +314,9 @@ export default class GridRow {
|
|||
}
|
||||
|
||||
add_column_configure_button() {
|
||||
if (this.configure_columns) {
|
||||
if (this.grid.df.in_place_edit && !this.frm) return;
|
||||
|
||||
if (this.configure_columns && this.frm) {
|
||||
this.configure_columns_button = $(`
|
||||
<div class="col grid-static-col col-xs-1 d-flex justify-content-center" style="cursor: pointer;">
|
||||
<a>${frappe.utils.icon('setting-gear', 'sm', '', 'filter: opacity(0.5)')}</a>
|
||||
|
|
@ -320,6 +326,10 @@ export default class GridRow {
|
|||
.on('click', () => {
|
||||
this.configure_dialog_for_columns_selector();
|
||||
});
|
||||
} else if (this.configure_columns && !this.frm) {
|
||||
this.configure_columns_button = $(`
|
||||
<div class="col grid-static-col col-xs-1"></div>
|
||||
`).appendTo(this.row);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ frappe.ui.form.Layout = class Layout {
|
|||
// remove previous color
|
||||
this.message.removeClass(this.message_color);
|
||||
}
|
||||
this.message_color = (color && ['yellow', 'blue', 'red'].includes(color)) ? color : 'blue';
|
||||
this.message_color = (color && ['yellow', 'blue', 'red', 'green', 'orange'].includes(color)) ? color : 'blue';
|
||||
if (html) {
|
||||
if (html.substr(0, 1)!=='<') {
|
||||
// wrap in a block
|
||||
|
|
@ -439,7 +439,7 @@ frappe.ui.form.Layout = class Layout {
|
|||
}
|
||||
|
||||
handle_tab(doctype, fieldname, shift) {
|
||||
let grid_row = null,
|
||||
let grid_row = null,
|
||||
prev = null,
|
||||
fields = this.fields_list,
|
||||
focused = false;
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
}
|
||||
|
||||
is_child_selection_enabled() {
|
||||
return this.dialog.fields_dict['allow_child_item_selection'].get_value();
|
||||
return this.dialog.fields_dict['allow_child_item_selection']?.get_value();
|
||||
}
|
||||
|
||||
toggle_child_selection() {
|
||||
|
|
|
|||
|
|
@ -84,16 +84,15 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
message: __("Unchanged")
|
||||
});
|
||||
}
|
||||
rename_document_title(new_name, new_title, merge=false) {
|
||||
rename_document_title(input_name, input_title, merge=false) {
|
||||
let confirm_message = null;
|
||||
const docname = this.frm.doc.name;
|
||||
const title_field = this.frm.meta.title_field || '';
|
||||
const doctype = this.frm.doctype;
|
||||
|
||||
let confirm_message=null;
|
||||
|
||||
if (new_name) {
|
||||
if (input_name) {
|
||||
const warning = __("This cannot be undone");
|
||||
const message = __("Are you sure you want to merge {0} with {1}?", [docname.bold(), new_name.bold()]);
|
||||
const message = __("Are you sure you want to merge {0} with {1}?", [docname.bold(), input_name.bold()]);
|
||||
confirm_message = `${message}<br><b>${warning}<b>`;
|
||||
}
|
||||
|
||||
|
|
@ -101,22 +100,45 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
return frappe.xcall("frappe.model.rename_doc.update_document_title", {
|
||||
doctype,
|
||||
docname,
|
||||
name: new_name,
|
||||
title: new_title,
|
||||
name: input_name,
|
||||
title: input_title,
|
||||
enqueue: true,
|
||||
merge,
|
||||
freeze: true,
|
||||
freeze_message: __("Updating related fields...")
|
||||
}).then(new_docname => {
|
||||
if (new_name != docname) {
|
||||
$(document).trigger("rename", [doctype, docname, new_docname || new_name]);
|
||||
const reload_form = (input_name) => {
|
||||
$(document).trigger("rename", [doctype, docname, input_name]);
|
||||
if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname];
|
||||
this.frm.reload_doc();
|
||||
}
|
||||
|
||||
// handle document renaming queued action
|
||||
if (input_name && (new_docname == docname)) {
|
||||
frappe.socketio.doc_subscribe(doctype, input_name);
|
||||
frappe.realtime.on("doc_update", data => {
|
||||
if (data.doctype == doctype && data.name == input_name) {
|
||||
reload_form(input_name);
|
||||
frappe.show_alert({
|
||||
message: __('Document renamed from {0} to {1}', [docname.bold(), input_name.bold()]),
|
||||
indicator: 'success',
|
||||
});
|
||||
}
|
||||
});
|
||||
frappe.show_alert(
|
||||
__('Document renaming from {0} to {1} has been queued', [docname.bold(), input_name.bold()])
|
||||
);
|
||||
}
|
||||
|
||||
// handle document sync rename action
|
||||
if (input_name && ((new_docname || input_name) != docname)) {
|
||||
reload_form(new_docname || input_name);
|
||||
}
|
||||
this.frm.reload_doc();
|
||||
});
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (new_title === this.frm.doc[title_field] && new_name === docname) {
|
||||
if (input_title === this.frm.doc[title_field] && input_name === docname) {
|
||||
this.show_unchanged_document_alert();
|
||||
resolve();
|
||||
} else if (merge) {
|
||||
|
|
@ -323,7 +345,7 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
}
|
||||
|
||||
// New
|
||||
if(p[CREATE] && !this.frm.meta.issingle) {
|
||||
if (p[CREATE] && !this.frm.meta.issingle && !this.frm.meta.in_create) {
|
||||
this.page.add_menu_item(__("New {0}", [__(me.frm.doctype)]), function() {
|
||||
frappe.new_doc(me.frm.doctype, true);
|
||||
}, true, {
|
||||
|
|
@ -569,7 +591,8 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
primary_action: ({ fieldname }) => {
|
||||
dialog.hide();
|
||||
this.frm.scroll_to_field(fieldname);
|
||||
}
|
||||
},
|
||||
animate: false,
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
|
|
|
|||
|
|
@ -403,7 +403,7 @@ $.extend(frappe.model, {
|
|||
}
|
||||
});
|
||||
} else {
|
||||
if(typeof filters==="string" && locals[doctype] && locals[doctype][filters]) {
|
||||
if (["number", "string"].includes(typeof filters) && locals[doctype] && locals[doctype][filters]) {
|
||||
return locals[doctype][filters][fieldname];
|
||||
} else {
|
||||
var l = frappe.get_list(doctype, filters);
|
||||
|
|
|
|||
103
frappe/public/js/frappe/phone_picker/phone_picker.js
Normal file
103
frappe/public/js/frappe/phone_picker/phone_picker.js
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
class PhonePicker {
|
||||
constructor(opts) {
|
||||
this.parent = opts.parent;
|
||||
this.width = opts.width;
|
||||
this.height = opts.height;
|
||||
this.country = opts.country;
|
||||
opts.country && this.set_country(opts.country);
|
||||
this.countries = opts.countries;
|
||||
this.setup_picker();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.update_icon_selected(true);
|
||||
}
|
||||
|
||||
setup_picker() {
|
||||
this.phone_picker_wrapper = $(`
|
||||
<div class="phone-picker">
|
||||
<div class="search-phones">
|
||||
<input type="search" placeholder="${__('Search for countries...')}" class="form-control">
|
||||
<span class="search-phone">${frappe.utils.icon('search', "sm")}</span>
|
||||
</div>
|
||||
<div class="phone-section">
|
||||
<div class="phones"></div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
this.parent.append(this.phone_picker_wrapper);
|
||||
this.phone_wrapper = this.phone_picker_wrapper.find('.phones');
|
||||
this.search_input = this.phone_picker_wrapper.find('.search-phones > input');
|
||||
this.refresh();
|
||||
this.setup_countries();
|
||||
}
|
||||
|
||||
setup_countries() {
|
||||
Object.entries(this.countries).forEach(([country, info]) => {
|
||||
if (!info.isd) {
|
||||
return;
|
||||
}
|
||||
let $country = $(`
|
||||
<div id="${country.toLowerCase()}" class="phone-wrapper">
|
||||
${frappe.utils.flag(info.code)}
|
||||
<span class="country">${country} (${info.isd})</span>
|
||||
</div>
|
||||
`);
|
||||
this.phone_wrapper.append($country);
|
||||
const set_values = () => {
|
||||
this.set_country(country);
|
||||
this.update_icon_selected();
|
||||
};
|
||||
$country.on('click', () => {
|
||||
set_values();
|
||||
});
|
||||
$country.hover(() => {
|
||||
$country.toggleClass("bg-gray-100");
|
||||
});
|
||||
this.search_input.keydown((e) => {
|
||||
const key_code = e.keyCode;
|
||||
if ([13].includes(key_code)) {
|
||||
e.preventDefault();
|
||||
set_values();
|
||||
}
|
||||
});
|
||||
this.search_input.keyup((e) => {
|
||||
e.preventDefault();
|
||||
this.filter_icons();
|
||||
});
|
||||
|
||||
this.search_input.on('search', () => {
|
||||
this.filter_icons();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
filter_icons() {
|
||||
let value = this.search_input.val();
|
||||
if (!value) {
|
||||
this.phone_wrapper.find(".phone-wrapper").removeClass('hidden');
|
||||
} else {
|
||||
this.phone_wrapper.find(".phone-wrapper").addClass('hidden');
|
||||
this.phone_wrapper.find(`.phone-wrapper[id*='${value.toLowerCase()}']`).removeClass('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
update_icon_selected(silent) {
|
||||
!silent && this.on_change && this.on_change(this.get_country());
|
||||
}
|
||||
|
||||
set_country(country) {
|
||||
this.country = country || '';
|
||||
}
|
||||
|
||||
get_country() {
|
||||
return this.country;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.set_country();
|
||||
this.update_icon_selected();
|
||||
}
|
||||
}
|
||||
|
||||
export default PhonePicker;
|
||||
|
|
@ -22,15 +22,17 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
|
|||
super.make();
|
||||
this.refresh();
|
||||
// set default
|
||||
$.each(this.fields_list, (_, field) => {
|
||||
if (!is_null(field.df.default)) {
|
||||
let def_value = field.df.default;
|
||||
$.each(this.fields_list, function(i, field) {
|
||||
if (field.df["default"]) {
|
||||
let def_value = field.df["default"];
|
||||
|
||||
if (def_value === "Today" && field.df.fieldtype === "Date") {
|
||||
if (def_value == 'Today' && field.df["fieldtype"] == 'Date') {
|
||||
def_value = frappe.datetime.get_today();
|
||||
}
|
||||
|
||||
this.set_value(field.df.fieldname, def_value);
|
||||
field.set_input(def_value);
|
||||
// if default and has depends_on, render its fields.
|
||||
me.refresh_dependency();
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -127,7 +129,6 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
|
|||
if (f) {
|
||||
f.set_value(val).then(() => {
|
||||
f.set_input(val);
|
||||
f.refresh();
|
||||
this.refresh_dependency();
|
||||
resolve();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ frappe.ui.keys.add_shortcut = ({shortcut, action, description, page, target, con
|
|||
if (is_input_focused && !ignore_inputs) return;
|
||||
if (!condition()) return;
|
||||
|
||||
if (!page || page.wrapper.is(':visible')) {
|
||||
if (action && (!page || page.wrapper.is(':visible'))) {
|
||||
let prevent_default = action(e);
|
||||
// prevent default if true is explicitly returned
|
||||
// or nothing returned (undefined)
|
||||
|
|
@ -221,11 +221,11 @@ frappe.ui.keys.add_shortcut({
|
|||
});
|
||||
|
||||
frappe.ui.keys.on('escape', function(e) {
|
||||
close_grid_and_dialog();
|
||||
handle_escape_key();
|
||||
});
|
||||
|
||||
frappe.ui.keys.on('esc', function(e) {
|
||||
close_grid_and_dialog();
|
||||
handle_escape_key();
|
||||
});
|
||||
|
||||
frappe.ui.keys.on('enter', function(e) {
|
||||
|
|
@ -293,6 +293,11 @@ frappe.ui.keyCode = {
|
|||
BACKSPACE: 8
|
||||
}
|
||||
|
||||
function handle_escape_key() {
|
||||
close_grid_and_dialog();
|
||||
document.activeElement?.blur();
|
||||
}
|
||||
|
||||
function close_grid_and_dialog() {
|
||||
// close open grid row
|
||||
var open_row = $(".grid-row-open");
|
||||
|
|
@ -308,10 +313,3 @@ function close_grid_and_dialog() {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// blur when escape is pressed on dropdowns
|
||||
$(document).on('keydown', '.dropdown-toggle', (e) => {
|
||||
if (e.which === frappe.ui.keyCode.ESCAPE) {
|
||||
$(e.currentTarget).blur();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ frappe.ui.SortSelector = class SortSelector {
|
|||
// bold, mandatory and fields that are available in list view
|
||||
meta.fields.forEach(function(df) {
|
||||
if (
|
||||
(df.mandatory || df.bold || df.in_list_view)
|
||||
(df.mandatory || df.bold || df.in_list_view || df.reqd)
|
||||
&& frappe.model.is_value_type(df.fieldtype)
|
||||
&& frappe.perm.has_perm(me.doctype, df.permlevel, "read")
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -249,7 +249,7 @@ frappe.dashboard_utils = {
|
|||
{args: values}
|
||||
).then(()=> {
|
||||
let dashboard_route_html =
|
||||
`<a href = "#dashboard/${values.dashboard}">${values.dashboard}</a>`;
|
||||
`<a href = "/app/dashboard/${values.dashboard}">${values.dashboard}</a>`;
|
||||
let message =
|
||||
__("{0} {1} added to Dashboard {2}", [doctype, values.name, dashboard_route_html]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1192,6 +1192,12 @@ Object.assign(frappe.utils, {
|
|||
</svg>`;
|
||||
},
|
||||
|
||||
flag(country_code) {
|
||||
return `<img
|
||||
src="https://flagcdn.com/${country_code}.svg"
|
||||
width="20" height="15">`;
|
||||
},
|
||||
|
||||
make_chart(wrapper, custom_options={}) {
|
||||
let chart_args = {
|
||||
type: 'bar',
|
||||
|
|
|
|||
|
|
@ -390,7 +390,7 @@ frappe.views.FileView = class FileView extends frappe.views.ListView {
|
|||
return `
|
||||
<div class="list-row-col ellipsis list-subject level">
|
||||
<span class="level-item file-select">
|
||||
<input class="list-row-checkbox hidden-xs"
|
||||
<input class="list-row-checkbox"
|
||||
type="checkbox" data-name="${file.name}">
|
||||
</span>
|
||||
<span class="level-item ellipsis" title="${file.file_name}">
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export default class Block {
|
|||
|
||||
make(block, block_name, widget_type = block) {
|
||||
let block_data = this.config.page_data[block+'s'].items.find(obj => {
|
||||
return frappe.utils.unescape_html(obj.label) == frappe.utils.unescape_html(block_name);
|
||||
return frappe.utils.unescape_html(obj.label) == frappe.utils.unescape_html(__(block_name));
|
||||
});
|
||||
if (!block_data) return false;
|
||||
this.wrapper.innerHTML = '';
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export default class Card extends Block {
|
|||
this.new('card', 'links');
|
||||
|
||||
if (this.data && this.data.card_name) {
|
||||
let has_data = this.make('card', __(this.data.card_name), 'links');
|
||||
let has_data = this.make('card', this.data.card_name, 'links');
|
||||
if (!has_data) return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export default class Chart extends Block {
|
|||
this.new('chart');
|
||||
|
||||
if (this.data && this.data.chart_name) {
|
||||
let has_data = this.make('chart', __(this.data.chart_name));
|
||||
let has_data = this.make('chart', this.data.chart_name);
|
||||
if (!has_data) return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ export default class Onboarding extends Block {
|
|||
|
||||
make(block, block_name) {
|
||||
let block_data = this.config.page_data['onboardings'].items.find(obj => {
|
||||
return obj.label == block_name;
|
||||
return obj.label == __(block_name);
|
||||
});
|
||||
if (!block_data) return false;
|
||||
this.wrapper.innerHTML = '';
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export default class Shortcut extends Block {
|
|||
this.new('shortcut');
|
||||
|
||||
if (this.data && this.data.shortcut_name) {
|
||||
let has_data = this.make('shortcut', __(this.data.shortcut_name));
|
||||
let has_data = this.make('shortcut', this.data.shortcut_name);
|
||||
if (!has_data) return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -228,30 +228,35 @@ class CardDialog extends WidgetDialog {
|
|||
}
|
||||
|
||||
process_data(data) {
|
||||
data.links.map((item, idx) => {
|
||||
let message = '';
|
||||
let row = idx+1;
|
||||
let message = '';
|
||||
|
||||
if (!item.link_type) {
|
||||
message = "Following fields have missing values: <br><br><ul>";
|
||||
message += `<li>Link Type in Row ${row}</li>`;
|
||||
}
|
||||
if (!data.links) {
|
||||
message = "You must add atleast one link.";
|
||||
} else {
|
||||
data.links.map((item, idx) => {
|
||||
let row = idx+1;
|
||||
|
||||
if (!item.link_to) {
|
||||
message += `<li>Link To in Row ${row}</li>`;
|
||||
}
|
||||
if (!item.link_type) {
|
||||
message = "Following fields have missing values: <br><br><ul>";
|
||||
message += `<li>Link Type in Row ${row}</li>`;
|
||||
}
|
||||
|
||||
if (message) {
|
||||
message += "</ul>";
|
||||
frappe.throw({
|
||||
message: __(message),
|
||||
title: __("Missing Values Required"),
|
||||
indicator: 'orange'
|
||||
});
|
||||
}
|
||||
if (!item.link_to) {
|
||||
message += `<li>Link To in Row ${row}</li>`;
|
||||
}
|
||||
|
||||
item.label = item.label ? item.label : item.link_to;
|
||||
});
|
||||
item.label = item.label ? item.label : item.link_to;
|
||||
});
|
||||
}
|
||||
|
||||
if (message) {
|
||||
message += "</ul>";
|
||||
frappe.throw({
|
||||
message: __(message),
|
||||
title: __("Missing Values Required"),
|
||||
indicator: 'orange'
|
||||
});
|
||||
}
|
||||
|
||||
data.label = data.label ? data.label : data.chart_name;
|
||||
return data;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
@import "color_picker";
|
||||
@import "icon_picker";
|
||||
@import "datepicker";
|
||||
@import "phone_picker";
|
||||
|
||||
// password
|
||||
.form-control[data-fieldtype="Password"] {
|
||||
|
|
@ -343,11 +344,10 @@ textarea.form-control {
|
|||
.duration-picker {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow-sm);
|
||||
background: var(--popover-bg);
|
||||
|
||||
width: max-content;
|
||||
&:after,
|
||||
&:before {
|
||||
border: solid transparent;
|
||||
|
|
@ -466,4 +466,4 @@ button.data-pill {
|
|||
top: 0;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
144
frappe/public/scss/common/phone_picker.scss
Normal file
144
frappe/public/scss/common/phone_picker.scss
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
.phone-picker {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
--phone-picker-width: 290px;
|
||||
width: var(--phone-picker-width);
|
||||
.phones {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow-y: scroll;
|
||||
max-height: 210px;
|
||||
cursor: pointer;
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.phone-wrapper {
|
||||
display: flex;
|
||||
width: 290px;
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.5rem;
|
||||
|
||||
img {
|
||||
height: 15px;
|
||||
}
|
||||
.country {
|
||||
display: flex;
|
||||
margin-left: 0.6rem;
|
||||
flex-grow: 1;
|
||||
width: 290px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-phones {
|
||||
position: relative;
|
||||
|
||||
input[type='search'] {
|
||||
height: inherit;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.search-phone {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.phone-picker-popover {
|
||||
max-width: 325px;
|
||||
left: 29px !important;
|
||||
.picker-arrow {
|
||||
left: 15px !important;
|
||||
}
|
||||
@media (max-width: 992px) {
|
||||
max-width: 325px;
|
||||
left: 48px !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.frappe-control[data-fieldtype='Phone']
|
||||
{
|
||||
input {
|
||||
padding-left: 30px;
|
||||
}
|
||||
.selected-phone {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
height: 20px;
|
||||
border-radius: 5px;
|
||||
position: absolute;
|
||||
top: calc(50% + 2px);
|
||||
left: 8px;
|
||||
content: ' ';
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
|
||||
.country {
|
||||
display: flex;
|
||||
margin-left: 0.6rem;
|
||||
align-items: flex-end;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
}
|
||||
.like-disabled-input {
|
||||
.phone-value {
|
||||
padding-left: 25px;
|
||||
}
|
||||
.selected-phone {
|
||||
top: 20%;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
.frappe-control[data-fieldtype='Phone']
|
||||
{
|
||||
.selected-phone {
|
||||
top: calc(50% - 0.5px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.data-row.row {
|
||||
.selected-phone {
|
||||
top: calc(50% - 10.1px);
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-gray-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgba(244,245,246,var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dt-cell__content {
|
||||
.selected-phone {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
.dt-cell__edit, .filter-field {
|
||||
.selected-phone {
|
||||
top: 5.5px !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
font-family: inherit;
|
||||
}
|
||||
|
||||
/*rtl:begin:ignore*/
|
||||
.ql-editor {
|
||||
font-family: var(--font-stack);
|
||||
color: var(--text-color);
|
||||
|
|
@ -22,7 +23,15 @@
|
|||
a[href] {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.ql-direction-rtl {
|
||||
direction: rtl;
|
||||
+ .table {
|
||||
direction: ltr;
|
||||
}
|
||||
}
|
||||
}
|
||||
/*rtl:end:ignore*/
|
||||
|
||||
|
||||
.ql-toolbar.ql-snow {
|
||||
border-top-left-radius: var(--border-radius);
|
||||
|
|
@ -70,6 +79,7 @@
|
|||
min-height: 0;
|
||||
max-height: none;
|
||||
overflow: hidden;
|
||||
resize: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,10 @@ def DocType(*args, **kwargs):
|
|||
return frappe.qb.DocType(*args, **kwargs)
|
||||
|
||||
|
||||
def Table(*args, **kwargs):
|
||||
return frappe.qb.Table(*args, **kwargs)
|
||||
|
||||
|
||||
def patch_query_execute():
|
||||
"""Patch the Query Builder with helper execute method
|
||||
This excludes the use of `frappe.db.sql` method while
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@ def run_tests_for_doctype(
|
|||
if force:
|
||||
for name in frappe.db.sql_list("select name from `tab%s`" % doctype):
|
||||
frappe.delete_doc(doctype, name, force=True)
|
||||
make_test_records(doctype, verbose=verbose, force=force)
|
||||
make_test_records(doctype, verbose=verbose, force=force, commit=True)
|
||||
modules.append(importlib.import_module(test_module))
|
||||
|
||||
return _run_unittest(
|
||||
|
|
@ -245,7 +245,7 @@ def run_tests_for_module(
|
|||
module = importlib.import_module(module)
|
||||
if hasattr(module, "test_dependencies"):
|
||||
for doctype in module.test_dependencies:
|
||||
make_test_records(doctype, verbose=verbose)
|
||||
make_test_records(doctype, verbose=verbose, commit=True)
|
||||
|
||||
frappe.db.commit()
|
||||
return _run_unittest(
|
||||
|
|
@ -330,7 +330,7 @@ def _add_test(app, path, filename, verbose, test_suite=None, ui_tests=False):
|
|||
|
||||
if hasattr(module, "test_dependencies"):
|
||||
for doctype in module.test_dependencies:
|
||||
make_test_records(doctype, verbose=verbose)
|
||||
make_test_records(doctype, verbose=verbose, commit=True)
|
||||
|
||||
is_ui_test = True if hasattr(module, "TestDriver") else False
|
||||
|
||||
|
|
@ -346,12 +346,12 @@ def _add_test(app, path, filename, verbose, test_suite=None, ui_tests=False):
|
|||
with open(txt_file, "r") as f:
|
||||
doc = json.loads(f.read())
|
||||
doctype = doc["name"]
|
||||
make_test_records(doctype, verbose)
|
||||
make_test_records(doctype, verbose, commit=True)
|
||||
|
||||
test_suite.addTest(unittest.TestLoader().loadTestsFromModule(module))
|
||||
|
||||
|
||||
def make_test_records(doctype, verbose=0, force=False):
|
||||
def make_test_records(doctype, verbose=0, force=False, commit=False):
|
||||
if not frappe.db:
|
||||
frappe.connect()
|
||||
|
||||
|
|
@ -364,8 +364,8 @@ def make_test_records(doctype, verbose=0, force=False):
|
|||
|
||||
if options not in frappe.local.test_objects:
|
||||
frappe.local.test_objects[options] = []
|
||||
make_test_records(options, verbose, force)
|
||||
make_test_records_for_doctype(options, verbose, force)
|
||||
make_test_records(options, verbose, force, commit=commit)
|
||||
make_test_records_for_doctype(options, verbose, force, commit=commit)
|
||||
|
||||
|
||||
def get_modules(doctype):
|
||||
|
|
@ -405,7 +405,7 @@ def get_dependencies(doctype):
|
|||
return options_list
|
||||
|
||||
|
||||
def make_test_records_for_doctype(doctype, verbose=0, force=False):
|
||||
def make_test_records_for_doctype(doctype, verbose=0, force=False, commit=False):
|
||||
if not force and doctype in get_test_record_log():
|
||||
return
|
||||
|
||||
|
|
@ -420,17 +420,19 @@ def make_test_records_for_doctype(doctype, verbose=0, force=False):
|
|||
elif hasattr(test_module, "test_records"):
|
||||
if doctype in frappe.local.test_objects:
|
||||
frappe.local.test_objects[doctype] += make_test_objects(
|
||||
doctype, test_module.test_records, verbose, force
|
||||
doctype, test_module.test_records, verbose, force, commit=commit
|
||||
)
|
||||
else:
|
||||
frappe.local.test_objects[doctype] = make_test_objects(
|
||||
doctype, test_module.test_records, verbose, force
|
||||
doctype, test_module.test_records, verbose, force, commit=commit
|
||||
)
|
||||
|
||||
else:
|
||||
test_records = frappe.get_test_records(doctype)
|
||||
if test_records:
|
||||
frappe.local.test_objects[doctype] += make_test_objects(doctype, test_records, verbose, force)
|
||||
frappe.local.test_objects[doctype] += make_test_objects(
|
||||
doctype, test_records, verbose, force, commit=commit
|
||||
)
|
||||
|
||||
elif verbose:
|
||||
print_mandatory_fields(doctype)
|
||||
|
|
@ -438,7 +440,7 @@ def make_test_records_for_doctype(doctype, verbose=0, force=False):
|
|||
add_to_test_record_log(doctype)
|
||||
|
||||
|
||||
def make_test_objects(doctype, test_records=None, verbose=None, reset=False):
|
||||
def make_test_objects(doctype, test_records=None, verbose=None, reset=False, commit=False):
|
||||
"""Make test objects from given list of `test_records` or from `test_records.json`"""
|
||||
records = []
|
||||
|
||||
|
|
@ -495,7 +497,8 @@ def make_test_objects(doctype, test_records=None, verbose=None, reset=False):
|
|||
|
||||
records.append(d.name)
|
||||
|
||||
frappe.db.commit()
|
||||
if commit:
|
||||
frappe.db.commit()
|
||||
return records
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -141,3 +141,40 @@ class TestClient(unittest.TestCase):
|
|||
self.assertEqual(get("ToDo", filters=filters_json).description, "test")
|
||||
|
||||
todo.delete()
|
||||
|
||||
def test_client_insert(self):
|
||||
from frappe.client import insert
|
||||
|
||||
def get_random_title():
|
||||
return "test-{0}".format(frappe.generate_hash())
|
||||
|
||||
# test insert dict
|
||||
doc = {"doctype": "Note", "title": get_random_title(), "content": "test"}
|
||||
note1 = insert(doc)
|
||||
self.assertTrue(note1)
|
||||
|
||||
# test insert json
|
||||
doc["title"] = get_random_title()
|
||||
json_doc = frappe.as_json(doc)
|
||||
note2 = insert(json_doc)
|
||||
self.assertTrue(note2)
|
||||
|
||||
# test insert child doc without parent fields
|
||||
child_doc = {"doctype": "Note Seen By", "user": "Administrator"}
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
insert(child_doc)
|
||||
|
||||
# test insert child doc with parent fields
|
||||
child_doc = {
|
||||
"doctype": "Note Seen By",
|
||||
"user": "Administrator",
|
||||
"parenttype": "Note",
|
||||
"parent": note1.name,
|
||||
"parentfield": "seen_by",
|
||||
}
|
||||
note3 = insert(child_doc)
|
||||
self.assertTrue(note3)
|
||||
|
||||
# cleanup
|
||||
frappe.delete_doc("Note", note1.name)
|
||||
frappe.delete_doc("Note", note2.name)
|
||||
|
|
|
|||
|
|
@ -87,6 +87,15 @@ class TestDB(unittest.TestCase):
|
|||
frappe.db.get_values("User", filters=[["name", "=", "Administrator"]], fieldname="email"),
|
||||
)
|
||||
|
||||
# test multiple orderby's
|
||||
delimiter = '"' if frappe.db.db_type == "postgres" else "`"
|
||||
self.assertIn(
|
||||
"ORDER BY {deli}creation{deli} DESC,{deli}modified{deli} ASC,{deli}name{deli} DESC".format(
|
||||
deli=delimiter
|
||||
),
|
||||
frappe.db.get_value("DocType", "DocField", order_by="creation desc, modified asc, name", run=0),
|
||||
)
|
||||
|
||||
def test_get_value_limits(self):
|
||||
|
||||
# check both dict and list style filters
|
||||
|
|
|
|||
|
|
@ -61,10 +61,12 @@ class TestReportview(unittest.TestCase):
|
|||
in build_match_conditions(as_condition=False)
|
||||
)
|
||||
# get as conditions
|
||||
self.assertEqual(
|
||||
build_match_conditions(as_condition=True),
|
||||
"""(((ifnull(`tabBlog Post`.`name`, '')='' or `tabBlog Post`.`name` in ('-test-blog-post-1', '-test-blog-post'))))""",
|
||||
)
|
||||
if frappe.db.db_type == "mariadb":
|
||||
assertion_string = """(((ifnull(`tabBlog Post`.`name`, '')='' or `tabBlog Post`.`name` in ('-test-blog-post-1', '-test-blog-post'))))"""
|
||||
else:
|
||||
assertion_string = """(((ifnull(cast(`tabBlog Post`.`name` as varchar), '')='' or cast(`tabBlog Post`.`name` as varchar) in ('-test-blog-post-1', '-test-blog-post'))))"""
|
||||
|
||||
self.assertEqual(build_match_conditions(as_condition=True), assertion_string)
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
|
|
@ -619,19 +621,22 @@ class TestReportview(unittest.TestCase):
|
|||
def test_cast_name(self):
|
||||
from frappe.core.doctype.doctype.test_doctype import new_doctype
|
||||
|
||||
frappe.delete_doc_if_exists("DocType", "autoinc_dt_test")
|
||||
dt = new_doctype("autoinc_dt_test", autoname="autoincrement").insert(ignore_permissions=True)
|
||||
|
||||
query = DatabaseQuery("autoinc_dt_test").execute(
|
||||
fields=["locate('1', `tabautoinc_dt_test`.`name`)", "`tabautoinc_dt_test`.`name`"],
|
||||
fields=["locate('1', `tabautoinc_dt_test`.`name`)", "name", "locate('1', name)"],
|
||||
filters={"name": 1},
|
||||
run=False,
|
||||
)
|
||||
|
||||
if frappe.db.db_type == "postgres":
|
||||
self.assertTrue('strpos( cast( "tabautoinc_dt_test"."name" as varchar), \'1\')' in query)
|
||||
self.assertTrue('strpos( cast("tabautoinc_dt_test"."name" as varchar), \'1\')' in query)
|
||||
self.assertTrue("strpos( cast(name as varchar), '1')" in query)
|
||||
self.assertTrue('where cast("tabautoinc_dt_test"."name" as varchar) = \'1\'' in query)
|
||||
else:
|
||||
self.assertTrue("locate('1', `tabautoinc_dt_test`.`name`)" in query)
|
||||
self.assertTrue("locate('1', name)" in query)
|
||||
self.assertTrue("where `tabautoinc_dt_test`.`name` = 1" in query)
|
||||
|
||||
dt.delete(ignore_permissions=True)
|
||||
|
|
@ -639,23 +644,53 @@ class TestReportview(unittest.TestCase):
|
|||
def test_fieldname_starting_with_int(self):
|
||||
from frappe.core.doctype.doctype.test_doctype import new_doctype
|
||||
|
||||
frappe.delete_doc_if_exists("DocType", "dt_with_int_named_fieldname")
|
||||
frappe.delete_doc_if_exists("DocType", "table_dt")
|
||||
|
||||
table_dt = new_doctype(
|
||||
"table_dt", istable=1, fields=[{"label": "1field", "fieldname": "2field", "fieldtype": "Data"}]
|
||||
).insert()
|
||||
|
||||
dt = new_doctype(
|
||||
"dt_with_int_named_fieldname",
|
||||
fields=[{"label": "1field", "fieldname": "1field", "fieldtype": "Int"}],
|
||||
fields=[
|
||||
{"label": "1field", "fieldname": "1field", "fieldtype": "Data"},
|
||||
{
|
||||
"label": "2table_field",
|
||||
"fieldname": "2table_field",
|
||||
"fieldtype": "Table",
|
||||
"options": table_dt.name,
|
||||
},
|
||||
],
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
frappe.get_doc({"doctype": "dt_with_int_named_fieldname", "1field": 10}).insert(
|
||||
dt_data = frappe.get_doc({"doctype": "dt_with_int_named_fieldname", "1field": "10"}).insert(
|
||||
ignore_permissions=True
|
||||
)
|
||||
|
||||
query = DatabaseQuery("dt_with_int_named_fieldname")
|
||||
self.assertTrue(query.execute(filters={"1field": 10}))
|
||||
self.assertTrue(query.execute(filters={"1field": "10"}))
|
||||
self.assertTrue(query.execute(filters={"1field": ["like", "1%"]}))
|
||||
self.assertTrue(query.execute(filters={"1field": ["in", "1,2,10"]}))
|
||||
self.assertTrue(query.execute(filters={"1field": ["is", "set"]}))
|
||||
self.assertFalse(query.execute(filters={"1field": ["not like", "1%"]}))
|
||||
self.assertTrue(query.execute(filters=[["table_dt", "2field", "is", "not set"]]))
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": table_dt.name,
|
||||
"2field": "10",
|
||||
"parent": dt_data.name,
|
||||
"parenttype": dt_data.doctype,
|
||||
"parentfield": "2table_field",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
self.assertTrue(query.execute(filters=[["table_dt", "2field", "is", "set"]]))
|
||||
|
||||
# cleanup
|
||||
dt.delete()
|
||||
table_dt.delete()
|
||||
|
||||
|
||||
def add_child_table_to_blog_post():
|
||||
|
|
|
|||
|
|
@ -24,25 +24,26 @@ test_dependencies = ["Blogger", "Blog Post", "User", "Contact", "Salutation"]
|
|||
|
||||
|
||||
class TestPermissions(FrappeTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
frappe.clear_cache(doctype="Blog Post")
|
||||
user = frappe.get_doc("User", "test1@example.com")
|
||||
user.add_roles("Website Manager")
|
||||
user.add_roles("System Manager")
|
||||
|
||||
user = frappe.get_doc("User", "test2@example.com")
|
||||
user.add_roles("Blogger")
|
||||
|
||||
user = frappe.get_doc("User", "test3@example.com")
|
||||
user.add_roles("Sales User")
|
||||
|
||||
user = frappe.get_doc("User", "testperm@example.com")
|
||||
user.add_roles("Website Manager")
|
||||
|
||||
def setUp(self):
|
||||
frappe.clear_cache(doctype="Blog Post")
|
||||
|
||||
if not frappe.flags.permission_user_setup_done:
|
||||
user = frappe.get_doc("User", "test1@example.com")
|
||||
user.add_roles("Website Manager")
|
||||
user.add_roles("System Manager")
|
||||
|
||||
user = frappe.get_doc("User", "test2@example.com")
|
||||
user.add_roles("Blogger")
|
||||
|
||||
user = frappe.get_doc("User", "test3@example.com")
|
||||
user.add_roles("Sales User")
|
||||
|
||||
user = frappe.get_doc("User", "testperm@example.com")
|
||||
user.add_roles("Website Manager")
|
||||
|
||||
frappe.flags.permission_user_setup_done = True
|
||||
|
||||
reset("Blogger")
|
||||
reset("Blog Post")
|
||||
|
||||
|
|
|
|||
|
|
@ -107,8 +107,25 @@ class TestRenameDoc(unittest.TestCase):
|
|||
|
||||
def setUp(self):
|
||||
frappe.flags.link_fields = {}
|
||||
if self._testMethodName == "test_doc_rename_method":
|
||||
self.property_setter = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Property Setter",
|
||||
"doctype_or_field": "DocType",
|
||||
"doc_type": self.test_doctype,
|
||||
"property": "allow_rename",
|
||||
"property_type": "Check",
|
||||
"value": "1",
|
||||
}
|
||||
).insert()
|
||||
|
||||
super().setUp()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
if self._testMethodName == "test_doc_rename_method":
|
||||
self.property_setter.delete()
|
||||
return super().tearDown()
|
||||
|
||||
def test_rename_doc(self):
|
||||
"""Rename an existing document via frappe.rename_doc"""
|
||||
old_name = choice(self.available_documents)
|
||||
|
|
@ -247,3 +264,12 @@ class TestRenameDoc(unittest.TestCase):
|
|||
|
||||
update_linked_doctypes("User", "ToDo", "str", "str")
|
||||
self.assertTrue("Function frappe.model.rename_doc.update_linked_doctypes" in stdout.getvalue())
|
||||
|
||||
def test_doc_rename_method(self):
|
||||
name = choice(self.available_documents)
|
||||
new_name = f"{name}-{frappe.generate_hash(length=4)}"
|
||||
doc = frappe.get_doc(self.test_doctype, name)
|
||||
doc.rename(new_name, merge=frappe.db.exists(self.test_doctype, new_name))
|
||||
self.assertEqual(doc.name, new_name)
|
||||
self.available_documents.append(new_name)
|
||||
self.available_documents.remove(name)
|
||||
|
|
|
|||
54
frappe/tests/test_sequence.py
Normal file
54
frappe/tests/test_sequence.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import psycopg2
|
||||
import pymysql
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestSequence(FrappeTestCase):
|
||||
def generate_sequence_name(self) -> str:
|
||||
return self._testMethodName + "_" + frappe.generate_hash(length=5)
|
||||
|
||||
def test_set_next_val(self):
|
||||
seq_name = self.generate_sequence_name()
|
||||
frappe.db.create_sequence(seq_name, check_not_exists=True, temporary=True)
|
||||
|
||||
next_val = frappe.db.get_next_sequence_val(seq_name)
|
||||
frappe.db.set_next_sequence_val(seq_name, next_val + 1)
|
||||
self.assertEqual(next_val + 1, frappe.db.get_next_sequence_val(seq_name))
|
||||
|
||||
next_val = frappe.db.get_next_sequence_val(seq_name)
|
||||
frappe.db.set_next_sequence_val(seq_name, next_val + 1, is_val_used=True)
|
||||
self.assertEqual(next_val + 2, frappe.db.get_next_sequence_val(seq_name))
|
||||
|
||||
def test_create_sequence(self):
|
||||
seq_name = self.generate_sequence_name()
|
||||
frappe.db.create_sequence(seq_name, max_value=2, cycle=True, temporary=True)
|
||||
frappe.db.get_next_sequence_val(seq_name)
|
||||
frappe.db.get_next_sequence_val(seq_name)
|
||||
self.assertEqual(1, frappe.db.get_next_sequence_val(seq_name))
|
||||
|
||||
seq_name = self.generate_sequence_name()
|
||||
frappe.db.create_sequence(seq_name, max_value=2, temporary=True)
|
||||
frappe.db.get_next_sequence_val(seq_name)
|
||||
frappe.db.get_next_sequence_val(seq_name)
|
||||
|
||||
try:
|
||||
frappe.db.get_next_sequence_val(seq_name)
|
||||
except pymysql.err.OperationalError as e:
|
||||
self.assertEqual(e.args[0], 4084)
|
||||
except psycopg2.errors.SequenceGeneratorLimitExceeded:
|
||||
pass
|
||||
else:
|
||||
self.fail("NEXTVAL didn't raise any error upon sequence's end")
|
||||
|
||||
# without this, we're not able to move further
|
||||
# as postgres doesn't allow moving further in a transaction
|
||||
# when an error occurs
|
||||
frappe.db.rollback()
|
||||
|
||||
seq_name = self.generate_sequence_name()
|
||||
frappe.db.create_sequence(seq_name, min_value=10, max_value=20, increment_by=5, temporary=True)
|
||||
self.assertEqual(10, frappe.db.get_next_sequence_val(seq_name))
|
||||
self.assertEqual(15, frappe.db.get_next_sequence_val(seq_name))
|
||||
self.assertEqual(20, frappe.db.get_next_sequence_val(seq_name))
|
||||
34
frappe/tests/test_test_utils.py
Normal file
34
frappe/tests/test_test_utils.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
|
||||
|
||||
class TestTestUtils(FrappeTestCase):
|
||||
SHOW_TRANSACTION_COMMIT_WARNINGS = True
|
||||
|
||||
def test_document_assertions(self):
|
||||
|
||||
currency = frappe.new_doc("Currency")
|
||||
currency.currency_name = "STONKS"
|
||||
currency.smallest_currency_fraction_value = 0.420_001
|
||||
currency.save()
|
||||
|
||||
self.assertDocumentEqual(currency.as_dict(), currency)
|
||||
|
||||
def test_thread_locals(self):
|
||||
frappe.flags.temp_flag_to_be_discarded = True
|
||||
|
||||
def test_temp_setting_changes(self):
|
||||
current_setting = frappe.get_system_settings("logout_on_password_reset")
|
||||
|
||||
with change_settings("System Settings", {"logout_on_password_reset": int(not current_setting)}):
|
||||
updated_settings = frappe.get_system_settings("logout_on_password_reset")
|
||||
self.assertNotEqual(current_setting, updated_settings)
|
||||
|
||||
restored_settings = frappe.get_system_settings("logout_on_password_reset")
|
||||
self.assertEqual(current_setting, restored_settings)
|
||||
|
||||
|
||||
def tearDownModule():
|
||||
"""assertions for ensuring tests didn't leave state behind"""
|
||||
assert "temp_flag_to_be_discarded" not in frappe.flags
|
||||
assert not frappe.db.exists("Currency", "STONKS")
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue